diff --git a/ui/src/components/SidebarCompanyMenu.production.tsx b/ui/src/components/SidebarCompanyMenu.production.tsx index 8d2a50a2e3..476fcf6e87 100644 --- a/ui/src/components/SidebarCompanyMenu.production.tsx +++ b/ui/src/components/SidebarCompanyMenu.production.tsx @@ -20,7 +20,7 @@ import { } from "@dnd-kit/core"; import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import type { Company } from "@paperclipai/shared"; +import { hidesCompanyPage, type Company } from "@paperclipai/shared"; import { Link, useLocation, useNavigate } from "@/lib/router"; import { authApi } from "@/api/auth"; import { cloudApi, type CloudStackSummary } from "@/api/cloud"; @@ -36,6 +36,7 @@ import { import { useCompany } from "@/context/CompanyContext"; import { useDialogActions } from "@/context/DialogContext"; import { useCloudInstance } from "@/hooks/useCloudInstance"; +import { useHiddenSettings } from "@/hooks/useHiddenSettings"; import { useCompanyOrder } from "@/hooks/useCompanyOrder"; import { useSignOut } from "@/hooks/useSignOut"; import { navigateTopLevel } from "@/lib/browserNavigation"; @@ -234,6 +235,18 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb // exactly one company, and switching means leaving this tenant host entirely. const cloud = useCloudInstance(); const isCloud = Boolean(cloud); + // The invite shortcut points at the company Invites surface, so an operator + // that hides that surface via PAPERCLIP_HIDDEN_SETTINGS (company.invites or + // company.members) hides this shortcut too. This is the per-deployment knob + // Paperclip Cloud uses to drop the shortcut on its managed stacks while + // other hosters keep it; the streamlined menu already honors it, this shell + // was the gap. Until the health response resolves the hidden set is unknown + // — keep the shortcut out rather than flash it. + const { hidden: hiddenSettings, loaded: hiddenSettingsLoaded } = useHiddenSettings(); + const showInvitePeople = + hiddenSettingsLoaded && + !hidesCompanyPage(hiddenSettings, "company.members") && + !hidesCompanyPage(hiddenSettings, "company.invites"); const cloudBaseUrl = cloud?.cloudBaseUrl ?? null; const stacksQuery = useQuery({ queryKey: queryKeys.cloud.stacks, @@ -477,23 +490,25 @@ export function SidebarCompanyMenu({ open: controlledOpen, onOpenChange }: Sideb )} - - { - if (isEditingOrder) { - event.preventDefault(); - return; - } - closeNavigationChrome(); - }} - > - - - {currentName ? `Invite people to ${currentName}` : "Invite people"} - - - + {showInvitePeople ? ( + + { + if (isEditingOrder) { + event.preventDefault(); + return; + } + closeNavigationChrome(); + }} + > + + + {currentName ? `Invite people to ${currentName}` : "Invite people"} + + + + ) : null} {session?.session ? ( <> diff --git a/ui/src/components/SidebarCompanyMenu.test.tsx b/ui/src/components/SidebarCompanyMenu.test.tsx index 133cfea4ab..5c6cd697ef 100644 --- a/ui/src/components/SidebarCompanyMenu.test.tsx +++ b/ui/src/components/SidebarCompanyMenu.test.tsx @@ -7,6 +7,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { queryKeys } from "@/lib/queryKeys"; import { SidebarCompanyMenu } from "./SidebarCompanyMenu"; +import { SidebarCompanyMenu as SidebarCompanyMenuProduction } from "./SidebarCompanyMenu.production"; const mockAuthApi = vi.hoisted(() => ({ getSession: vi.fn(), @@ -370,6 +371,56 @@ describe("SidebarCompanyMenu", () => { }); }); + it("shows the production-shell invite shortcut when no surface is hidden", async () => { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + queryClient.setQueryData(queryKeys.health, { status: "ok" }); + const root = createRoot(container); + act(() => { + root.render( + + + , + ); + }); + await flushReact(); + await flushReact(); + + await openMenu("Open Acme Labs company switcher"); + + expect(document.body.textContent).toContain("Invite people to Acme Labs"); + + act(() => { + root.unmount(); + }); + }); + + it("hides the production-shell invite shortcut when the operator hides the invites surface", async () => { + // The production shell (streamlined UI disabled) must honor + // PAPERCLIP_HIDDEN_SETTINGS like the streamlined menu — this is the knob + // Paperclip Cloud uses to drop the shortcut on its managed stacks. + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + queryClient.setQueryData(queryKeys.health, { status: "ok", hiddenSettings: ["company.invites"] }); + const root = createRoot(container); + act(() => { + root.render( + + + , + ); + }); + await flushReact(); + await flushReact(); + + await openMenu("Open Acme Labs company switcher"); + + expect(document.body.textContent).toContain("Switch"); + expect(document.body.textContent).not.toContain("Invite people"); + + act(() => { + root.unmount(); + }); + }); + it("keeps the invite shortcut out of the menu until hidden settings resolve", async () => { // No health data in the cache: the hidden-settings set is unknown, so the // shortcut must not flash in and then disappear once the response lands.