From 0da1f70b916e8a50981237f186b49686b9f1677d Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Tue, 21 Apr 2026 02:18:17 +0700 Subject: [PATCH] fix(frontend): show org-wide settings badge beside title on org-defaults pages (#14031) --- .../features/user/user-context-menu.test.tsx | 22 +++- .../hooks/use-settings-nav-items.test.tsx | 18 ++- .../__tests__/routes/llm-settings.test.tsx | 7 +- frontend/__tests__/routes/settings.test.tsx | 115 ++++++++++++++++++ .../utils/personal-workspace-guard.test.ts | 8 +- .../features/settings/org-defaults-banner.tsx | 2 - frontend/src/hooks/use-settings-nav-items.ts | 4 +- frontend/src/routes/condenser-settings.tsx | 6 +- frontend/src/routes/llm-settings.tsx | 15 +-- .../routes/org-default-condenser-settings.tsx | 2 +- .../src/routes/org-default-llm-settings.tsx | 2 +- .../org-default-verification-settings.tsx | 2 +- frontend/src/routes/settings.tsx | 16 +-- frontend/src/routes/verification-settings.tsx | 13 +- ...=> saas-redirect-to-org-defaults-guard.ts} | 6 +- 15 files changed, 185 insertions(+), 53 deletions(-) rename frontend/src/utils/org/{personal-workspace-guard.ts => saas-redirect-to-org-defaults-guard.ts} (92%) diff --git a/frontend/__tests__/components/features/user/user-context-menu.test.tsx b/frontend/__tests__/components/features/user/user-context-menu.test.tsx index ea022efeea..a1953bbb03 100644 --- a/frontend/__tests__/components/features/user/user-context-menu.test.tsx +++ b/frontend/__tests__/components/features/user/user-context-menu.test.tsx @@ -192,12 +192,21 @@ describe("UserContextMenu", () => { renderUserContextMenu({ type: "member", onClose: vi.fn, onOpenInviteModal: vi.fn }); - // Wait for config to load and verify that navigation items are rendered (except organization-members/org which are filtered out) + // In SaaS, personal LLM/Condenser/Verification routes are hidden in favor + // of /settings/org-defaults/* (visible only when an org is selected, which + // this test does not seed). Org-only and billing routes are also filtered. + const personalLlmPaths = new Set([ + "/settings", + "/settings/condenser", + "/settings/verification", + ]); const expectedItems = SAAS_NAV_ITEMS.filter( (item) => item.to !== "/settings/org-members" && item.to !== "/settings/org" && - item.to !== "/settings/billing", + item.to !== "/settings/billing" && + !item.to.startsWith("/settings/org-defaults") && + !personalLlmPaths.has(item.to), ); await waitFor(() => { @@ -366,6 +375,15 @@ describe("UserContextMenu", () => { }, }), ); + // The LLM nav item now lives under /settings/org-defaults, which only + // appears when an org is selected. Seed a personal org for that. + vi.spyOn(organizationService, "getOrganizations").mockResolvedValue({ + items: [MOCK_PERSONAL_ORG], + currentOrgId: MOCK_PERSONAL_ORG.id, + }); + useSelectedOrganizationStore.setState({ + organizationId: MOCK_PERSONAL_ORG.id, + }); renderUserContextMenu({ type: "member", onClose: vi.fn, onOpenInviteModal: vi.fn }); diff --git a/frontend/__tests__/hooks/use-settings-nav-items.test.tsx b/frontend/__tests__/hooks/use-settings-nav-items.test.tsx index f7e39c5c22..9e435189ca 100644 --- a/frontend/__tests__/hooks/use-settings-nav-items.test.tsx +++ b/frontend/__tests__/hooks/use-settings-nav-items.test.tsx @@ -113,8 +113,12 @@ describe("useSettingsNavItems", () => { expect(findItemByPath(result.current, "/settings/billing")).toBeUndefined(); expect(findItemByPath(result.current, "/settings/org")).toBeUndefined(); expect(findItemByPath(result.current, "/settings/org-members")).toBeUndefined(); - // But should see other items - expect(findItemByPath(result.current, "/settings")).toBeDefined(); + // Personal LLM/Condenser/Verification routes are hidden in SaaS; + // members see the org-defaults equivalents (read-only on the page itself). + expect(findItemByPath(result.current, "/settings")).toBeUndefined(); + expect( + findItemByPath(result.current, "/settings/org-defaults"), + ).toBeDefined(); expect(findItemByPath(result.current, "/settings/user")).toBeDefined(); }); }); @@ -375,9 +379,13 @@ describe("useSettingsNavItems", () => { expect( findItemByPath(result.current, "/settings/integrations"), ).toBeUndefined(); - // Non-hidden pages should still be present + // Personal LLM is hidden in SaaS; the org-defaults equivalent + // shows up instead (an org is selected in this test's setup). expect( findItemByPath(result.current, "/settings"), + ).toBeUndefined(); + expect( + findItemByPath(result.current, "/settings/org-defaults"), ).toBeDefined(); expect( findItemByPath(result.current, "/settings/app"), @@ -406,8 +414,12 @@ describe("useSettingsNavItems", () => { expect( findItemByPath(result.current, "/settings/integrations"), ).toBeDefined(); + // Personal LLM is hidden in SaaS; users see /settings/org-defaults instead. expect( findItemByPath(result.current, "/settings"), + ).toBeUndefined(); + expect( + findItemByPath(result.current, "/settings/org-defaults"), ).toBeDefined(); expect( findItemByPath(result.current, "/settings/app"), diff --git a/frontend/__tests__/routes/llm-settings.test.tsx b/frontend/__tests__/routes/llm-settings.test.tsx index 0106dbb6e9..e99705653c 100644 --- a/frontend/__tests__/routes/llm-settings.test.tsx +++ b/frontend/__tests__/routes/llm-settings.test.tsx @@ -554,7 +554,7 @@ describe("LlmSettingsScreen", () => { ).toBeInTheDocument(); }); - it("should not show info message for personal workspace", async () => { + it("shows the personal info message for personal workspace in SaaS mode", async () => { vi.spyOn(SettingsService, "getSettings").mockResolvedValue( buildSettings(), ); @@ -566,10 +566,9 @@ describe("LlmSettingsScreen", () => { organizations: [buildOrganization({ id: "1", is_personal: true })], }); - await screen.findByTestId("llm-settings-screen"); expect( - screen.queryByTestId("llm-settings-info-message"), - ).not.toBeInTheDocument(); + await screen.findByTestId("llm-settings-info-message"), + ).toBeInTheDocument(); }); it("should not show info message in OSS mode", async () => { diff --git a/frontend/__tests__/routes/settings.test.tsx b/frontend/__tests__/routes/settings.test.tsx index 24c286270c..f04319af7f 100644 --- a/frontend/__tests__/routes/settings.test.tsx +++ b/frontend/__tests__/routes/settings.test.tsx @@ -122,6 +122,32 @@ describe("Settings Screen", () => { Component: () =>
, path: "/settings/org", }, + { + Component: () => , + path: "/settings/condenser", + }, + { + Component: () => , + path: "/settings/verification", + }, + { + Component: () => ( + + ), + path: "/settings/org-defaults", + }, + { + Component: () => ( + + ), + path: "/settings/org-defaults/condenser", + }, + { + Component: () => ( + + ), + path: "/settings/org-defaults/verification", + }, ], }, ]); @@ -792,6 +818,95 @@ describe("Settings Screen", () => { ).toBeInTheDocument(); }); }); + + describe("OrgWideSettingsBadge display", () => { + const seedSaasOrgContext = ( + org: typeof MOCK_TEAM_ORG_ACME | typeof MOCK_PERSONAL_ORG, + user: Partial{t(I18nKey.SETTINGS$ORG_DEFAULTS_INFO)}
diff --git a/frontend/src/hooks/use-settings-nav-items.ts b/frontend/src/hooks/use-settings-nav-items.ts index 2e6f43996c..19737f6749 100644 --- a/frontend/src/hooks/use-settings-nav-items.ts +++ b/frontend/src/hooks/use-settings-nav-items.ts @@ -71,7 +71,7 @@ export function useSettingsNavItems(): SettingsNavRenderedItem[] { items = items.filter((item) => item.to !== "/settings/org-members"); } - if (!hasPermission("edit_llm_settings") || !organizationId || isPersonalOrg) { + if (!organizationId) { items = items.filter( (item) => !item.to.startsWith("/settings/org-defaults"), ); @@ -82,7 +82,7 @@ export function useSettingsNavItems(): SettingsNavRenderedItem[] { "/settings/condenser", "/settings/verification", ]); - if (isTeamOrg) { + if (isSaasMode) { items = items.filter((item) => !PERSONAL_LLM_PATHS.has(item.to)); } diff --git a/frontend/src/routes/condenser-settings.tsx b/frontend/src/routes/condenser-settings.tsx index 4cef893712..8222b18f5c 100644 --- a/frontend/src/routes/condenser-settings.tsx +++ b/frontend/src/routes/condenser-settings.tsx @@ -1,6 +1,6 @@ import { SdkSectionPage } from "#/components/features/settings/sdk-settings/sdk-section-page"; import { createPermissionGuard } from "#/utils/org/permission-guard"; -import { requirePersonalWorkspaceLoader } from "#/utils/org/personal-workspace-guard"; +import { requireOrgDefaultsRedirect } from "#/utils/org/saas-redirect-to-org-defaults-guard"; function CondenserSettingsScreen() { return ( @@ -11,13 +11,13 @@ function CondenserSettingsScreen() { ); } -const personalWorkspaceGuard = requirePersonalWorkspaceLoader( +const orgDefaultsRedirectGuard = requireOrgDefaultsRedirect( "/settings/org-defaults/condenser", ); const condenserPermissionGuard = createPermissionGuard("view_llm_settings"); export const clientLoader = async (args: { request: Request }) => { - const blocked = await personalWorkspaceGuard(args); + const blocked = await orgDefaultsRedirectGuard(args); if (blocked) return blocked; return condenserPermissionGuard(args); }; diff --git a/frontend/src/routes/llm-settings.tsx b/frontend/src/routes/llm-settings.tsx index e73b53691e..67b0cddfdf 100644 --- a/frontend/src/routes/llm-settings.tsx +++ b/frontend/src/routes/llm-settings.tsx @@ -3,14 +3,13 @@ import { useSearchParams } from "react-router"; import { useTranslation } from "react-i18next"; import { ModelSelector } from "#/components/shared/modals/settings/model-selector"; import { createPermissionGuard } from "#/utils/org/permission-guard"; -import { requirePersonalWorkspaceLoader } from "#/utils/org/personal-workspace-guard"; +import { requireOrgDefaultsRedirect } from "#/utils/org/saas-redirect-to-org-defaults-guard"; import { useAgentSettingsSchema } from "#/hooks/query/use-agent-settings-schema"; import { useSettings } from "#/hooks/query/use-settings"; import { SettingsInput } from "#/components/features/settings/settings-input"; import { HelpLink } from "#/ui/help-link"; import { useConfig } from "#/hooks/query/use-config"; import { KeyStatusIcon } from "#/components/features/settings/key-status-icon"; -import { useOrgTypeAndAccess } from "#/hooks/use-org-type-and-access"; import { SdkSectionHeaderProps, SdkSectionPage, @@ -21,7 +20,6 @@ import { displaySuccessToast, } from "#/utils/custom-toast-handlers"; import { Settings, SettingsSchema, SettingsScope } from "#/types/settings"; -import { OrgWideSettingsBadge } from "#/components/features/settings/org-wide-settings-badge"; import { extractModelAndProvider } from "#/utils/extract-model-and-provider"; import { inferInitialView, @@ -126,7 +124,6 @@ export function LlmSettingsScreen({ settings?.agent_settings_schema, ); const { data: config } = useConfig(); - const { isTeamOrg } = useOrgTypeAndAccess(); const [selectedProvider, setSelectedProvider] = React.useState {
- const blocked = await personalWorkspaceGuard(args);
+ const blocked = await orgDefaultsRedirectGuard(args);
if (blocked) return blocked;
return llmPermissionGuard(args);
};
diff --git a/frontend/src/routes/org-default-condenser-settings.tsx b/frontend/src/routes/org-default-condenser-settings.tsx
index 958f269344..fe962f9ea7 100644
--- a/frontend/src/routes/org-default-condenser-settings.tsx
+++ b/frontend/src/routes/org-default-condenser-settings.tsx
@@ -16,7 +16,7 @@ function OrgDefaultCondenserSettingsScreen() {
}
export const clientLoader = createPermissionGuard(
- "edit_llm_settings",
+ "view_llm_settings",
"/settings/condenser",
);
diff --git a/frontend/src/routes/org-default-llm-settings.tsx b/frontend/src/routes/org-default-llm-settings.tsx
index 9bcfb0e183..48507b3fcc 100644
--- a/frontend/src/routes/org-default-llm-settings.tsx
+++ b/frontend/src/routes/org-default-llm-settings.tsx
@@ -2,7 +2,7 @@ import { createPermissionGuard } from "#/utils/org/permission-guard";
import { LlmSettingsScreen } from "./llm-settings";
export const clientLoader = createPermissionGuard(
- "edit_llm_settings",
+ "view_llm_settings",
"/settings",
);
diff --git a/frontend/src/routes/org-default-verification-settings.tsx b/frontend/src/routes/org-default-verification-settings.tsx
index d608473c4a..f46c62f2bb 100644
--- a/frontend/src/routes/org-default-verification-settings.tsx
+++ b/frontend/src/routes/org-default-verification-settings.tsx
@@ -15,7 +15,7 @@ function OrgDefaultVerificationSettingsScreen() {
}
export const clientLoader = createPermissionGuard(
- "edit_llm_settings",
+ "view_llm_settings",
"/settings/verification",
);
diff --git a/frontend/src/routes/settings.tsx b/frontend/src/routes/settings.tsx
index f4e037c864..c5e15f3637 100644
--- a/frontend/src/routes/settings.tsx
+++ b/frontend/src/routes/settings.tsx
@@ -18,7 +18,6 @@ import {
isSettingsPageHidden,
getFirstAvailablePath,
} from "#/utils/settings-utils";
-import { useMe } from "#/hooks/query/use-me";
import { useOrgTypeAndAccess } from "#/hooks/use-org-type-and-access";
import { useConfig } from "#/hooks/query/use-config";
import { OrgWideSettingsBadge } from "#/components/features/settings/org-wide-settings-badge";
@@ -32,6 +31,12 @@ const SAAS_ONLY_PATHS = [
"/settings/org",
];
+const ORG_WIDE_BADGE_PATHS = new Set