merge: pull latest main into SaaS

This commit is contained in:
Anthony Stirling
2026-06-08 16:28:14 +01:00
94 changed files with 3798 additions and 430 deletions
@@ -39,13 +39,15 @@ export function OverviewHeader() {
</Text>
{user?.email && (
<Text size="xs" c="dimmed" mt="0.25rem">
Signed in as: {user.email}
{t("account.overview.signedInAs", "Signed in as: {{email}}", {
email: user.email,
})}
</Text>
)}
</div>
{user && (
<Button color="red" variant="filled" onClick={handleLogout}>
Log out
{t("account.overview.logOut", "Log out")}
</Button>
)}
</div>
@@ -809,7 +809,10 @@ export default function AdminAdvancedSection() {
},
})
}
placeholder="Default: java.io.tmpdir/stirling-pdf"
placeholder={t(
"admin.settings.advanced.tempFileManagement.baseTmpDir.placeholder",
"Default: java.io.tmpdir/stirling-pdf",
)}
disabled={!loginEnabled}
/>
</div>
@@ -834,7 +837,10 @@ export default function AdminAdvancedSection() {
},
})
}
placeholder="Default: baseTmpDir/libreoffice"
placeholder={t(
"admin.settings.advanced.tempFileManagement.libreofficeDir.placeholder",
"Default: baseTmpDir/libreoffice",
)}
disabled={!loginEnabled}
/>
</div>
@@ -859,7 +865,10 @@ export default function AdminAdvancedSection() {
},
})
}
placeholder="System temp directory path"
placeholder={t(
"admin.settings.advanced.tempFileManagement.systemTempDir.placeholder",
"System temp directory path",
)}
disabled={!loginEnabled}
/>
</div>
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { isAxiosError } from "axios";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
NumberInput,
@@ -52,6 +53,7 @@ interface DatabaseSettingsData {
export default function AdminDatabaseSection() {
const { t } = useTranslation();
const navigate = useNavigate();
const { loginEnabled, validateLoginEnabled, getDisabledStyles } =
useLoginRequired();
const {
@@ -462,7 +464,16 @@ export default function AdminDatabaseSection() {
)}
</Text>
</div>
<Badge color="grape" size="lg">
<Badge
color="grape"
size="lg"
style={{ cursor: "pointer" }}
onClick={() => navigate("/settings/adminPlan")}
title={t(
"admin.settings.badge.clickToUpgrade",
"Click to view plan details",
)}
>
ENTERPRISE
</Badge>
</Group>
@@ -698,7 +709,10 @@ export default function AdminDatabaseSection() {
onChange={(value) =>
setSettings({ ...settings, password: value })
}
placeholder="Enter database password"
placeholder={t(
"admin.settings.database.password.placeholder",
"Enter database password",
)}
disabled={!loginEnabled}
/>
</div>
@@ -296,7 +296,10 @@ export default function AdminEndpointsSection() {
}))}
searchable
clearable
placeholder="Select endpoints to disable"
placeholder={t(
"admin.settings.endpoints.toRemove.placeholder",
"Select endpoints to disable",
)}
comboboxProps={{ zIndex: 1400 }}
disabled={!loginEnabled}
/>
@@ -330,7 +333,10 @@ export default function AdminEndpointsSection() {
}))}
searchable
clearable
placeholder="Select groups to disable"
placeholder={t(
"admin.settings.endpoints.groupsToRemove.placeholder",
"Select groups to disable",
)}
comboboxProps={{ zIndex: 1400 }}
disabled={!loginEnabled}
/>
@@ -286,7 +286,10 @@ export default function AdminMailSection() {
onChange={(value) =>
setSettings({ ...settings, password: value })
}
placeholder="Enter SMTP password"
placeholder={t(
"admin.settings.mail.password.placeholder",
"Enter SMTP password",
)}
/>
</div>
@@ -183,8 +183,14 @@ const AdminPlanSection: React.FC = () => {
if (!plans || plans.length === 0) {
return (
<Alert color="yellow" title="No data available">
Plans data is not available at the moment.
<Alert
color="yellow"
title={t("admin.settings.plan.noData.title", "No data available")}
>
{t(
"admin.settings.plan.noData.message",
"Plans data is not available at the moment.",
)}
</Alert>
);
}
@@ -1,5 +1,5 @@
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import {
TextInput,
Switch,
@@ -132,17 +132,29 @@ export default function AdminPremiumSection() {
</List.Item>
<List.Item>
<Text size="sm" component="span">
<strong>Custom Metadata</strong> (PRO) - General
<Trans
i18nKey="admin.settings.premium.movedFeatures.customMetadata"
defaults="<0>Custom Metadata</0> (PRO) - General"
components={[<strong />]}
/>
</Text>
</List.Item>
<List.Item>
<Text size="sm" component="span">
<strong>Audit Logging</strong> (ENTERPRISE) - Security
<Trans
i18nKey="admin.settings.premium.movedFeatures.auditLogging"
defaults="<0>Audit Logging</0> (ENTERPRISE) - Security"
components={[<strong />]}
/>
</Text>
</List.Item>
<List.Item>
<Text size="sm" component="span">
<strong>Database Configuration</strong> (ENTERPRISE) - Database
<Trans
i18nKey="admin.settings.premium.movedFeatures.databaseConfiguration"
defaults="<0>Database Configuration</0> (ENTERPRISE) - Database"
components={[<strong />]}
/>
</Text>
</List.Item>
</List>
@@ -1,4 +1,5 @@
import { useCallback, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
NumberInput,
@@ -67,6 +68,7 @@ interface SecuritySettingsData {
export default function AdminSecuritySection() {
const { t } = useTranslation();
const navigate = useNavigate();
const { loginEnabled, validateLoginEnabled } = useLoginRequired();
const {
restartModalOpened,
@@ -817,7 +819,16 @@ export default function AdminSecuritySection() {
<Text fw={600} size="sm">
{t("admin.settings.security.audit.label", "Audit Logging")}
</Text>
<Badge color="grape" size="sm">
<Badge
color="grape"
size="sm"
style={{ cursor: "pointer" }}
onClick={() => navigate("/settings/adminPlan")}
title={t(
"admin.settings.badge.clickToUpgrade",
"Click to view plan details",
)}
>
ENTERPRISE
</Badge>
</Group>
@@ -11,7 +11,10 @@ import {
import { useTranslation } from "react-i18next";
import { alert } from "@app/components/toast";
import { LicenseInfo, mapLicenseToTier } from "@app/services/licenseService";
import { PLAN_FEATURES, PLAN_HIGHLIGHTS } from "@app/constants/planConstants";
import {
usePlanFeatures,
usePlanHighlights,
} from "@app/constants/planConstants";
import FeatureComparisonTable from "@app/components/shared/config/configSections/plan/FeatureComparisonTable";
import StaticCheckoutModal from "@app/components/shared/config/configSections/plan/StaticCheckoutModal";
import LicenseKeySection from "@app/components/shared/config/configSections/plan/LicenseKeySection";
@@ -32,6 +35,8 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({
currentLicenseInfo,
}) => {
const { t } = useTranslation();
const planFeatures = usePlanFeatures();
const planHighlights = usePlanHighlights();
const [showComparison, setShowComparison] = useState(false);
// Static checkout modal state
@@ -88,8 +93,8 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({
price: 0,
currency: "£",
period: "",
highlights: PLAN_HIGHLIGHTS.FREE,
features: PLAN_FEATURES.FREE,
highlights: planHighlights.FREE,
features: planFeatures.FREE,
maxUsers: 5,
},
{
@@ -99,8 +104,8 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({
currency: "",
period: "",
popular: false,
highlights: PLAN_HIGHLIGHTS.SERVER_MONTHLY,
features: PLAN_FEATURES.SERVER,
highlights: planHighlights.SERVER_MONTHLY,
features: planFeatures.SERVER,
maxUsers: "Unlimited users",
},
{
@@ -109,8 +114,8 @@ const StaticPlanSection: React.FC<StaticPlanSectionProps> = ({
price: 0,
currency: "",
period: "",
highlights: PLAN_HIGHLIGHTS.ENTERPRISE_MONTHLY,
features: PLAN_FEATURES.ENTERPRISE,
highlights: planHighlights.ENTERPRISE_MONTHLY,
features: planFeatures.ENTERPRISE,
maxUsers: "Custom",
},
];
@@ -185,14 +185,20 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
return (
<Stack align="center" justify="center" p="xl">
<Loader size="lg" />
<Text c="dimmed">Loading session...</Text>
<Text c="dimmed">
{t("workflow.participant.loadingSession", "Loading session...")}
</Text>
</Stack>
);
}
if (error) {
return (
<Alert icon={<InfoIcon fontSize="small" />} color="red" title="Error">
<Alert
icon={<InfoIcon fontSize="small" />}
color="red"
title={t("workflow.participant.errorTitle", "Error")}
>
{error}
</Alert>
);
@@ -201,7 +207,10 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
if (!session || !participant) {
return (
<Alert icon={<InfoIcon fontSize="small" />} color="orange">
Session not found or access denied.
{t(
"workflow.participant.sessionNotFound",
"Session not found or access denied.",
)}
</Alert>
);
}
@@ -209,15 +218,35 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
const getStatusBadge = (status: string) => {
switch (status) {
case "SIGNED":
return <Badge color="green">Signed</Badge>;
return (
<Badge color="green">
{t("workflow.participant.statusSigned", "Signed")}
</Badge>
);
case "DECLINED":
return <Badge color="red">Declined</Badge>;
return (
<Badge color="red">
{t("workflow.participant.statusDeclined", "Declined")}
</Badge>
);
case "VIEWED":
return <Badge color="blue">Viewed</Badge>;
return (
<Badge color="blue">
{t("workflow.participant.statusViewed", "Viewed")}
</Badge>
);
case "NOTIFIED":
return <Badge color="yellow">Notified</Badge>;
return (
<Badge color="yellow">
{t("workflow.participant.statusNotified", "Notified")}
</Badge>
);
case "PENDING":
return <Badge color="gray">Pending</Badge>;
return (
<Badge color="gray">
{t("workflow.participant.statusPending", "Pending")}
</Badge>
);
default:
return <Badge>{status}</Badge>;
}
@@ -254,7 +283,9 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
{session.documentName}
</Text>
<Text size="sm" c="dimmed">
From: {session.ownerUsername}
{t("workflow.participant.from", "From: {{name}}", {
name: session.ownerUsername,
})}
</Text>
</div>
{getStatusBadge(participant.status)}
@@ -272,7 +303,9 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
{session.dueDate && (
<Text size="sm" c="dimmed">
Due Date: {session.dueDate}
{t("workflow.participant.dueDate", "Due Date: {{date}}", {
date: session.dueDate,
})}
</Text>
)}
@@ -283,7 +316,7 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
onClick={() => downloadDocument(token)}
variant="light"
>
Download Document
{t("workflow.participant.downloadDocument", "Download Document")}
</Button>
</Group>
</Stack>
@@ -293,17 +326,35 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
<Card shadow="sm" padding="md" radius="md" withBorder>
<Stack gap="md">
<Text fw={500} size="lg">
Sign Document
{t("workflow.participant.signDocument", "Sign Document")}
</Text>
<Select
label="Certificate Type"
label={t(
"workflow.participant.certificateType",
"Certificate Type",
)}
value={certType}
onChange={(value) => setCertType(value || "P12")}
data={[
{ value: "P12", label: "P12/PKCS12 Certificate" },
{ value: "JKS", label: "JKS Keystore" },
{ value: "SERVER", label: "Server Certificate (if available)" },
{
value: "P12",
label: t(
"workflow.participant.certTypeP12",
"P12/PKCS12 Certificate",
),
},
{
value: "JKS",
label: t("workflow.participant.certTypeJks", "JKS Keystore"),
},
{
value: "SERVER",
label: t(
"workflow.participant.certTypeServer",
"Server Certificate (if available)",
),
},
]}
size="sm"
data-testid="cert-type-select"
@@ -312,8 +363,14 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
{certType !== "SERVER" && (
<>
<FileInput
label="Certificate File"
placeholder="Select certificate file"
label={t(
"workflow.participant.certificateFile",
"Certificate File",
)}
placeholder={t(
"workflow.participant.certificateFilePlaceholder",
"Select certificate file",
)}
value={certFile}
onChange={setCertFile}
accept=".p12,.pfx,.jks"
@@ -322,7 +379,10 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
/>
<TextInput
label="Certificate Password"
label={t(
"workflow.participant.certificatePassword",
"Certificate Password",
)}
type="password"
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
@@ -386,23 +446,32 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
)}
<TextInput
label="Location"
placeholder="e.g., San Francisco, CA"
label={t("workflow.participant.location", "Location")}
placeholder={t(
"workflow.participant.locationPlaceholder",
"e.g., San Francisco, CA",
)}
value={location}
onChange={(e) => setLocation(e.currentTarget.value)}
size="sm"
/>
<TextInput
label="Reason"
placeholder="e.g., Document approval"
label={t("workflow.participant.reason", "Reason")}
placeholder={t(
"workflow.participant.reasonPlaceholder",
"e.g., Document approval",
)}
value={reason}
onChange={(e) => setReason(e.currentTarget.value)}
size="sm"
/>
<TextInput
label="Page Number (optional)"
label={t(
"workflow.participant.pageNumber",
"Page Number (optional)",
)}
type="number"
value={pageNumber}
onChange={(e) =>
@@ -423,7 +492,7 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
color="green"
data-testid="submit-signature-button"
>
Submit Signature
{t("workflow.participant.submitSignature", "Submit Signature")}
</Button>
<Button
@@ -433,7 +502,7 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
variant="light"
data-testid="decline-button"
>
Decline
{t("workflow.participant.decline", "Decline")}
</Button>
</Group>
</Stack>
@@ -442,14 +511,24 @@ const ParticipantView: React.FC<ParticipantViewProps> = ({ token }) => {
{participant.hasCompleted && (
<Alert icon={<CheckCircleIcon fontSize="small" />} color="green">
You have {participant.status === "SIGNED" ? "signed" : "declined"}{" "}
this document.
{participant.status === "SIGNED"
? t(
"workflow.participant.completedSigned",
"You have signed this document.",
)
: t(
"workflow.participant.completedDeclined",
"You have declined this document.",
)}
</Alert>
)}
{participant.isExpired && (
<Alert icon={<InfoIcon fontSize="small" />} color="orange">
Your access to this document has expired.
{t(
"workflow.participant.accessExpired",
"Your access to this document has expired.",
)}
</Alert>
)}
</Stack>