From e3e49c07aef1aaccdc29a6b87307f56050850791 Mon Sep 17 00:00:00 2001 From: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:51:26 +0100 Subject: [PATCH] Policies: let team leaders configure policies in the UI (#6634) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend follow-up to #6632 (team-scoped policies, editing gated to team leaders on the backend). Brings the UI's edit gate in line. ## Problem The policy config UI gated editing to `config.isAdmin`. On SaaS, org users are never the single global admin, so **no one could open the policy editor** — the same lockout #6632 fixed on the backend. ## Fix `usePolicies` now allows a **team leader** to configure, falling back to a global admin self-hosted: ```ts canConfigure = config != null && (!config.enableLogin || isTeamLeader || config.isAdmin === true); ``` - SaaS → `isTeamLeader` (from `useSaaSTeam()`) — team leaders can configure; members get the read-only surface. - Self-hosted → `config.isAdmin` (the core `useSaaSTeam` stub returns `false`, so admins aren't locked out). - Login disabled (single-user) → always allowed. - The `config != null` guard keeps the gate closed until app-config resolves, so edit controls never flash for users who can't use them. The two locked-policy banners now read "Contact a team leader to change this policy" (updated in the `t()` defaults and the `en-GB` translations). ## Verification - Typecheck clean (proprietary + saas); eslint clean. - Tests pass: `usePolicies`, `PoliciesSidebar`. --- .../public/locales/en-GB/translation.toml | 4 ++-- .../components/policies/PolicyDetailPanel.tsx | 2 +- .../components/policies/PolicySetupWizard.tsx | 2 +- .../src/proprietary/hooks/usePolicies.ts | 18 ++++++++---------- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/frontend/editor/public/locales/en-GB/translation.toml b/frontend/editor/public/locales/en-GB/translation.toml index 3a0dadacf..342fa0c80 100644 --- a/frontend/editor/public/locales/en-GB/translation.toml +++ b/frontend/editor/public/locales/en-GB/translation.toml @@ -5869,7 +5869,7 @@ security = "Security" [policies.detail] editSettings = "Edit Settings" enforces = "Enforces" -managedByOrg = "Managed by your organization. Contact an admin to change this policy." +managedByOrg = "Managed by your organization. Contact a team leader to change this policy." noActivityDescription = "Documents will appear here once this policy runs." noActivityTitle = "No activity yet" onEveryUpload = "On every upload" @@ -5931,7 +5931,7 @@ filenamePrefix = "Prefix" filenameSuffix = "Suffix" filenameTextAria = "Filename text" filenameTextPlaceholder = "Text to add (optional)" -lockedDescription = "Contact an admin to change this policy." +lockedDescription = "Contact a team leader to change this policy." lockedTitle = "Managed by your organization" maxRetriesLabel = "Max retries" noToolsError = "Add at least one configured tool to the workflow first." diff --git a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx index 76d5a9688..114f1ac16 100644 --- a/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx +++ b/frontend/editor/src/proprietary/components/policies/PolicyDetailPanel.tsx @@ -279,7 +279,7 @@ export function PolicyDetailPanel({ icon={} description={t( "policies.detail.managedByOrg", - "Managed by your organization. Contact an admin to change this policy.", + "Managed by your organization. Contact a team leader to change this policy.", )} /> )} diff --git a/frontend/editor/src/proprietary/components/policies/PolicySetupWizard.tsx b/frontend/editor/src/proprietary/components/policies/PolicySetupWizard.tsx index 6bb2ce312..27dedf617 100644 --- a/frontend/editor/src/proprietary/components/policies/PolicySetupWizard.tsx +++ b/frontend/editor/src/proprietary/components/policies/PolicySetupWizard.tsx @@ -179,7 +179,7 @@ export function PolicySetupWizard({ )} description={t( "policies.wizard.lockedDescription", - "Contact an admin to change this policy.", + "Contact a team leader to change this policy.", )} /> diff --git a/frontend/editor/src/proprietary/hooks/usePolicies.ts b/frontend/editor/src/proprietary/hooks/usePolicies.ts index a1ed78a28..292cb9d0b 100644 --- a/frontend/editor/src/proprietary/hooks/usePolicies.ts +++ b/frontend/editor/src/proprietary/hooks/usePolicies.ts @@ -8,6 +8,7 @@ import { useState, useEffect, useCallback } from "react"; import { useAppConfig } from "@app/contexts/AppConfigContext"; +import { useSaaSTeam } from "@app/contexts/SaaSTeamContext"; import { loadPolicies, onPoliciesChange, @@ -65,6 +66,7 @@ function toStoreRequest( export function usePolicies() { const [policies, setPolicies] = useState(loadPolicies); const { config } = useAppConfig(); + const { isTeamLeader } = useSaaSTeam(); useEffect(() => onPoliciesChange(() => setPolicies(loadPolicies())), []); @@ -293,17 +295,13 @@ export function usePolicies() { return folder.id; }, []); - // Editing/creating policies is admin-only, mirroring the backend's enforcement - // on the save/delete endpoints. On single-user deployments (login disabled) - // there's no admin concept, so the local operator can always configure. When - // login is enabled, only admins can — non-admins get the read-only surface. - // The `config != null` guard keeps the gate CLOSED until app-config resolves: - // config is null while loading (and in bare test renders), and a failed/partial - // fetch can yield `{ enableLogin: true }` with isAdmin omitted. Without it a - // non-admin would briefly see edit controls during load, and an admin would be - // wrongly locked out on a transient config-fetch error. + // Only a team leader (SaaS) or a global admin (self-hosted) may configure; + // everyone else gets the read-only surface. Login disabled (single-user) + // always can. Stays closed until config loads, so edit controls never flash + // for users who can't use them. const canConfigure = - config != null && (!config.enableLogin || config.isAdmin === true); + config != null && + (!config.enableLogin || isTeamLeader || config.isAdmin === true); return { policies,