From 9d6fbff9b42ee0ffcc6e2cce28440f113c5f2dfc Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 21 May 2026 13:47:06 +0000 Subject: [PATCH] Fix plugin route sidebar on nested page loads Co-Authored-By: Paperclip --- ui/src/components/Layout.test.tsx | 62 ++++++++++++++++++++++++++++++- ui/src/components/Layout.tsx | 9 +++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/ui/src/components/Layout.test.tsx b/ui/src/components/Layout.test.tsx index 998607145f3..c67d72c1a43 100644 --- a/ui/src/components/Layout.test.tsx +++ b/ui/src/components/Layout.test.tsx @@ -39,8 +39,11 @@ vi.mock("@/lib/router", () => ({ useNavigate: () => mockNavigate, useNavigationType: () => "PUSH", useParams: () => { - const firstSegment = currentPathname.split("/").filter(Boolean)[0]; - return { companyPrefix: firstSegment === "instance" ? undefined : firstSegment ?? "PAP" }; + const [firstSegment, secondSegment] = currentPathname.split("/").filter(Boolean); + return { + companyPrefix: firstSegment === "instance" ? undefined : firstSegment ?? "PAP", + pluginRoutePath: firstSegment === "instance" ? undefined : secondSegment, + }; }, })); @@ -447,6 +450,61 @@ describe("Layout", () => { }); }); + it("keeps the route-scoped plugin sidebar on nested plugin page routes", async () => { + currentPathname = "/PAP/wiki/page/templates"; + mockPluginSlots.slots = [ + { + type: "page", + id: "wiki-page", + displayName: "Wiki Page", + exportName: "WikiPage", + routePath: "wiki", + pluginId: "plugin-1", + pluginKey: "wiki-plugin", + pluginDisplayName: "Wiki Plugin", + pluginVersion: "1.0.0", + }, + { + type: "routeSidebar", + id: "wiki-route-sidebar", + displayName: "Wiki Sidebar", + exportName: "WikiSidebar", + routePath: "wiki", + pluginId: "plugin-1", + pluginKey: "wiki-plugin", + pluginDisplayName: "Wiki Plugin", + pluginVersion: "1.0.0", + }, + ]; + const root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + await act(async () => { + root.render( + + + , + ); + }); + await flushReact(); + await flushReact(); + + expect(mockUsePluginSlots).toHaveBeenCalledWith( + expect.objectContaining({ + companyId: "company-1", + enabled: true, + }), + ); + expect(container.textContent).toContain("Plugin route sidebar: Wiki Sidebar"); + expect(container.textContent).not.toContain("Main company nav"); + + await act(async () => { + root.unmount(); + }); + }); + it("uses the route company context for plugin route sidebars on the first render", async () => { currentPathname = "/ALT/wiki"; mockCompanyState.companies = [ diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 2b268c076f6..d5a76fcdc91 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -74,7 +74,10 @@ export function Layout() { selectionSource, setSelectedCompanyId, } = useCompany(); - const { companyPrefix } = useParams<{ companyPrefix: string }>(); + const { + companyPrefix, + pluginRoutePath: matchedPluginRoutePath, + } = useParams<{ companyPrefix: string; pluginRoutePath?: string }>(); const navigate = useNavigate(); const location = useLocation(); const navigationType = useNavigationType(); @@ -95,8 +98,8 @@ export function Layout() { const hasUnknownCompanyPrefix = Boolean(companyPrefix) && !companiesLoading && companies.length > 0 && !matchedCompany; const pluginRoutePath = useMemo( - () => getCompanyRouteSegment(location.pathname, companyPrefix), - [companyPrefix, location.pathname], + () => matchedPluginRoutePath?.toLowerCase() ?? getCompanyRouteSegment(location.pathname, companyPrefix), + [companyPrefix, location.pathname, matchedPluginRoutePath], ); const routeSidebarCompanyId = matchedCompany?.id ?? null; const routeSidebarCompanyPrefix = matchedCompany?.issuePrefix ?? null;