From 8e485801c98050b4673dc8aa2f785ec1700df6bc Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:57:17 +0100 Subject: [PATCH 1/6] change policies ui (#6683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Removed colors from policies to make them look more professional. • upgraded to enterprise link to contact us. • Hid inactive policies from users (Kept for admin and team lead). • Closing policies had wrong arrow, made a standard component for chat, tools and policies header. --- .../public/locales/en-US/translation.toml | 2 + .../components/policies/PoliciesSidebar.tsx | 5 + .../core/components/tools/RightSidebar.tsx | 130 +++++------ .../src/core/components/tools/ToolPanel.css | 61 ------ .../proprietary/components/chat/ChatPanel.css | 98 +-------- .../proprietary/components/chat/ChatPanel.tsx | 64 ++---- .../components/policies/Policies.css | 61 +++++- .../policies/PoliciesSidebar.test.tsx | 4 +- .../components/policies/PoliciesSidebar.tsx | 137 +++++++----- .../components/policies/PolicyDetailPanel.tsx | 5 +- .../components/policies/PolicySetupWizard.tsx | 30 ++- .../components/policies/policyStatus.ts | 4 +- frontend/shared/components/IconBadge.css | 21 +- frontend/shared/components/PanelHeader.css | 204 ++++++++++++++---- .../shared/components/PanelHeader.stories.tsx | 71 +++--- frontend/shared/components/PanelHeader.tsx | 181 +++++++++++----- 16 files changed, 591 insertions(+), 487 deletions(-) diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index 0bfb5749ce..84f9c26de5 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -5854,6 +5854,7 @@ routing = "Routing" security = "Security" [policies.detail] +close = "Close" editSettings = "Edit Settings" enforces = "Enforces" managedByOrg = "Managed by your organization. Contact a team leader to change this policy." @@ -5907,6 +5908,7 @@ allDocTypesTitle = "All document types" back = "Back" builderDesc = "Build the sequence of tools this policy runs on each document." clear = "Clear" +close = "Close" continue = "Continue" docTypesLabel = "Document types" edit = "Edit" diff --git a/frontend/editor/src/core/components/policies/PoliciesSidebar.tsx b/frontend/editor/src/core/components/policies/PoliciesSidebar.tsx index d787c6435f..9d9cbed2e2 100644 --- a/frontend/editor/src/core/components/policies/PoliciesSidebar.tsx +++ b/frontend/editor/src/core/components/policies/PoliciesSidebar.tsx @@ -14,6 +14,11 @@ export function usePoliciesEnabled(): boolean { return false; } +/** Whether the Policies list should appear for the current user. False in core. */ +export function usePoliciesVisible(): boolean { + return false; +} + /** * Whether a policy is open (its detail should take over the rail). Always false * in core; proprietary bridges to the policy-selection store. diff --git a/frontend/editor/src/core/components/tools/RightSidebar.tsx b/frontend/editor/src/core/components/tools/RightSidebar.tsx index 494553a690..bfd98d7b32 100644 --- a/frontend/editor/src/core/components/tools/RightSidebar.tsx +++ b/frontend/editor/src/core/components/tools/RightSidebar.tsx @@ -11,6 +11,7 @@ import { PoliciesSection, PolicyDetailTakeover, usePoliciesEnabled, + usePoliciesVisible, usePolicyDetailActive, } from "@app/components/policies/PoliciesSidebar"; import { PolicyAutoRunController } from "@app/components/policies/PolicyAutoRunController"; @@ -18,6 +19,7 @@ import { useFavoriteToolItems } from "@app/hooks/tools/useFavoriteToolItems"; import { useToolSections } from "@app/hooks/useToolSections"; import type { SubcategoryGroup } from "@app/hooks/useToolSections"; import { ToolIcon } from "@app/components/shared/ToolIcon"; +import { PanelHeader } from "@shared/components/PanelHeader"; import { Tooltip as AppTooltip } from "@app/components/shared/Tooltip"; import { withViewTransition } from "@app/utils/viewTransition"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; @@ -64,6 +66,7 @@ export default function RightSidebar() { } = useToolWorkflow(); const policiesEnabled = usePoliciesEnabled(); + const policiesVisible = usePoliciesVisible(); const rawPolicyDetailActive = usePolicyDetailActive(); const fullscreenExpanded = useIsFullscreenExpanded(); const fullscreenGeometry = useToolPanelGeometry({ @@ -115,9 +118,14 @@ export default function RightSidebar() { const inToolView = leftPanelView !== "toolPicker"; // Show X (close) button only when there's somewhere to go back to. const showCloseButton = inToolView || allToolsView; - // Policies sit above the tool list in the default tool-picker view. + // Policies sit above the tool list in the default tool-picker view — but only + // when the current user actually has policies to see (see usePoliciesVisible), + // so regular users with none get the plain tool picker with no empty block. const showPolicies = - policiesEnabled && !allToolsView && leftPanelView === "toolPicker"; + policiesEnabled && + policiesVisible && + !allToolsView && + leftPanelView === "toolPicker"; // When Policies are shown, the search moves OUT of the header to sit between // the Policies and Tools sections (separating them); otherwise it stays in the // header. Show the header search when there's a close button, or in the @@ -281,65 +289,67 @@ export default function RightSidebar() { ) : ( <> - {!showPolicies && ( -
- {activeTool ? ( -
- - - - - {activeTool.name} - -
- ) : showHeaderSearch ? ( -
- -
- ) : null} - {showCloseButton ? ( - - - - ) : ( - - - - )} -
- )} + } + title={activeTool.name} + onClose={handleHeaderBack} + closeLabel={ + inToolView + ? t("toolPanel.backToAllTools", "Back to all tools") + : t("toolPanel.goBack", "Go back") + } + /> + ) : ( +
+ {showHeaderSearch ? ( +
+ +
+ ) : null} + {showCloseButton ? ( + + + + ) : ( + + + + )} +
+ ))} {showPolicies && ( -
- - - - - - } - onClick={clearChat} - disabled={messages.length === 0 && !isLoading} - > - {t("chat.header.clearChat", "Clear chat")} - - - - - - -
+ } + title={t("agents.stirling_name", "Stirling")} + loading={isLoading} + className="chat-panel__header" + barClassName="chat-panel__agent-pill-vt" + menuLabel={t("chat.header.agentMenu", "Stirling agent options")} + menuItems={[ + { + key: "clear-chat", + icon: , + label: t("chat.header.clearChat", "Clear chat"), + onClick: clearChat, + disabled: messages.length === 0 && !isLoading, + }, + ]} + onClose={onBack} + closeLabel={backLabel} + /> {showQuickActions && (
diff --git a/frontend/editor/src/proprietary/components/policies/Policies.css b/frontend/editor/src/proprietary/components/policies/Policies.css index 637e30d380..07fcb1865f 100644 --- a/frontend/editor/src/proprietary/components/policies/Policies.css +++ b/frontend/editor/src/proprietary/components/policies/Policies.css @@ -8,6 +8,27 @@ /* scaffolding + the collapsed rail; spacing snaps to the SUI --space-* */ /* scale and colour to the SUI token set so it reads as one product. */ +/* Dark mode only: remap SUI surface/border tokens to the app's neutral-grey values so policy cards read as one product with the rail; accent tokens are left alone. */ +[data-theme="dark"] .pol-list, +[data-theme="dark"] .pol-takeover, +[data-theme="dark"] .pol-detail, +[data-theme="dark"] .pol-crail { + --color-bg: var(--bg-toolbar); + --color-bg-alt: var(--bg-toolbar); + --color-bg-subtle: var(--bg-toolbar); + --color-surface: var(--bg-surface); + --color-surface-alt: #323942; + --color-bg-hover: #323942; + --color-bg-muted: var(--bg-surface); + --color-border: var(--border-default); + --color-border-light: var(--border-subtle); + --color-border-input: var(--border-strong); + --color-border-hover: var(--border-strong); + --color-divider: var(--border-subtle); + --color-dropdown-bg: var(--bg-surface); + --color-dropdown-border: var(--border-default); +} + /* ---- List ---- */ .pol-list { width: 100%; @@ -69,6 +90,19 @@ outline: 2px solid var(--color-blue); outline-offset: -2px; } +/* Policy icons are colourless at rest; hovering or focusing the row reveals the + category colour (blue/purple/green/amber/red — see ROW_ACCENT). The accent + class sets --ib-base; we neutralise --ib-accent here and restore it on hover. */ +.pol-row .sui-iconbadge { + --ib-accent: var(--color-text-3); + transition: + color var(--motion-fast), + background var(--motion-fast); +} +.pol-row:hover .sui-iconbadge, +.pol-row:focus-visible .sui-iconbadge { + --ib-accent: var(--ib-base); +} .pol-row-label { flex: 1; min-width: 0; @@ -126,20 +160,28 @@ text-decoration: underline; } -/* Locked "Coming soon" row — muted, not interactive. */ +/* Enterprise-only ("coming soon") row — shown to admins / team leaders but not + available on the current plan, so the whole box is dimmed to read as disabled. + The row itself isn't a button; its trailing "Upgrade to enterprise" link is. */ .pol-row--soon { - opacity: 0.55; cursor: default; + opacity: 0.55; } .pol-row--soon:hover { background: transparent; } -.pol-row-soon { +/* Trailing "Upgrade to enterprise" link → contact us. Greyed to match the + disabled row; still clickable for admins who want to enquire. */ +.pol-row-upgrade { font-size: 0.6875rem; - font-weight: 500; + font-weight: 600; color: var(--color-text-4); + text-decoration: none; white-space: nowrap; } +.pol-row-upgrade:hover { + text-decoration: underline; +} /* In-progress activity icon spins gently. */ .pol-spin { @@ -192,9 +234,17 @@ /* ---- Step indicator (wraps a SUI StepIndicator) ---- */ .pol-steps { - padding: var(--space-3) var(--space-5); + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0 var(--space-5) var(--space-3); border-bottom: 1px solid var(--color-border); } +.pol-step-label { + font-size: 0.75rem; + font-weight: 600; + color: var(--color-text-4); +} /* ---- Scroll body ---- */ .pol-scroll { @@ -211,6 +261,7 @@ line-height: 1.5; color: var(--color-text-4); margin: 0; + margin-bottom: var(--space-3); } .pol-section-label { font-size: 0.6875rem; diff --git a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx index c05680cfb7..2ab524b495 100644 --- a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx +++ b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.test.tsx @@ -169,11 +169,11 @@ describe("Policies right-sidebar surface", () => { expect(await screen.findByText("No activity yet")).toBeInTheDocument(); }); - it("returns to the list via the back button", () => { + it("returns to the list via the close button", () => { renderHost(); fireEvent.click(screen.getByText("Security")); expect(screen.getByText("Enforces")).toBeInTheDocument(); - fireEvent.click(screen.getByLabelText("Back")); + fireEvent.click(screen.getByLabelText("Close")); expect(screen.getByText("Policies")).toBeInTheDocument(); }); }); diff --git a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx index 8b6e551cde..0e87d9f982 100644 --- a/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx +++ b/frontend/editor/src/proprietary/components/policies/PoliciesSidebar.tsx @@ -61,6 +61,16 @@ export function usePoliciesEnabled(): boolean { return POLICIES_ENABLED; } +/** + * Whether the right rail should show the Policies section. + */ +export function usePoliciesVisible(): boolean { + const pol = usePolicies(); + const { categories } = usePolicyCatalog(); + if (!POLICIES_ENABLED) return false; + return pol.canConfigure || categories.some((c) => !c.comingSoon); +} + /** * Whether the current user is a guest who can't open or configure policies — * an anonymous user on a login-enabled deployment (i.e. a SaaS sign-up prompt @@ -122,6 +132,14 @@ export function PoliciesSection({ if (!POLICIES_ENABLED) return null; + // Admins / team leads see the full catalogue (coming-soon rows greyed as an + // enterprise upsell); regular users only see the live policies — the + // coming-soon "Upgrade to enterprise" rows are hidden from them. + const visibleCategories = pol.canConfigure + ? categories + : categories.filter((c) => !c.comingSoon); + if (visibleCategories.length === 0) return null; + // The header tally counts every CONFIGURED policy (active + paused), not just // the active ones. const configuredCount = categories.filter( @@ -170,14 +188,10 @@ export function PoliciesSection({ {expanded && ( <>
- {categories.map((cat) => { + {visibleCategories.map((cat) => { if (cat.comingSoon) { return ( -
+
{cat.icon} @@ -185,12 +199,17 @@ export function PoliciesSection({ {t(`policies.catalog.${cat.id}`, cat.label)} - + {t( "policies.sidebar.upgradeToEnterprise", "Upgrade to enterprise", )} - +
); @@ -457,58 +476,64 @@ export function PoliciesCollapsedButton({ if (!POLICIES_ENABLED) return null; + // Coming-soon policies are excluded; admins see all real policies, others only see configured ones — renders nothing when empty. + const railCategories = categories.filter((cat) => { + if (cat.comingSoon) return false; + if (pol.canConfigure) return true; + return pol.policies[cat.id]?.configured; + }); + if (railCategories.length === 0) return null; + return ( <>
- {categories - .filter((cat) => !cat.comingSoon) - .map((cat) => { - const status = deriveRowStatus(pol.policies[cat.id]); - const label = t(`policies.catalog.${cat.id}`, cat.label); - const statusLabel = t( - `policies.status.${status}`, - STATUS_LABEL[status], - ); - const suffix = - status === "active" - ? t("policies.sidebar.railSuffixActive", " (Active)") - : status === "paused" - ? t("policies.sidebar.railSuffixPaused", " (Paused)") - : ""; - return ( - { + const status = deriveRowStatus(pol.policies[cat.id]); + const label = t(`policies.catalog.${cat.id}`, cat.label); + const statusLabel = t( + `policies.status.${status}`, + STATUS_LABEL[status], + ); + const suffix = + status === "active" + ? t("policies.sidebar.railSuffixActive", " (Active)") + : status === "paused" + ? t("policies.sidebar.railSuffixPaused", " (Paused)") + : ""; + return ( + + - - ); - })} + {cat.icon} + {(status === "active" || status === "paused") && ( + + )} + + + ); + })}
diff --git a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx index 114f1ac16b..aed4062869 100644 --- a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx +++ b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx @@ -132,9 +132,10 @@ export function PolicyDetailPanel({
} - /> - } + onClose={onCancel} + closeLabel={t("cancel", "Cancel")} />
+ + {t("policies.wizard.stepOf", "Step {{step}} of {{total}}", { + step, + total: TOTAL_STEPS, + })} +
diff --git a/frontend/editor/src/proprietary/components/policies/policyStatus.ts b/frontend/editor/src/proprietary/components/policies/policyStatus.ts index d3c3d8ea7b..fab615ab25 100644 --- a/frontend/editor/src/proprietary/components/policies/policyStatus.ts +++ b/frontend/editor/src/proprietary/components/policies/policyStatus.ts @@ -17,7 +17,9 @@ export const STATUS_LABEL: Record = { setup: "Set up", }; -/** A soft tinted icon tile per category — gives each policy a calm identity colour. */ +/** + * Per-category accent colour + */ export const ROW_ACCENT: Record = { ingestion: "blue", security: "purple", diff --git a/frontend/shared/components/IconBadge.css b/frontend/shared/components/IconBadge.css index 2a510b282f..10e2d28915 100644 --- a/frontend/shared/components/IconBadge.css +++ b/frontend/shared/components/IconBadge.css @@ -4,6 +4,12 @@ justify-content: center; border-radius: var(--radius-md); flex-shrink: 0; + /* Resolved tint. Each accent class sets --ib-base; --ib-accent defaults to it + but a consumer can override --ib-accent alone (e.g. to neutralise the badge + until hover) without losing the per-accent base. */ + --ib-accent: var(--ib-base, var(--color-blue)); + color: var(--ib-accent); + background: color-mix(in srgb, var(--ib-accent) 14%, transparent); } .sui-iconbadge--sm { width: 1.75rem; @@ -14,22 +20,17 @@ height: 2rem; } .sui-iconbadge--blue { - color: var(--color-blue); - background: color-mix(in srgb, var(--color-blue) 14%, transparent); + --ib-base: var(--color-blue); } .sui-iconbadge--purple { - color: var(--color-purple); - background: color-mix(in srgb, var(--color-purple) 14%, transparent); + --ib-base: var(--color-purple); } .sui-iconbadge--green { - color: var(--color-green); - background: color-mix(in srgb, var(--color-green) 14%, transparent); + --ib-base: var(--color-green); } .sui-iconbadge--amber { - color: var(--color-amber); - background: color-mix(in srgb, var(--color-amber) 14%, transparent); + --ib-base: var(--color-amber); } .sui-iconbadge--red { - color: var(--color-red); - background: color-mix(in srgb, var(--color-red) 14%, transparent); + --ib-base: var(--color-red); } diff --git a/frontend/shared/components/PanelHeader.css b/frontend/shared/components/PanelHeader.css index dabffb27d5..9869b56be3 100644 --- a/frontend/shared/components/PanelHeader.css +++ b/frontend/shared/components/PanelHeader.css @@ -1,54 +1,168 @@ +/* ===================== PanelHeader ===================== */ +/* The header shared by the active-tool panel, the AI chat panel and the */ +/* Policies detail/wizard. Mirrors the AI chat header treatment so it reads well */ +/* in both light and dark mode (thin border, no heavy fill). */ + .sui-panelhdr { display: flex; align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1rem 1.25rem; - border-bottom: 1px solid var(--color-border); -} -.sui-panelhdr__left { - display: flex; - align-items: center; - gap: 0.75rem; - min-width: 0; -} -.sui-panelhdr__back { - display: inline-flex; - align-items: center; - justify-content: center; + gap: 0.5rem; + padding: 1rem 1rem 0.75rem; flex-shrink: 0; - width: 1.875rem; - height: 1.875rem; - border: 1px solid var(--color-border); - border-radius: 50%; - background: var(--color-surface); - color: var(--color-text-2); - cursor: pointer; - transition: - background var(--motion-fast), - color var(--motion-fast), - border-color var(--motion-fast); } -.sui-panelhdr__back:hover { - background: var(--color-bg-hover); - border-color: var(--color-text-4); - color: var(--color-text-1); -} -.sui-panelhdr__text { - min-width: 0; -} -.sui-panelhdr__title { - font-size: 1.0625rem; - font-weight: 600; - color: var(--color-text-1); -} -.sui-panelhdr__sub { - font-size: 0.8125rem; - color: var(--color-text-4); - margin-top: 0.125rem; -} -.sui-panelhdr__actions { + +.sui-panelhdr__bar { display: inline-flex; align-items: center; gap: 0.5rem; + flex: 1; + min-width: 0; + padding: 0.4rem 0.75rem 0.4rem 0.4rem; + border: 1px solid var(--border-subtle, var(--mantine-color-default-border)); + border-radius: 9999px; + background: var(--mantine-color-body); + text-align: left; + color: inherit; +} + +/* Only the menu-trigger variant is interactive. */ +button.sui-panelhdr__bar { + cursor: pointer; + transition: + background 120ms ease-out, + border-color 120ms ease-out; +} + +button.sui-panelhdr__bar:hover { + background: var(--mantine-color-default-hover); +} + +.sui-panelhdr__icon { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + border-radius: 9999px; + background: var(--mantine-color-blue-light); + color: var(--mantine-color-blue-filled); + flex-shrink: 0; +} + +.sui-panelhdr__icon svg { + font-size: 1rem; + width: 1rem; + height: 1rem; +} + +/* ToolIcon wraps its glyph in .tool-button-icon with its own margin/transform; + reset them so the glyph sits dead-centre in the circular badge. */ +.sui-panelhdr__icon .tool-button-icon { + margin: 0 !important; + transform: none !important; + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; +} + +.sui-panelhdr__label { + flex: 1; + min-width: 0; + font-size: 0.9rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--mantine-color-text); +} + +.sui-panelhdr__chevron { + flex-shrink: 0; + color: var(--mantine-color-dimmed); +} + +/* Right-aligned content inside the bar (e.g. a status badge); the flexible + label pushes it to the trailing edge of the pill. */ +.sui-panelhdr__actions { + display: inline-flex; + align-items: center; + flex-shrink: 0; +} + +/* Running / in-progress status dot, anchored to the header icon. */ +.sui-panelhdr__dot { + position: absolute; + bottom: 0; + right: 0; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--mantine-color-blue-5); + border: 1.5px solid var(--mantine-color-body); + animation: sui-panelhdr-dot-pulse 2.4s ease-in-out infinite; + pointer-events: none; +} + +@keyframes sui-panelhdr-dot-pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.45; + } +} + +@media (prefers-reduced-motion: reduce) { + .sui-panelhdr__dot { + animation: none; + } +} + +.sui-panelhdr__bar--loading { + border-color: color-mix( + in srgb, + var(--mantine-color-blue-5) 60%, + var(--border-subtle, var(--mantine-color-default-border)) + ); +} + +/* Dark mode: let the header blend into the rail — just a thin border, no fill — + so it doesn't read as a clashing lighter card on the dark toolbar. */ +[data-mantine-color-scheme="dark"] .sui-panelhdr__bar { + background: transparent; + border-color: var(--border-subtle, var(--mantine-color-default-border)); +} + +[data-mantine-color-scheme="dark"] button.sui-panelhdr__bar:hover { + background: rgba(255, 255, 255, 0.04); +} + +[data-mantine-color-scheme="dark"] .sui-panelhdr__bar--loading { + border-color: color-mix( + in srgb, + var(--mantine-color-blue-4) 55%, + var(--border-subtle, var(--mantine-color-default-border)) + ); +} + +[data-mantine-color-scheme="dark"] .sui-panelhdr__icon { + background: color-mix( + in srgb, + var(--mantine-color-blue-filled) 18%, + transparent + ); + color: var(--mantine-color-blue-3, var(--mantine-color-blue-filled)); +} + +/* Dark mode: the subtle gray close button is too dim against the dark rail — + brighten it to a clearly-visible light grey (near-white on hover). */ +[data-mantine-color-scheme="dark"] .sui-panelhdr__close { + color: var(--mantine-color-gray-4); +} + +[data-mantine-color-scheme="dark"] .sui-panelhdr__close:hover { + color: var(--mantine-color-gray-2); } diff --git a/frontend/shared/components/PanelHeader.stories.tsx b/frontend/shared/components/PanelHeader.stories.tsx index 1aa37b9556..02cf26360a 100644 --- a/frontend/shared/components/PanelHeader.stories.tsx +++ b/frontend/shared/components/PanelHeader.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined"; +import DeleteSweepIcon from "@mui/icons-material/DeleteSweep"; import { PanelHeader } from "@shared/components/PanelHeader"; -import { Button } from "@shared/components/Button"; import { StatusBadge } from "@shared/components/StatusBadge"; const meta: Meta = { @@ -9,53 +10,53 @@ const meta: Meta = { tags: ["autodocs"], parameters: { layout: "padded" }, args: { - title: "Pipeline detail", - subtitle: "COI Compliance · us-east-1", + icon: , + title: "Security", + closeLabel: "Close", }, - argTypes: { onBack: { action: "back" } }, + argTypes: { onClose: { action: "close" } }, }; export default meta; type Story = StoryObj; -/** Toggle title / subtitle / onBack / actions in controls. */ +/** Plain header pill with a trailing close button. */ export const Playground: Story = {}; -export const WithActions: Story = { +/** Category-accented icon badge (blue / purple / green / amber / red). */ +export const Accented: Story = { + args: { accent: "purple" }, +}; + +/** Dropdown trigger — a disclosure chevron appears and clicking the pill opens + * the menu (e.g. the chat header's "Clear chat"). */ +export const WithMenu: Story = { args: { - subtitle: "Last deploy 14m ago · golden set 48/48", - actions: ( - <> - - Healthy - - - - - ), + title: "Stirling", + menuLabel: "Stirling agent options", + menuItems: [ + { + key: "clear", + icon: , + label: "Clear chat", + onClick: () => {}, + }, + ], }, }; -export const Everything: Story = { +/** Loading state — pulsing status dot on the icon + a tinted border. */ +export const Loading: Story = { + args: { title: "Stirling", loading: true }, +}; + +/** Right-aligned actions rendered before the close button. */ +export const WithActions: Story = { args: { - title: "Pipeline detail — COI Compliance", - subtitle: "Forked from Compliance Pack · 1,287 docs / 24h", - onBack: () => {}, + accent: "purple", actions: ( - <> - - Healthy - - - - + + Active + ), }, }; diff --git a/frontend/shared/components/PanelHeader.tsx b/frontend/shared/components/PanelHeader.tsx index e65b21f376..1fcedd88ac 100644 --- a/frontend/shared/components/PanelHeader.tsx +++ b/frontend/shared/components/PanelHeader.tsx @@ -1,73 +1,152 @@ -import type { ReactNode } from "react"; +import type { CSSProperties, ReactNode } from "react"; +import { ActionIcon, Menu } from "@mantine/core"; +import CloseIcon from "@mui/icons-material/Close"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import type { IconBadgeAccent } from "@shared/components/IconBadge"; import "@shared/components/PanelHeader.css"; -import { IconBadge } from "@shared/components/IconBadge"; + +export interface PanelHeaderMenuItem { + /** Stable key; falls back to the item index. */ + key?: string; + /** Optional leading glyph. */ + icon?: ReactNode; + label: ReactNode; + onClick: () => void; + disabled?: boolean; +} export interface PanelHeaderProps { + /** Glyph rendered in the tinted circular badge at the header's leading edge. */ + icon: ReactNode; + /** Header title. */ title: ReactNode; - /** Sub-heading below the title. */ - subtitle?: ReactNode; - /** Show a back chevron and trigger this callback when clicked. */ - onBack?: () => void; - /** Optional leading visual (e.g. a category glyph) shown in a tinted box. */ - icon?: ReactNode; - /** Accent tint for the leading icon box. Defaults to blue. */ - iconAccent?: "blue" | "purple" | "green" | "amber" | "red"; - /** Right-aligned action buttons / chips. */ + /** Close (X) handler. The trailing close button renders only when supplied. */ + onClose?: () => void; + /** aria-label for the close button. */ + closeLabel?: string; + /** + * When provided, the header becomes a dropdown trigger: a disclosure chevron is + * shown and clicking it opens a menu of these items (e.g. "Clear chat"). + */ + menuItems?: PanelHeaderMenuItem[]; + /** aria-label for the header when it acts as a menu trigger. */ + menuLabel?: string; + /** + * Tints the icon badge with a category colour (blue/purple/green/amber/red). + * Defaults to the standard blue when omitted (tool + AI chat headers). + */ + accent?: IconBadgeAccent; + /** Shows a pulsing status dot on the icon + a tinted border (e.g. AI running). */ + loading?: boolean; + /** Right-aligned content rendered inside the header bar, after the title + * (e.g. a status badge). */ actions?: ReactNode; + /** Applied to the inner header element — e.g. to set a view-transition-name. */ + barClassName?: string; + /** Applied to the outer header container. */ className?: string; } /** - * Header strip used by drill-down panels (admin tabs, agent detail, settings - * sub-pages). Back chevron renders only when `onBack` is supplied; an optional - * leading `icon` renders in a tinted box before the title. + * The header shared by the rail surfaces — the active tool panel, the AI chat + * panel, and the Policies detail/wizard. A tinted icon badge + title sit in a + * rounded bar, with an optional dropdown menu and a trailing close button. The + * styling stays legible in dark mode (thin border, no heavy fill) across every + * surface. */ export function PanelHeader({ - title, - subtitle, - onBack, icon, - iconAccent = "blue", + title, + onClose, + closeLabel, + menuItems, + menuLabel, + accent, + loading = false, actions, + barClassName, className, }: PanelHeaderProps) { + const hasMenu = menuItems != null && menuItems.length > 0; + + // Tint the icon badge with the category colour when an accent is given. Inline + // so it wins over the default blue treatment in both light and dark mode; the + // --color-* tokens are theme-aware and match the badge tint used elsewhere. + const iconStyle: CSSProperties | undefined = accent + ? { + color: `var(--color-${accent})`, + background: `color-mix(in srgb, var(--color-${accent}) 14%, transparent)`, + } + : undefined; + + const barClasses = [ + "sui-panelhdr__bar", + loading ? "sui-panelhdr__bar--loading" : "", + barClassName ?? "", + ] + .filter(Boolean) + .join(" "); + + const barBody = ( + <> + + {icon} + {loading && } + + {title} + {actions != null && ( + {actions} + )} + {hasMenu && ( + + )} + + ); + return (
-
- {onBack && ( - - )} - {icon && ( - - {icon} - - )} -
-
{title}
- {subtitle &&
{subtitle}
} -
-
- {actions &&
{actions}
} + {hasMenu ? ( + + + + + + {(menuItems ?? []).map((item, i) => ( + + {item.label} + + ))} + + + ) : ( +
{barBody}
+ )} + + {onClose && ( + + + + )}
); } From 41181c9da197c670bf46b46569dd2e67ba8d242d Mon Sep 17 00:00:00 2001 From: James Brunton Date: Tue, 23 Jun 2026 16:44:52 +0100 Subject: [PATCH 2/6] Redesign tool config types to avoid `any` typing (#6582) # Description of Changes Fixes one of the main causes of `any` typing left in tools, the way that we register tool parameters in the registry. Currently, it just accepts tool params via `any`, but instead we can explicitly change them to `Record void; + onSave: (parameters: ErasedToolParams) => void; onCancel: () => void; toolRegistry: Partial; } @@ -40,7 +40,7 @@ export default function ToolConfigurationModal({ }: ToolConfigurationModalProps) { const { t } = useTranslation(); - const [parameters, setParameters] = useState({}); + const [parameters, setParameters] = useState({}); // Get tool info from registry const toolInfo = toolRegistry[tool.operation as ToolId]; @@ -74,26 +74,11 @@ export default function ToolConfigurationModal({ ); } - // Special handling for ConvertSettings which needs additional props - if (tool.operation === "convert") { - return ( - { - setParameters((prev: any) => ({ ...prev, [key]: value })); - }} - getAvailableToExtensions={getAvailableToExtensions} - selectedFiles={[]} - disabled={false} - /> - ); - } - return ( { - setParameters((prev: any) => ({ ...prev, [key]: value })); + onParameterChange={(key, value) => { + setParameters((prev) => ({ ...prev, [key]: value })); }} disabled={false} /> diff --git a/frontend/editor/src/core/components/tools/automate/ToolList.tsx b/frontend/editor/src/core/components/tools/automate/ToolList.tsx index 7345f0a9b0..e57e96daa3 100644 --- a/frontend/editor/src/core/components/tools/automate/ToolList.tsx +++ b/frontend/editor/src/core/components/tools/automate/ToolList.tsx @@ -18,7 +18,7 @@ interface ToolListProps { onToolConfigure: (index: number) => void; onToolAdd: () => void; getToolName: (operation: string) => string; - getToolDefaultParameters: (operation: string) => Record; + getToolDefaultParameters: (operation: string) => Record; } export default function ToolList({ diff --git a/frontend/editor/src/core/components/tools/convert/ConvertSettings.tsx b/frontend/editor/src/core/components/tools/convert/ConvertSettings.tsx index 8b72e20ad5..5c0843660f 100644 --- a/frontend/editor/src/core/components/tools/convert/ConvertSettings.tsx +++ b/frontend/editor/src/core/components/tools/convert/ConvertSettings.tsx @@ -10,7 +10,11 @@ import { import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import { useTranslation } from "react-i18next"; import { useMultipleEndpointsEnabled } from "@app/hooks/useEndpointConfig"; -import { isImageFormat, isWebFormat } from "@app/utils/convertUtils"; +import { + isImageFormat, + isWebFormat, + getAvailableToExtensions as defaultGetAvailableToExtensions, +} from "@app/utils/convertUtils"; import { getConversionEndpoints } from "@app/data/toolsTaxonomy"; import { useFileSelection } from "@app/contexts/FileContext"; import { useFileState } from "@app/contexts/FileContext"; @@ -47,18 +51,18 @@ interface ConvertSettingsProps { key: K, value: ConvertParameters[K], ) => void; - getAvailableToExtensions: ( + getAvailableToExtensions?: ( fromExtension: string, ) => Array<{ value: string; label: string; group: string }>; - selectedFiles: StirlingFile[]; + selectedFiles?: StirlingFile[]; disabled?: boolean; } const ConvertSettings = ({ parameters, onParameterChange, - getAvailableToExtensions, - selectedFiles, + getAvailableToExtensions = defaultGetAvailableToExtensions, + selectedFiles = [], disabled = false, }: ConvertSettingsProps) => { const { t } = useTranslation(); diff --git a/frontend/editor/src/core/components/tools/shared/FilesToolStep.tsx b/frontend/editor/src/core/components/tools/shared/FilesToolStep.tsx index bca4f6aaa8..19701c2755 100644 --- a/frontend/editor/src/core/components/tools/shared/FilesToolStep.tsx +++ b/frontend/editor/src/core/components/tools/shared/FilesToolStep.tsx @@ -10,14 +10,20 @@ export interface FilesToolStepProps { minFiles?: number; } -export function createFilesToolStep( +interface StepBaseProps { + isVisible?: boolean; + isCollapsed?: boolean; + onCollapsedClick?: () => void; +} + +export function createFilesToolStep( createStep: ( title: string, - props: any, + props: StepBaseProps, children?: React.ReactNode, - ) => React.ReactElement, + ) => T, props: FilesToolStepProps, -): React.ReactElement { +): T { return createStep( i18n.t("files.title", "Files"), { diff --git a/frontend/editor/src/core/data/toolsTaxonomy.ts b/frontend/editor/src/core/data/toolsTaxonomy.ts index b12ab38813..18b8561535 100644 --- a/frontend/editor/src/core/data/toolsTaxonomy.ts +++ b/frontend/editor/src/core/data/toolsTaxonomy.ts @@ -1,6 +1,10 @@ import { type TFunction } from "i18next"; import React from "react"; -import { ToolOperationConfig } from "@app/hooks/tools/shared/toolOperationTypes"; +import { + type ErasedToolParams, + type ToolAutomationSettingsProps, + ToolOperationConfig, +} from "@app/hooks/tools/shared/toolOperationTypes"; import { BaseToolProps } from "@app/types/tool"; import { WorkbenchType } from "@app/types/workbench"; import { @@ -58,10 +62,13 @@ export type ToolRegistryEntry = { kind?: ToolKind; // Workbench type for navigation workbench?: WorkbenchType; - // Operation configuration for automation - operationConfig?: ToolOperationConfig; - // Settings component for automation configuration - automationSettings: React.ComponentType | null; + // Operation configuration for automation. TParams is erased at the registry + // boundary; tools are authored type-safely via defineToolAutomation. + operationConfig?: ToolOperationConfig; + // Settings component for automation configuration. + automationSettings: React.ComponentType< + ToolAutomationSettingsProps + > | null; // Whether this tool supports automation (defaults to true) supportsAutomate?: boolean; // Synonyms for search (optional) diff --git a/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx b/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx index 74b676dceb..605c6a3925 100644 --- a/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx +++ b/frontend/editor/src/core/data/useTranslatedToolRegistry.tsx @@ -12,6 +12,10 @@ import { LinkToolRegistry, } from "@app/data/toolsTaxonomy"; import { isSuperToolId, isLinkToolId } from "@app/types/toolId"; +import { + asRegistryConfig, + lazySettings, +} from "@app/hooks/tools/shared/toolOperationTypes"; import { adjustContrastOperationConfig } from "@app/hooks/tools/adjustContrast/useAdjustContrastOperation"; import { getSynonyms } from "@app/utils/toolSynonyms"; import { useProprietaryToolRegistry } from "@app/data/useProprietaryToolRegistry"; @@ -143,8 +147,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, endpoints: ["merge-pdfs"], - operationConfig: mergeOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(mergeOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/merge/MergeSettings"), ), synonyms: getSynonyms(t, "merge"), @@ -169,8 +173,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { synonyms: getSynonyms(t, "certSign"), maxFiles: -1, endpoints: ["cert-sign"], - operationConfig: certSignOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(certSignOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/certSign/CertSignAutomationSettings"), ), @@ -193,7 +197,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.SIGNING, maxFiles: -1, endpoints: ["timestamp-pdf"], - operationConfig: timestampPdfOperationConfig, + operationConfig: asRegistryConfig(timestampPdfOperationConfig), automationSettings: null, synonyms: getSynonyms(t, "timestampPdf"), }, @@ -210,8 +214,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.SIGNING, endpoints: ["sign"], - operationConfig: signOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(signOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/sign/SignSettings"), ), // TODO:: not all settings shown, suggested next tools shown synonyms: getSynonyms(t, "sign"), @@ -234,7 +238,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.GENERAL, endpoints: ["sign"], - operationConfig: signOperationConfig, + operationConfig: asRegistryConfig(signOperationConfig), automationSettings: null, synonyms: getSynonyms(t, "addText"), supportsAutomate: false, @@ -253,7 +257,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.GENERAL, endpoints: ["add-image"], - operationConfig: signOperationConfig, + operationConfig: asRegistryConfig(signOperationConfig), automationSettings: null, synonyms: getSynonyms(t, "addImage"), supportsAutomate: false, @@ -276,7 +280,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.GENERAL, workbench: "viewer", endpoints: ["view-pdf"], - operationConfig: signOperationConfig, + operationConfig: asRegistryConfig(signOperationConfig), automationSettings: null, synonyms: getSynonyms(t, "annotate"), supportsAutomate: false, @@ -298,8 +302,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["add-password"], - operationConfig: addPasswordOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(addPasswordOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/addPassword/AddPasswordSettings"), ), synonyms: getSynonyms(t, "addPassword"), @@ -322,8 +326,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.DOCUMENT_SECURITY, endpoints: ["add-watermark"], - operationConfig: addWatermarkOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(addWatermarkOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/addWatermark/AddWatermarkSingleStepSettings"), ), @@ -348,8 +352,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { synonyms: getSynonyms(t, "addStamp"), maxFiles: -1, endpoints: ["add-stamp"], - operationConfig: addStampOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(addStampOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/addStamp/AddStampAutomationSettings"), ), @@ -372,8 +376,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { "Remove potentially harmful elements from PDF files", ), endpoints: ["sanitize-pdf"], - operationConfig: sanitizeOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(sanitizeOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/sanitize/SanitizeSettings"), ), synonyms: getSynonyms(t, "sanitize"), @@ -396,8 +400,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["flatten"], - operationConfig: flattenOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(flattenOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/flatten/FlattenSettings"), ), synonyms: getSynonyms(t, "flatten"), @@ -420,7 +424,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["unlock-pdf-forms"], - operationConfig: unlockPdfFormsOperationConfig, + operationConfig: asRegistryConfig(unlockPdfFormsOperationConfig), synonyms: getSynonyms(t, "unlockPDFForms"), automationSettings: null, }, @@ -458,8 +462,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["add-password"], - operationConfig: changePermissionsOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(changePermissionsOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/changePermissions/ChangePermissionsSettings"), ), @@ -551,8 +555,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_REVIEW, maxFiles: -1, endpoints: ["update-metadata"], - operationConfig: changeMetadataOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(changeMetadataOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/changeMetadata/ChangeMetadataSingleStep"), ), @@ -570,7 +574,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.DOCUMENT_REVIEW, maxFiles: 1, endpoints: ["edit-table-of-contents"], - operationConfig: editTableOfContentsOperationConfig, + operationConfig: asRegistryConfig(editTableOfContentsOperationConfig), automationSettings: null, supportsAutomate: false, synonyms: getSynonyms(t, "editTableOfContents"), @@ -589,8 +593,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["crop"], - operationConfig: cropOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(cropOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/crop/CropAutomationSettings"), ), }, @@ -609,8 +613,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["rotate-pdf"], - operationConfig: rotateOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(rotateOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/rotate/RotateAutomationSettings"), ), synonyms: getSynonyms(t, "rotate"), @@ -629,8 +633,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, endpoints: Array.from(new Set(Object.values(SPLIT_ENDPOINT_NAMES))), - operationConfig: splitOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(splitOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/split/SplitAutomationSettings"), ), synonyms: getSynonyms(t, "split"), @@ -648,7 +652,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, endpoints: ["rearrange-pages"], - operationConfig: reorganizePagesOperationConfig, + operationConfig: asRegistryConfig(reorganizePagesOperationConfig), synonyms: getSynonyms(t, "reorganizePages"), automationSettings: null, }, @@ -666,8 +670,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["scale-pages"], - operationConfig: adjustPageScaleOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(adjustPageScaleOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/adjustPageScale/AdjustPageScaleSettings"), ), @@ -683,13 +687,13 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { ), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, - automationSettings: lazy( + automationSettings: lazySettings( () => import("@app/components/tools/addPageNumbers/AddPageNumbersAutomationSettings"), ), maxFiles: -1, endpoints: ["add-page-numbers"], - operationConfig: addPageNumbersOperationConfig, + operationConfig: asRegistryConfig(addPageNumbersOperationConfig), synonyms: getSynonyms(t, "addPageNumbers"), }, pageLayout: { @@ -710,7 +714,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["multi-page-layout"], - automationSettings: lazy( + automationSettings: lazySettings( () => import("@app/components/tools/pageLayout/PageLayoutSettings"), ), synonyms: getSynonyms(t, "pageLayout"), @@ -725,8 +729,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { ), name: t("home.bookletImposition.title", "Booklet Imposition"), component: lazy(() => import("@app/tools/BookletImposition")), - operationConfig: bookletImpositionOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(bookletImpositionOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/bookletImposition/BookletImpositionSettings"), ), @@ -757,7 +761,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["pdf-to-single-page"], - operationConfig: singleLargePageOperationConfig, + operationConfig: asRegistryConfig(singleLargePageOperationConfig), synonyms: getSynonyms(t, "pdfToSinglePage"), automationSettings: null, }, @@ -776,8 +780,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { synonyms: getSynonyms(t, "addAttachments"), maxFiles: 1, endpoints: ["add-attachments"], - operationConfig: addAttachmentsOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(addAttachmentsOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/addAttachments/AddAttachmentsSettings"), ), @@ -798,11 +802,11 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.EXTRACTION, synonyms: getSynonyms(t, "extractPages"), - automationSettings: lazy( + automationSettings: lazySettings( () => import("@app/components/tools/extractPages/ExtractPagesSettings"), ), - operationConfig: extractPagesOperationConfig, + operationConfig: asRegistryConfig(extractPagesOperationConfig), endpoints: ["rearrange-pages"], }, extractImages: { @@ -823,8 +827,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.EXTRACTION, maxFiles: -1, endpoints: ["extract-images"], - operationConfig: extractImagesOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(extractImagesOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/extractImages/ExtractImagesSettings"), ), @@ -852,8 +856,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: 1, endpoints: ["remove-pages"], synonyms: getSynonyms(t, "removePages"), - operationConfig: removePagesOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(removePagesOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/removePages/RemovePagesSettings"), ), }, @@ -876,8 +880,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { maxFiles: 1, endpoints: ["remove-blanks"], synonyms: getSynonyms(t, "removeBlanks"), - operationConfig: removeBlanksOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(removeBlanksOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/removeBlanks/RemoveBlanksSettings"), ), @@ -900,7 +904,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.REMOVAL, maxFiles: -1, endpoints: ["remove-annotations"], - operationConfig: removeAnnotationsOperationConfig, + operationConfig: asRegistryConfig(removeAnnotationsOperationConfig), automationSettings: null, synonyms: getSynonyms(t, "removeAnnotations"), }, @@ -944,8 +948,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.REMOVAL, endpoints: ["remove-password"], maxFiles: -1, - operationConfig: removePasswordOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(removePasswordOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/removePassword/RemovePasswordSettings"), ), @@ -969,7 +973,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.REMOVAL, maxFiles: -1, endpoints: ["remove-cert-sign"], - operationConfig: removeCertificateSignOperationConfig, + operationConfig: asRegistryConfig(removeCertificateSignOperationConfig), synonyms: getSynonyms(t, "removeCertSign"), automationSettings: null, }, @@ -1002,7 +1006,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { component: lazy(() => import("@app/tools/AutoRename")), maxFiles: -1, endpoints: ["auto-rename"], - operationConfig: autoRenameOperationConfig, + operationConfig: asRegistryConfig(autoRenameOperationConfig), description: t( "home.autoRename.desc", "Automatically rename PDF files based on their content", @@ -1028,8 +1032,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.ADVANCED_FORMATTING, maxFiles: -1, endpoints: ["adjust-contrast"], - operationConfig: adjustContrastOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(adjustContrastOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/adjustContrast/AdjustContrastSingleStepSettings"), ), @@ -1053,7 +1057,7 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.ADVANCED_FORMATTING, maxFiles: -1, endpoints: ["repair"], - operationConfig: repairOperationConfig, + operationConfig: asRegistryConfig(repairOperationConfig), synonyms: getSynonyms(t, "repair"), automationSettings: null, }, @@ -1078,8 +1082,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.ADVANCED_FORMATTING, maxFiles: -1, endpoints: ["extract-image-scans"], - operationConfig: scannerImageSplitOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(scannerImageSplitOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/scannerImageSplit/ScannerImageSplitSettings"), ), @@ -1102,9 +1106,9 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.ADVANCED_FORMATTING, endpoints: ["overlay-pdf"], - operationConfig: overlayPdfsOperationConfig, + operationConfig: asRegistryConfig(overlayPdfsOperationConfig), synonyms: getSynonyms(t, "overlay-pdfs"), - automationSettings: lazy( + automationSettings: lazySettings( () => import("@app/components/tools/overlayPdfs/OverlayPdfsSettings"), ), }, @@ -1126,8 +1130,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.ADVANCED_FORMATTING, maxFiles: -1, endpoints: ["replace-invert-pdf"], - operationConfig: replaceColorOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(replaceColorOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/replaceColor/ReplaceColorSettings"), ), @@ -1299,8 +1303,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, endpoints: ["compress-pdf"], - operationConfig: compressOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(compressOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/compress/CompressSettings"), ), synonyms: getSynonyms(t, "compress"), @@ -1338,8 +1342,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { "pdf-to-epub", ], - operationConfig: convertOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(convertOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/convert/ConvertSettings"), ), synonyms: getSynonyms(t, "convert"), @@ -1363,8 +1367,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, endpoints: ["ocr-pdf"], - operationConfig: ocrOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(ocrOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/ocr/OCRSettings"), ), synonyms: getSynonyms(t, "ocr"), @@ -1387,8 +1391,8 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, endpoints: ["auto-redact"], - operationConfig: redactOperationConfig, - automationSettings: lazy( + operationConfig: asRegistryConfig(redactOperationConfig), + automationSettings: lazySettings( () => import("@app/components/tools/redact/RedactSingleStepSettings"), ), synonyms: getSynonyms(t, "redact"), diff --git a/frontend/editor/src/core/hooks/tools/automate/useAutomationForm.ts b/frontend/editor/src/core/hooks/tools/automate/useAutomationForm.ts index 91f908c05a..a27cf1f7a3 100644 --- a/frontend/editor/src/core/hooks/tools/automate/useAutomationForm.ts +++ b/frontend/editor/src/core/hooks/tools/automate/useAutomationForm.ts @@ -40,7 +40,7 @@ export function useAutomationForm({ ); const getToolDefaultParameters = useCallback( - (operation: string): Record => { + (operation: string): Record => { const config = toolRegistry[operation as ToolId]?.operationConfig; if (config?.defaultParameters) { return { ...config.defaultParameters }; diff --git a/frontend/editor/src/core/hooks/tools/shared/toolOperationTypes.ts b/frontend/editor/src/core/hooks/tools/shared/toolOperationTypes.ts index 27cc16b13a..0d48ce6d09 100644 --- a/frontend/editor/src/core/hooks/tools/shared/toolOperationTypes.ts +++ b/frontend/editor/src/core/hooks/tools/shared/toolOperationTypes.ts @@ -1,3 +1,4 @@ +import { lazy, type ComponentType } from "react"; import { StirlingFile } from "@app/types/fileContext"; import type { ResponseHandler } from "@app/utils/toolResponseProcessor"; import { ToolId } from "@app/types/toolId"; @@ -147,6 +148,56 @@ export type ToolOperationConfig = | MultiFileToolOperationConfig | CustomToolOperationConfig; +/** + * One generic source-of-truth for the props every automation settings component + * accepts: the tool's parameters plus a typed change handler. + */ +export interface ToolAutomationSettingsProps { + parameters: TParams; + onParameterChange: ( + key: K, + value: TParams[K], + ) => void; + disabled?: boolean; +} + +/** + * Erased parameter shape stored in the registry. Spreadable and callable, so + * consumers can merge defaults and invoke buildFormData/customProcessor/endpoint + * without per-tool type knowledge. + */ +export type ErasedToolParams = Record; + +export type RegistryToolOperationConfig = ToolOperationConfig; +export type RegistryAutomationSettings = ComponentType< + ToolAutomationSettingsProps +> | null; + +/** + * Store a tool's typed operationConfig in the registry. The input is validated as + * a real ToolOperationConfig, then TParams is erased here. TParams is + * invariant in ToolOperationConfig, so the erasure cannot be a plain assignment; + * the `as unknown as` is the localized existential boundary. + */ +export function asRegistryConfig( + config: ToolOperationConfig, +): RegistryToolOperationConfig { + return config as unknown as RegistryToolOperationConfig; +} + +/** + * Lazily load a tool's automation settings component for the registry. The loaded + * component is validated against ToolAutomationSettingsProps (inferred + * from the module), then erased to the registry's shared props shape. + */ +export function lazySettings( + loader: () => Promise<{ + default: ComponentType>; + }>, +): RegistryAutomationSettings { + return lazy(loader) as unknown as RegistryAutomationSettings; +} + /** * Complete tool operation interface returned by useToolOperation. */ diff --git a/frontend/editor/src/core/tools/Automate.tsx b/frontend/editor/src/core/tools/Automate.tsx index 6952bb31c1..5a197c357a 100644 --- a/frontend/editor/src/core/tools/Automate.tsx +++ b/frontend/editor/src/core/tools/Automate.tsx @@ -4,7 +4,10 @@ import { useViewScopedFiles } from "@app/hooks/tools/shared/useViewScopedFiles"; import { useNavigationActions } from "@app/contexts/NavigationContext"; import { useToolWorkflow } from "@app/contexts/ToolWorkflowContext"; -import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; +import { + createToolFlow, + type MiddleStepConfig, +} from "@app/components/tools/shared/createToolFlow"; import { createFilesToolStep } from "@app/components/tools/shared/FilesToolStep"; import AutomationSelection from "@app/components/tools/automate/AutomationSelection"; import AutomationCreation from "@app/components/tools/automate/AutomationCreation"; @@ -200,7 +203,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const createStep = ( title: string, - props: any, + props: Omit, content?: React.ReactNode, ) => ({ title, diff --git a/frontend/editor/src/core/utils/automationExecutor.ts b/frontend/editor/src/core/utils/automationExecutor.ts index 139fca981a..a29ffbcb83 100644 --- a/frontend/editor/src/core/utils/automationExecutor.ts +++ b/frontend/editor/src/core/utils/automationExecutor.ts @@ -4,6 +4,11 @@ import { ToolId } from "@app/types/toolId"; import { AUTOMATION_CONSTANTS } from "@app/constants/automation"; import { AutomationFileProcessor } from "@app/utils/automationFileProcessor"; import { ToolType } from "@app/hooks/tools/shared/useToolOperation"; +import { + type ErasedToolParams, + type MultiFileToolOperationConfig, + type SingleFileToolOperationConfig, +} from "@app/hooks/tools/shared/toolOperationTypes"; import { zipFileService } from "@app/services/zipFileService"; import { processResponse } from "@app/utils/toolResponseProcessor"; @@ -76,8 +81,8 @@ const executeApiRequest = async ( * Execute single-file tool operation (processes files one at a time) */ const executeSingleFileOperation = async ( - config: any, - parameters: any, + config: SingleFileToolOperationConfig, + parameters: ErasedToolParams, files: File[], filePrefix: string, ): Promise => { @@ -89,9 +94,7 @@ const executeSingleFileOperation = async ( ? config.endpoint(parameters) : config.endpoint; - const formData = ( - config.buildFormData as (params: any, file: File) => FormData - )(parameters, file); + const formData = config.buildFormData(parameters, file); const processedFiles = await executeApiRequest( endpoint, @@ -110,8 +113,8 @@ const executeSingleFileOperation = async ( * Execute multi-file tool operation (processes all files in one request) */ const executeMultiFileOperation = async ( - config: any, - parameters: any, + config: MultiFileToolOperationConfig, + parameters: ErasedToolParams, files: File[], filePrefix: string, ): Promise => { @@ -120,9 +123,7 @@ const executeMultiFileOperation = async ( ? config.endpoint(parameters) : config.endpoint; - const formData = ( - config.buildFormData as (params: any, files: File[]) => FormData - )(parameters, files); + const formData = config.buildFormData(parameters, files); return await executeApiRequest( endpoint, @@ -138,7 +139,7 @@ const executeMultiFileOperation = async ( */ export const executeToolOperation = async ( operationName: string, - parameters: any, + parameters: ErasedToolParams, files: File[], toolRegistry: ToolRegistry, ): Promise => { @@ -156,7 +157,7 @@ export const executeToolOperation = async ( */ export const executeToolOperationWithPrefix = async ( operationName: string, - parameters: any, + parameters: ErasedToolParams, files: File[], toolRegistry: ToolRegistry, filePrefix: string = AUTOMATION_CONSTANTS.FILE_PREFIX, diff --git a/frontend/editor/src/prototypes/data/usePrototypeToolRegistry.tsx b/frontend/editor/src/prototypes/data/usePrototypeToolRegistry.tsx index 3ebcfeade5..7267bed751 100644 --- a/frontend/editor/src/prototypes/data/usePrototypeToolRegistry.tsx +++ b/frontend/editor/src/prototypes/data/usePrototypeToolRegistry.tsx @@ -8,6 +8,7 @@ import { type PrototypeToolRegistry, } from "@app/data/toolsTaxonomy"; import { pdfCommentAgentOperationConfig } from "@app/hooks/tools/pdfCommentAgent/pdfCommentAgentOperationConfig"; +import { asRegistryConfig } from "@app/hooks/tools/shared/toolOperationTypes"; import PdfCommentAgent from "@app/tools/PdfCommentAgent"; import { getSynonyms } from "@app/utils/toolSynonyms"; @@ -42,7 +43,7 @@ export function usePrototypeToolRegistry(): PrototypeToolRegistry { subcategoryId: SubcategoryId.DOCUMENT_REVIEW, maxFiles: 1, endpoints: ["pdf-comment-agent"], - operationConfig: pdfCommentAgentOperationConfig, + operationConfig: asRegistryConfig(pdfCommentAgentOperationConfig), automationSettings: null, synonyms: getSynonyms(t, "pdfCommentAgent"), versionStatus: "beta", diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 444ca380b3..d74cfbbc1d 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -214,10 +214,8 @@ export default defineConfig( ignores: [ "editor/src/core/components/**/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/contexts/**/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/data/**/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/hooks/**/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/services/**/*.{js,mjs,jsx,ts,tsx}", - "editor/src/core/tools/Automate.tsx", "editor/src/core/tools/annotate/useAnnotationSelection.ts", "editor/src/core/types/**/*.{js,mjs,jsx,ts,tsx}", "editor/src/core/utils/**/*.{js,mjs,jsx,ts,tsx}", From 60fff188a641eb31af0505b4f16d283390693c6f Mon Sep 17 00:00:00 2001 From: Ludy Date: Wed, 24 Jun 2026 10:15:08 +0200 Subject: [PATCH 3/6] fix(team): hide users already assigned to the selected team (#6760) # Description of Changes # Description of Changes - What was changed - Filtered the "Add Member to Team" user picker so users who are already members of the selected target team are no longer shown. - Applied the same filtering in both team management entry points: - `TeamsSection` - `TeamDetailsSection` - Kept users in other teams visible, so they can still be moved into the selected team. - Why the change was made - The modal was showing users who were already part of the target team, which made the action misleading and allowed redundant selection. - Hiding already-assigned users keeps the UI aligned with the actual action: adding new members to the team before: image after: image --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .../config/configSections/TeamDetailsSection.tsx | 10 +++++++--- .../shared/config/configSections/TeamsSection.tsx | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx index 5363525cfb..90f4fda9e0 100644 --- a/frontend/editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx +++ b/frontend/editor/src/proprietary/components/shared/config/configSections/TeamDetailsSection.tsx @@ -55,6 +55,9 @@ export default function TeamDetailsSection({ const [selectedUserId, setSelectedUserId] = useState(""); const [selectedTeamId, setSelectedTeamId] = useState(""); const [processing, setProcessing] = useState(false); + const availableUsersForTeam = team + ? availableUsers.filter((user) => user.team?.id !== team.id) + : []; // License information const [licenseInfo, setLicenseInfo] = useState<{ @@ -725,7 +728,7 @@ export default function TeamDetailsSection({ placeholder={t( "workspace.teams.addMemberToTeam.selectUserPlaceholder", )} - data={availableUsers.map((user) => ({ + data={availableUsersForTeam.map((user) => ({ value: user.id.toString(), label: `${user.username}${user.team ? ` (${t("workspace.teams.addMemberToTeam.currentlyIn")} ${user.team.name})` : ""}`, }))} @@ -739,8 +742,9 @@ export default function TeamDetailsSection({ /> {selectedUserId && - availableUsers.find((u) => u.id.toString() === selectedUserId) - ?.team && ( + availableUsersForTeam.find( + (u) => u.id.toString() === selectedUserId, + )?.team && ( {t("workspace.teams.addMemberToTeam.willBeMoved")} diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/TeamsSection.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/TeamsSection.tsx index 9904e32755..e3cd79be02 100644 --- a/frontend/editor/src/proprietary/components/shared/config/configSections/TeamsSection.tsx +++ b/frontend/editor/src/proprietary/components/shared/config/configSections/TeamsSection.tsx @@ -46,6 +46,9 @@ export default function TeamsSection() { const [newTeamName, setNewTeamName] = useState(""); const [renameTeamName, setRenameTeamName] = useState(""); const [selectedUserId, setSelectedUserId] = useState(""); + const availableUsersForSelectedTeam = selectedTeam + ? availableUsers.filter((user) => user.team?.id !== selectedTeam.id) + : []; useEffect(() => { fetchTeams(); @@ -604,7 +607,7 @@ export default function TeamsSection() { placeholder={t( "workspace.teams.addMemberToTeam.selectUserPlaceholder", )} - data={availableUsers.map((user) => ({ + data={availableUsersForSelectedTeam.map((user) => ({ value: user.id.toString(), label: `${user.username}${user.team ? ` (${t("workspace.teams.addMemberToTeam.currentlyIn")} ${user.team.name})` : ""}`, }))} @@ -618,8 +621,9 @@ export default function TeamsSection() { /> {selectedUserId && - availableUsers.find((u) => u.id.toString() === selectedUserId) - ?.team && ( + availableUsersForSelectedTeam.find( + (u) => u.id.toString() === selectedUserId, + )?.team && ( {t("workspace.teams.addMemberToTeam.willBeMoved")} From 8a0b12b5abbe366118af5cdc9e030f81bbeeb643 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 24 Jun 2026 09:21:28 +0100 Subject: [PATCH 4/6] Remove ffmpeg from published Docker images (#6791) ## Summary Published Docker images (`stirling-pdf:latest`, `:2.13.1`) still shipped the full `ffmpeg` package even though it was disabled in source back in #6053. **Root cause:** `push-docker.yml` passed a hardcoded `BASE_VERSION=1.0.0` build-arg for the regular image, overriding the Dockerfile's `ARG BASE_VERSION=1.0.2` default. Base `1.0.0` is the original base that still does the explicit `ffmpeg` apt install, so the published image never picked up the removal. --- .github/aur/stirling-pdf-desktop/PKGBUILD | 2 +- .github/aur/stirling-pdf-server-bin/PKGBUILD | 2 +- .github/workflows/push-docker.yml | 2 +- build.gradle | 2 +- frontend/editor/src-tauri/tauri.conf.json | 2 +- frontend/editor/src/core/testing/serverExperienceSimulations.ts | 2 +- .../src/proprietary/testing/serverExperienceSimulations.ts | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/aur/stirling-pdf-desktop/PKGBUILD b/.github/aur/stirling-pdf-desktop/PKGBUILD index d35c874238..5a8563e942 100644 --- a/.github/aur/stirling-pdf-desktop/PKGBUILD +++ b/.github/aur/stirling-pdf-desktop/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Stirling PDF Inc pkgname=stirling-pdf-desktop -pkgver=2.13.1 +pkgver=2.13.2 pkgrel=1 pkgdesc="Locally hosted, web-based PDF manipulation tool (Tauri desktop app, official Stirling PDF Inc build)" arch=('x86_64') diff --git a/.github/aur/stirling-pdf-server-bin/PKGBUILD b/.github/aur/stirling-pdf-server-bin/PKGBUILD index 2e71087c7c..6ed6757b92 100644 --- a/.github/aur/stirling-pdf-server-bin/PKGBUILD +++ b/.github/aur/stirling-pdf-server-bin/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Stirling PDF Inc pkgname=stirling-pdf-server-bin -pkgver=2.13.1 +pkgver=2.13.2 pkgrel=1 pkgdesc="Locally hosted, web-based PDF manipulation tool (server JAR, prebuilt)" arch=('any') diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index ad7653eab3..1f38f0ef57 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -155,9 +155,9 @@ jobs: cache-to: type=gha,mode=max,scope=stirling-pdf-latest tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + # No BASE_VERSION pin: inherit the Dockerfile ARG default (single source of truth). build-args: | VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }} - BASE_VERSION=1.0.0 platforms: linux/amd64,linux/arm64/v8 provenance: true sbom: true diff --git a/build.gradle b/build.gradle index b8423fc920..6b324598f3 100644 --- a/build.gradle +++ b/build.gradle @@ -78,7 +78,7 @@ springBoot { allprojects { group = 'stirling.software' - version = '2.13.1' + version = '2.13.2' configurations.configureEach { exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" diff --git a/frontend/editor/src-tauri/tauri.conf.json b/frontend/editor/src-tauri/tauri.conf.json index 55dba6de7d..3eb545c231 100644 --- a/frontend/editor/src-tauri/tauri.conf.json +++ b/frontend/editor/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Stirling-PDF", - "version": "2.13.1", + "version": "2.13.2", "identifier": "stirling.pdf.dev", "build": { "frontendDist": "../dist", diff --git a/frontend/editor/src/core/testing/serverExperienceSimulations.ts b/frontend/editor/src/core/testing/serverExperienceSimulations.ts index 8338efb9e9..2e9db84852 100644 --- a/frontend/editor/src/core/testing/serverExperienceSimulations.ts +++ b/frontend/editor/src/core/testing/serverExperienceSimulations.ts @@ -38,7 +38,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: "2.13.1", + appVersion: "2.13.2", serverCertificateEnabled: false, enableAlphaFunctionality: false, serverPort: 8080, diff --git a/frontend/editor/src/proprietary/testing/serverExperienceSimulations.ts b/frontend/editor/src/proprietary/testing/serverExperienceSimulations.ts index 1e19f4be0c..65d611fc09 100644 --- a/frontend/editor/src/proprietary/testing/serverExperienceSimulations.ts +++ b/frontend/editor/src/proprietary/testing/serverExperienceSimulations.ts @@ -48,7 +48,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: "2.13.1", + appVersion: "2.13.2", serverCertificateEnabled: false, enableAlphaFunctionality: false, enableDesktopInstallSlide: true, From e35594f946835dcb7ac5dd32693cda18cb2244f8 Mon Sep 17 00:00:00 2001 From: Ludy Date: Wed, 24 Jun 2026 10:34:59 +0200 Subject: [PATCH 5/6] chore(build): centralize Gradle dependency version management (#6499) # Description of Changes This change centralizes several dependency version declarations into shared Gradle version properties and updates module build files to reference those properties instead of hardcoded version strings. ### What was changed - Added centralized version properties in the root `build.gradle` for: - commons-io - commons-lang3 - rhino - okhttp BOM - gson - guava - bucket4j - archunit - batik - jpdfium - JWT - AWS SDK - Testcontainers - Replaced hardcoded dependency versions across multiple modules with shared version variables. - Updated `resolutionStrategy.force` declarations to use centralized version properties. - Updated dependency constraints and BOM references to use shared version variables. - Removed module-specific duplicate version declarations from `app/proprietary/build.gradle`. - Standardized dependency declarations across `common`, `core`, `proprietary`, and `saas` modules. ## Why the change was made - Reduce duplication of dependency version definitions. - Simplify future dependency upgrades and maintenance. - Ensure consistent dependency versions across all modules. - Improve readability and reduce the risk of version drift between subprojects. - Make security-related dependency overrides easier to maintain from a single location. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- app/common/build.gradle | 12 ++++++------ app/core/build.gradle | 13 ++++++------- app/proprietary/build.gradle | 32 +++++++++++++------------------- app/saas/build.gradle | 2 +- build.gradle | 30 +++++++++++++++++++++--------- 5 files changed, 47 insertions(+), 42 deletions(-) diff --git a/app/common/build.gradle b/app/common/build.gradle index 170f964af4..516edd4897 100644 --- a/app/common/build.gradle +++ b/app/common/build.gradle @@ -29,13 +29,13 @@ spotless { } } dependencies { - api 'com.google.guava:guava:33.6.0-jre' + api "com.google.guava:guava:${guavaVersion}" api 'org.springframework.boot:spring-boot-starter-webmvc' api 'org.springframework.boot:spring-boot-starter-aspectj' api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260313.1' api 'com.fathzer:javaluator:3.0.6' api 'com.posthog.java:posthog:1.2.0' - api 'org.apache.commons:commons-lang3:3.20.0' + api "org.apache.commons:commons-lang3:${commonsLang3}" api 'com.drewnoakes:metadata-extractor:2.20.0' // Image metadata extractor api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8' api "org.apache.pdfbox:pdfbox:$pdfboxVersion" @@ -60,7 +60,7 @@ dependencies { exclude group: 'com.google.code.gson', module: 'gson' } - api 'com.stirling:jpdfium:1.0.2' + api "com.stirling:jpdfium:${jpdfiumVersion}" // -PjpdfiumPlatforms=all| def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim() @@ -75,12 +75,12 @@ dependencies { } logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms.join(', ')}") jpdfiumPlatforms.each { platform -> - runtimeOnly "com.stirling:jpdfium-natives-${platform}:1.0.2" + runtimeOnly "com.stirling:jpdfium-natives-${platform}:${jpdfiumVersion}" } // Bucket4j (local in-process token bucket for RateLimitStore default impl) - implementation 'com.bucket4j:bucket4j_jdk17-core:8.19.0' + implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}" // ArchUnit: enforces module dependency direction (see ArchitectureTest) - testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.2' + testImplementation "com.tngtech.archunit:archunit-junit5:${archunitVersion}" } diff --git a/app/core/build.gradle b/app/core/build.gradle index 21acdb36e0..d77fbdd438 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -67,7 +67,7 @@ dependencies { exclude group: 'com.fasterxml.jackson.jaxrs' exclude group: 'com.fasterxml.jackson.module', module: 'jackson-module-jaxb-annotations' } - implementation 'commons-io:commons-io:2.22.0' + implementation "commons-io:commons-io:$commonsIoVersion" implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" implementation 'io.micrometer:micrometer-core' @@ -81,25 +81,24 @@ dependencies { implementation 'org.verapdf:validation-model:1.28.2' // CVE-2025-66453: Explicit rhino 1.7.15 to override verapdf's 1.7.13 - implementation 'org.mozilla:rhino:1.9.1' + implementation "org.mozilla:rhino:${rhinoVersion}" // veraPDF still uses javax.xml.bind, not the new jakarta namespace implementation 'javax.xml.bind:jaxb-api:2.3.1' implementation 'com.sun.xml.bind:jaxb-impl:2.3.9' implementation 'com.sun.xml.bind:jaxb-core:4.0.7' - implementation 'org.apache.poi:poi-ooxml:5.5.1' - // CVE-2022-25647: Explicit gson 2.13.2 to prevent unsafe deserialization (tabula would pull 2.8.7) - implementation 'com.google.code.gson:gson:2.13.2' + // CVE-2022-25647: Explicit gson to prevent unsafe deserialization (tabula would pull 2.8.7) + implementation "com.google.code.gson:gson:${gsonVersion}" implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4' implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv implementation 'org.apache.poi:poi-ooxml:5.5.1' // Batik only bridge module needed (transitively pulls anim, gvt, util, css, dom, svg-dom) // Replaces batik-all which included unused codec, svggen, transcoder, script modules - implementation 'org.apache.xmlgraphics:batik-bridge:1.19' + implementation "org.apache.xmlgraphics:batik-bridge:${batikVersion}" // Required by TwelveMonkeys imageio-batik SPI (SVGImageReaderSpi) during ImageIO init - runtimeOnly 'org.apache.xmlgraphics:batik-transcoder:1.19' + runtimeOnly "org.apache.xmlgraphics:batik-transcoder:${batikVersion}" // PDFBox Graphics2D bridge for Batik SVG to PDF conversion implementation 'de.rototor.pdfbox:graphics2d:3.0.5' diff --git a/app/proprietary/build.gradle b/app/proprietary/build.gradle index 821def1fec..7fb12de5ce 100644 --- a/app/proprietary/build.gradle +++ b/app/proprietary/build.gradle @@ -3,12 +3,6 @@ repositories { maven { url = "https://build.shibboleth.net/maven/releases" } } -ext { - jwtVersion = '0.13.0' - awsSdkVersion = '2.44.12' - testcontainersMinioVersion = '1.21.4' -} - bootRun { enabled = false } @@ -41,7 +35,7 @@ spotless { } dependencies { implementation project(':common') - api 'com.google.guava:guava:33.6.0-jre' + api "com.google.guava:guava:${guavaVersion}" api 'org.springframework:spring-jdbc' api 'org.springframework:spring-webmvc' @@ -61,37 +55,37 @@ dependencies { api 'com.github.ben-manes.caffeine:caffeine' implementation 'org.springframework.boot:spring-boot-starter-data-redis' api 'io.swagger.core.v3:swagger-core-jakarta:2.2.46' - implementation 'com.bucket4j:bucket4j_jdk17-core:8.19.0' + implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}" // Lettuce-backed Bucket4j ProxyManager used by ValkeyRateLimitStore for cluster-wide // token-bucket rate limiting (parity with in-process Bucket4j semantics; no fixed-window // boundary doubling). - implementation 'com.bucket4j:bucket4j_jdk17-lettuce:8.19.0' + implementation "com.bucket4j:bucket4j_jdk17-lettuce:${bucket4jVersion}" // https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17 implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" - implementation 'com.google.code.gson:gson:2.13.2' + implementation "com.google.code.gson:gson:${gsonVersion}" api 'io.micrometer:micrometer-registry-prometheus' - api "io.jsonwebtoken:jjwt-api:$jwtVersion" - runtimeOnly "io.jsonwebtoken:jjwt-impl:$jwtVersion" - runtimeOnly "io.jsonwebtoken:jjwt-jackson:$jwtVersion" + api "io.jsonwebtoken:jjwt-api:${jwtVersion}" + runtimeOnly "io.jsonwebtoken:jjwt-impl:${jwtVersion}" + runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jwtVersion}" runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database - file format incompatible with 2.4.x, would break existing user databases runtimeOnly 'org.postgresql:postgresql:42.7.11' implementation('com.coveo:saml-client:5.0.0') { exclude group: 'org.opensaml', module: 'opensaml-core' } - implementation "software.amazon.awssdk:s3:$awsSdkVersion" - implementation "software.amazon.awssdk:url-connection-client:$awsSdkVersion" + implementation "software.amazon.awssdk:s3:${awsSdkVersion}" + implementation "software.amazon.awssdk:url-connection-client:${awsSdkVersion}" // Testcontainers: real MinIO/LocalStack (S3) and Valkey for integration tests in CI without // manually-started instances. Tests skip cleanly when Docker is unavailable. - testImplementation "org.testcontainers:testcontainers:$testcontainersMinioVersion" - testImplementation "org.testcontainers:minio:$testcontainersMinioVersion" - testImplementation "org.testcontainers:localstack:$testcontainersMinioVersion" - testImplementation "org.testcontainers:junit-jupiter:$testcontainersMinioVersion" + testImplementation "org.testcontainers:testcontainers:${testcontainersMinioVersion}" + testImplementation "org.testcontainers:minio:${testcontainersMinioVersion}" + testImplementation "org.testcontainers:localstack:${testcontainersMinioVersion}" + testImplementation "org.testcontainers:junit-jupiter:${testcontainersMinioVersion}" } tasks.register('prepareKotlinBuildScriptModel') {} diff --git a/app/saas/build.gradle b/app/saas/build.gradle index 560ad58f5f..d6ae305def 100644 --- a/app/saas/build.gradle +++ b/app/saas/build.gradle @@ -41,5 +41,5 @@ dependencies { api 'org.flywaydb:flyway-core' runtimeOnly 'org.flywaydb:flyway-database-postgresql' - testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.2' + testImplementation "com.tngtech.archunit:archunit-junit5:${archunitVersion}" } diff --git a/build.gradle b/build.gradle index 6b324598f3..3b91ca502c 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,19 @@ ext { commonmarkVersion = "0.28.0" googleJavaFormatVersion = "1.28.0" logback = "1.5.32" + commonsIoVersion = "2.22.0" + commonsLang3 = "3.20.0" + rhinoVersion = "1.9.1" + okhttpBomVersion = "5.3.2" + gsonVersion = "2.14.0" + guavaVersion = "33.6.0-jre" + bucket4jVersion = "8.19.0" + archunitVersion = "1.4.2" + batikVersion = "1.19" + jpdfiumVersion = "1.0.2" + jwtVersion = "0.13.0" + awsSdkVersion = "2.44.12" + testcontainersMinioVersion = "1.21.4" // junit-platform-launcher version managed by Spring Boot BOM modernJavaVersion = 25 } @@ -203,12 +216,12 @@ subprojects { // - CVE-2022-25647: gson 2.8.9+ (explicit dependency overrides tabula 2.8.7) // - CVE-2025-66453: rhino 1.7.15 (explicit dependency overrides verapdf 1.7.13) // Fallback strategy force declarations for additional safety: - resolutionStrategy.force 'com.google.code.gson:gson:2.13.2' - resolutionStrategy.force 'org.mozilla:rhino:1.9.1' + resolutionStrategy.force "com.google.code.gson:gson:${gsonVersion}" + resolutionStrategy.force "org.mozilla:rhino:${rhinoVersion}" // CVE-2025-48924: commons-lang3 3.20.0 DoS prevention - resolutionStrategy.force 'org.apache.commons:commons-lang3:3.20.0' - // CVE-2024-47554: commons-io 2.21.0 DoS prevention - resolutionStrategy.force 'commons-io:commons-io:2.21.0' + resolutionStrategy.force "org.apache.commons:commons-lang3:${commonsLang3}" + // CVE-2024-47554: commons-io DoS prevention + resolutionStrategy.force "commons-io:commons-io:${commonsIoVersion}" // Keep BouncyCastle modules aligned to avoid runtime linkage errors resolutionStrategy.force "org.bouncycastle:bcprov-jdk18on:${bouncycastleVersion}" resolutionStrategy.force "org.bouncycastle:bcpkix-jdk18on:${bouncycastleVersion}" @@ -221,7 +234,7 @@ subprojects { } dependencies { // Override BOM-managed commons-lang3 for CVE-2025-48924 fix - dependency 'org.apache.commons:commons-lang3:3.20.0' + dependency "org.apache.commons:commons-lang3:$commonsLang3" } } @@ -243,7 +256,7 @@ subprojects { testRuntimeOnly 'org.mockito:mockito-inline:5.2.0' testRuntimeOnly "org.junit.platform:junit-platform-launcher" - testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.3.2") + testImplementation platform("com.squareup.okhttp3:okhttp-bom:${okhttpBomVersion}") testImplementation "com.squareup.okhttp3:mockwebserver" } @@ -599,7 +612,7 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly "org.junit.platform:junit-platform-launcher" - testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.3.2") + testImplementation platform("com.squareup.okhttp3:okhttp-bom:${okhttpBomVersion}") testImplementation "com.squareup.okhttp3:mockwebserver" } @@ -670,7 +683,6 @@ tasks.register('buildRestartHelper', Jar) { } } - tasks.withType(Test).configureEach { // maxParallelForks: parallelise JUnit across cores // Half of available CPUs is a safe default; bump if your tests are I/O-bound. From 26021425e3c429ef4eaa942bf165b71190c5eccb Mon Sep 17 00:00:00 2001 From: Ludy Date: Wed, 24 Jun 2026 10:36:05 +0200 Subject: [PATCH 6/6] chore(ci): upgrade Gradle to 9.6.0 across workflows, Docker builds, and wrapper (#6790) # Description of Changes ## What was changed - Updated all GitHub Actions workflows using Gradle from older versions (9.3.1 and 9.5.1) to Gradle 9.6.0. - Updated the Gradle Wrapper distribution URL to use Gradle 9.6.0. - Updated all Gradle-based Docker build stages to use the `gradle:9.6.0-jdk25` image and corresponding image digest. - Aligned CI, Docker, and local development environments on the same Gradle version. - Included the regenerated `gradlew` script changes produced by the Gradle wrapper update process. ## Why the change was made - Ensures consistent Gradle versions across local development, CI workflows, and Docker builds. - Takes advantage of the latest Gradle 9.6.0 improvements, fixes, and compatibility updates. - Reduces the risk of version mismatches causing build or deployment inconsistencies. - Simplifies maintenance by standardizing the build toolchain throughout the repository. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have run `task check` to verify linters, typechecks, and tests pass - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#7-testing) for more details. --- .github/workflows/PR-Demo-Comment-with-react.yml | 2 +- .github/workflows/ai-engine.yml | 2 +- .github/workflows/backend-build.yml | 2 +- .github/workflows/check-licence.yml | 2 +- .github/workflows/check-openapi.yml | 2 +- .github/workflows/coverage-aggregate.yml | 2 +- .github/workflows/db-migration-test.yml | 2 +- .github/workflows/docker-compose-tests.yml | 2 +- .github/workflows/frontend-backend-licenses-update.yml | 2 +- .github/workflows/multiOSReleases.yml | 6 +++--- .github/workflows/push-docker.yml | 2 +- .github/workflows/swagger.yml | 2 +- .github/workflows/tauri-build.yml | 4 ++-- .github/workflows/test-build-docker.yml | 2 +- .github/workflows/testdriver.yml | 2 +- docker/backend/Dockerfile | 2 +- docker/embedded/Dockerfile | 2 +- docker/embedded/Dockerfile.fat | 2 +- docker/embedded/Dockerfile.ultra-lite | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 4 ++-- 21 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index 95afebfe4c..e478f95c89 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -222,7 +222,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/ai-engine.yml b/.github/workflows/ai-engine.yml index 33212b2330..03b172c4c3 100644 --- a/.github/workflows/ai-engine.yml +++ b/.github/workflows/ai-engine.yml @@ -43,7 +43,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml index 0a252100a0..37a42c419f 100644 --- a/.github/workflows/backend-build.yml +++ b/.github/workflows/backend-build.yml @@ -58,7 +58,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 cache-disabled: true - name: Install Task diff --git a/.github/workflows/check-licence.yml b/.github/workflows/check-licence.yml index 1159c75dc6..b984f7d47d 100644 --- a/.github/workflows/check-licence.yml +++ b/.github/workflows/check-licence.yml @@ -40,7 +40,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 cache-disabled: true - name: Install Task diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml index 49c19433c3..46ce829a87 100644 --- a/.github/workflows/check-openapi.yml +++ b/.github/workflows/check-openapi.yml @@ -45,7 +45,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 cache-disabled: true - name: Install Task diff --git a/.github/workflows/coverage-aggregate.yml b/.github/workflows/coverage-aggregate.yml index 35c252c4bd..bbfaf09363 100644 --- a/.github/workflows/coverage-aggregate.yml +++ b/.github/workflows/coverage-aggregate.yml @@ -51,7 +51,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.3.1 + gradle-version: 9.6.0 cache-disabled: true - name: Set up Python diff --git a/.github/workflows/db-migration-test.yml b/.github/workflows/db-migration-test.yml index 114855c4bc..4b5ca8b674 100644 --- a/.github/workflows/db-migration-test.yml +++ b/.github/workflows/db-migration-test.yml @@ -48,7 +48,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 cache-disabled: true # No `-PnoSpotless` here yet because the upstream cache layer matches the diff --git a/.github/workflows/docker-compose-tests.yml b/.github/workflows/docker-compose-tests.yml index add65d6a8e..faa3c2a280 100644 --- a/.github/workflows/docker-compose-tests.yml +++ b/.github/workflows/docker-compose-tests.yml @@ -61,7 +61,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 cache-disabled: true - name: Set up Docker Buildx diff --git a/.github/workflows/frontend-backend-licenses-update.yml b/.github/workflows/frontend-backend-licenses-update.yml index 8bf89bf210..41d3f35c6e 100644 --- a/.github/workflows/frontend-backend-licenses-update.yml +++ b/.github/workflows/frontend-backend-licenses-update.yml @@ -349,7 +349,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index f29714d94e..f5fb743a3f 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -73,7 +73,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 @@ -148,7 +148,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 - name: Setup Node.js if: matrix.variant.build_frontend == true @@ -252,7 +252,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 - name: Install Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index 1f38f0ef57..75b1aa60c8 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -78,7 +78,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 - name: Set up Docker Buildx id: buildx diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml index 00c368c75f..9696aa419c 100644 --- a/.github/workflows/swagger.yml +++ b/.github/workflows/swagger.yml @@ -48,7 +48,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 - name: Generate Swagger documentation run: ./gradlew :stirling-pdf:generateOpenApiDocs diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index 65b1e40f36..944940e973 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -122,7 +122,7 @@ jobs: uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: workspaces: frontend/editor/src-tauri - # Stable key shared across workflows so the nightly warmer. + # Stable key shared across workflows so the nightly warmer. # rust-cache still appends OS + rustc + Cargo.lock. shared-key: tauri-${{ matrix.name }} save-if: ${{ github.ref == 'refs/heads/main' }} @@ -150,7 +150,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 - name: Setup Task uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0 diff --git a/.github/workflows/test-build-docker.yml b/.github/workflows/test-build-docker.yml index 0b692a008a..4fd00a494c 100644 --- a/.github/workflows/test-build-docker.yml +++ b/.github/workflows/test-build-docker.yml @@ -106,7 +106,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 cache-disabled: true - name: Install Task diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml index 05fce0199c..e9774694e7 100644 --- a/.github/workflows/testdriver.yml +++ b/.github/workflows/testdriver.yml @@ -51,7 +51,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@50e97c2cd7a37755bbfafc9c5b7cafaece252f6e # v6.1.0 with: - gradle-version: 9.5.1 + gradle-version: 9.6.0 - name: Build with Gradle run: ./gradlew build diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 40abd00933..b2b7aa644b 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -4,7 +4,7 @@ ARG BASE_VERSION=1.0.2 ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application (backend only, no frontend) -FROM gradle:9.3.1-jdk25@sha256:85aec999629f4774a383cb792da4b598bdf5a7e69c4b9570bb70c0f919179183 AS app-build +FROM gradle:9.6.0-jdk25@sha256:e3905233ae349e72016daf8a0e19f085a1dd89ded8ec88b3d8335d3fd0b350f4 AS app-build # JDK 25+: --add-exports is no longer accepted via JAVA_TOOL_OPTIONS; use JDK_JAVA_OPTIONS instead ENV JDK_JAVA_OPTIONS="--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ diff --git a/docker/embedded/Dockerfile b/docker/embedded/Dockerfile index 89ed06397a..7ea8201f59 100644 --- a/docker/embedded/Dockerfile +++ b/docker/embedded/Dockerfile @@ -5,7 +5,7 @@ ARG BASE_VERSION=1.0.2 ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application and frontend -FROM gradle:9.5.1-jdk25@sha256:8de3543f1772bb66be3b275893e5977b6d8bd2b0d25551faa5846a821d1f0600 AS app-build +FROM gradle:9.6.0-jdk25@sha256:e3905233ae349e72016daf8a0e19f085a1dd89ded8ec88b3d8335d3fd0b350f4 AS app-build ARG TASK_VERSION=3.49.1 RUN apt-get update \ diff --git a/docker/embedded/Dockerfile.fat b/docker/embedded/Dockerfile.fat index 0beb8406ec..71f2a12414 100644 --- a/docker/embedded/Dockerfile.fat +++ b/docker/embedded/Dockerfile.fat @@ -6,7 +6,7 @@ ARG BASE_VERSION=1.0.2 ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application and frontend -FROM gradle:9.5.1-jdk25@sha256:8de3543f1772bb66be3b275893e5977b6d8bd2b0d25551faa5846a821d1f0600 AS app-build +FROM gradle:9.6.0-jdk25@sha256:e3905233ae349e72016daf8a0e19f085a1dd89ded8ec88b3d8335d3fd0b350f4 AS app-build ARG TASK_VERSION=3.49.1 RUN apt-get update \ diff --git a/docker/embedded/Dockerfile.ultra-lite b/docker/embedded/Dockerfile.ultra-lite index 9f77716646..01048e864d 100644 --- a/docker/embedded/Dockerfile.ultra-lite +++ b/docker/embedded/Dockerfile.ultra-lite @@ -2,7 +2,7 @@ # Single JAR contains both frontend and backend with minimal dependencies # Stage 1: Build application with embedded frontend -FROM gradle:9.5.1-jdk25@sha256:8de3543f1772bb66be3b275893e5977b6d8bd2b0d25551faa5846a821d1f0600 AS build +FROM gradle:9.6.0-jdk25@sha256:e3905233ae349e72016daf8a0e19f085a1dd89ded8ec88b3d8335d3fd0b350f4 AS build # Install Node.js and npm for frontend build ARG TASK_VERSION=3.49.1 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df6a6ad763..eb84db68da 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/gradlew b/gradlew index b9bb139f79..249efbb032 100755 --- a/gradlew +++ b/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: