diff --git a/apps/web/components/app-sidebar/app-sidebar-footer.test.tsx b/apps/web/components/app-sidebar/app-sidebar-footer.test.tsx index dc22b26cc0..fc574dc4aa 100644 --- a/apps/web/components/app-sidebar/app-sidebar-footer.test.tsx +++ b/apps/web/components/app-sidebar/app-sidebar-footer.test.tsx @@ -17,7 +17,6 @@ const state = { ], }, appSidebar: { settingsMode: false }, - toggleAppSidebarSettingsMode: mocks.toggleSettingsMode, }; let officeEnabled = false; @@ -64,7 +63,7 @@ import { AppSidebarFooter } from "./app-sidebar-footer"; function renderFooter() { return render( - + , ); } diff --git a/apps/web/components/app-sidebar/app-sidebar-footer.tsx b/apps/web/components/app-sidebar/app-sidebar-footer.tsx index 1df28655b5..8e5b67a83d 100644 --- a/apps/web/components/app-sidebar/app-sidebar-footer.tsx +++ b/apps/web/components/app-sidebar/app-sidebar-footer.tsx @@ -32,6 +32,7 @@ import { type AppSidebarFooterProps = { collapsed: boolean; + onToggleSettingsMode: () => void; }; type FooterIconButtonProps = { @@ -95,7 +96,7 @@ function FooterIconButton({ ); } -export function AppSidebarFooter({ collapsed }: AppSidebarFooterProps) { +export function AppSidebarFooter({ collapsed, onToggleSettingsMode }: AppSidebarFooterProps) { const router = useRouter(); const workspaces = useAppStore((s) => s.workspaces); const workspaceId = workspaces.activeId; @@ -105,7 +106,6 @@ export function AppSidebarFooter({ collapsed }: AppSidebarFooterProps) { ? resolveLastKanbanWorkspace(workspaces.items) : resolveLastOfficeWorkspace(workspaces.items); const settingsMode = useAppStore((s) => s.appSidebar.settingsMode); - const toggleSettingsMode = useAppStore((s) => s.toggleAppSidebarSettingsMode); const officeEnabled = useFeature("office"); const releaseNotes = useReleaseNotes(); const [improveOpen, setImproveOpen] = useState(false); @@ -121,7 +121,7 @@ export function AppSidebarFooter({ collapsed }: AppSidebarFooterProps) { icon={IconSettings} label={settingsMode ? "Close settings" : "Settings"} collapsed={collapsed} - onClick={toggleSettingsMode} + onClick={onToggleSettingsMode} active={settingsMode} testId="sidebar-settings-gear" /> diff --git a/apps/web/components/app-sidebar/app-sidebar.tsx b/apps/web/components/app-sidebar/app-sidebar.tsx index 655b263f1e..2355b92f9e 100644 --- a/apps/web/components/app-sidebar/app-sidebar.tsx +++ b/apps/web/components/app-sidebar/app-sidebar.tsx @@ -103,6 +103,7 @@ export function AppSidebar() { const storedWidth = useAppStore((s) => s.appSidebar.width); const toggleSection = useAppStore((s) => s.toggleAppSidebarSection); const toggleCollapsed = useAppStore((s) => s.toggleAppSidebar); + const toggleSettingsMode = useAppStore((s) => s.toggleAppSidebarSettingsMode); const setSettingsMode = useAppStore((s) => s.setAppSidebarSettingsMode); const setWidth = useAppStore((s) => s.setAppSidebarWidth); const pathname = usePathname(); @@ -140,6 +141,14 @@ export function AppSidebar() { ); const expandedWidth = Math.max(APP_SIDEBAR_EXPANDED_WIDTH, storedWidth); + const settingsModeTogglePathnameRef = useRef(null); + + const handleToggleSettingsMode = useCallback(() => { + // History updates before React renders the new pathname; remember which + // route the user actually clicked on so delayed route sync cannot undo it. + settingsModeTogglePathnameRef.current = window.location.pathname; + toggleSettingsMode(); + }, [toggleSettingsMode]); // Keep the transient settings takeover aligned with route ownership. It is // intentionally not persisted, so direct reloads on `/settings/...` need to @@ -150,10 +159,11 @@ export function AppSidebar() { if (!pathname || prevPathnameRef.current === pathname) return; prevPathnameRef.current = pathname; if (isSettingsRoute(pathname)) { + settingsModeTogglePathnameRef.current = null; if (!settingsMode) setSettingsMode(true); return; } - if (settingsMode) { + if (settingsMode && settingsModeTogglePathnameRef.current !== pathname) { setSettingsMode(false); } }, [pathname, settingsMode, setSettingsMode]); @@ -192,7 +202,7 @@ export function AppSidebar() { > - + {!collapsed && } ); diff --git a/apps/web/e2e/tests/settings/settings-gear-only.spec.ts b/apps/web/e2e/tests/settings/settings-gear-only.spec.ts index 91c83605aa..a95eb58519 100644 --- a/apps/web/e2e/tests/settings/settings-gear-only.spec.ts +++ b/apps/web/e2e/tests/settings/settings-gear-only.spec.ts @@ -72,4 +72,23 @@ test.describe("Settings sidebar takeover", () => { await expect(sidebar).toHaveAttribute("data-collapsed", "false"); await expect(takeover).toBeVisible(); }); + + test("a route change cannot undo the first gear click", async ({ testPage }) => { + await testPage.goto("/"); + + const takeover = testPage.getByTestId("app-sidebar-settings-mode"); + await expect(testPage.getByTestId("sidebar-settings-gear")).toBeVisible(); + + // Keep both actions in one browser task so the route-sync effect is still + // pending when the user action arrives. The click must win. + await testPage.evaluate(() => { + window.history.pushState({}, "", "/tasks"); + window.dispatchEvent(new Event("kandev:navigation")); + const gear = document.querySelector('[data-testid="sidebar-settings-gear"]'); + if (!gear) throw new Error("sidebar-settings-gear not found"); + gear.click(); + }); + + await expect(takeover).toBeVisible(); + }); });