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 && (
-
-
-
-
-
-
- {isLoading && }
-
-
- {t("agents.stirling_name", "Stirling")}
-
-
-
-
-
- }
- 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 (
-
+
);
@@ -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 (
+
+ {
+ if (guestBlocked) {
+ promptGuestSignup();
+ return;
+ }
+ selectPolicy(cat.id);
+ onExpand();
+ }}
>
- {
- if (guestBlocked) {
- promptGuestSignup();
- return;
- }
- selectPolicy(cat.id);
- onExpand();
- }}
- >
- {cat.icon}
- {(status === "active" || status === "paused") && (
-
- )}
-
-
- );
- })}
+ {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
-
-
- Edit composition
-
-
- View runs
-
- >
- ),
+ 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
-
-
- Edit composition
-
-
- View runs
-
- >
+
+ 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 ? (
+
+
+
+ {barBody}
+
+
+
+ {(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:
after:
---
## 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: