fix(web): navigate to Settings on the first gear click from a session view (#1775)
This commit is contained in:
@@ -20,9 +20,11 @@ const state = {
|
||||
};
|
||||
|
||||
let officeEnabled = false;
|
||||
let pathname = "/tasks/session-1";
|
||||
|
||||
vi.mock("@/lib/routing/client-router", () => ({
|
||||
useRouter: () => ({ push: mocks.routerPush }),
|
||||
usePathname: () => pathname,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/state-provider", () => ({
|
||||
@@ -71,6 +73,7 @@ function renderFooter() {
|
||||
describe("AppSidebarFooter", () => {
|
||||
beforeEach(() => {
|
||||
officeEnabled = false;
|
||||
pathname = "/tasks/session-1";
|
||||
state.workspaces.activeId = "kanban-1";
|
||||
state.workspaces.items = [
|
||||
{ id: "kanban-1", name: "Kanban", office_workflow_id: "" },
|
||||
@@ -162,4 +165,36 @@ describe("AppSidebarFooter", () => {
|
||||
expect(document.cookie).toContain("office-active-workspace=office-2");
|
||||
expect(mocks.routerPush).toHaveBeenCalledWith("/?workspaceId=kanban-1");
|
||||
});
|
||||
it("navigates to /settings when the gear opens settings mode from a non-settings route", () => {
|
||||
pathname = "/tasks/session-1";
|
||||
state.appSidebar.settingsMode = false;
|
||||
|
||||
renderFooter();
|
||||
fireEvent.click(screen.getByTestId("sidebar-settings-gear"));
|
||||
|
||||
expect(mocks.routerPush).toHaveBeenCalledWith("/settings");
|
||||
expect(mocks.toggleSettingsMode).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not navigate when the gear reopens settings mode while already on a settings route", () => {
|
||||
pathname = "/settings/agents";
|
||||
state.appSidebar.settingsMode = false;
|
||||
|
||||
renderFooter();
|
||||
fireEvent.click(screen.getByTestId("sidebar-settings-gear"));
|
||||
|
||||
expect(mocks.routerPush).not.toHaveBeenCalled();
|
||||
expect(mocks.toggleSettingsMode).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not navigate when the gear closes an already-open settings mode", () => {
|
||||
pathname = "/settings";
|
||||
state.appSidebar.settingsMode = true;
|
||||
|
||||
renderFooter();
|
||||
fireEvent.click(screen.getByTestId("sidebar-settings-gear"));
|
||||
|
||||
expect(mocks.routerPush).not.toHaveBeenCalled();
|
||||
expect(mocks.toggleSettingsMode).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "@/lib/routing/client-router";
|
||||
import { useRouter, usePathname } from "@/lib/routing/client-router";
|
||||
import {
|
||||
IconBuildings,
|
||||
IconChartBar,
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
resolveLastKanbanWorkspace,
|
||||
workspaceHomeHref,
|
||||
} from "./app-sidebar-workspace-navigation";
|
||||
import { isSettingsRoute } from "./app-sidebar-route";
|
||||
|
||||
type AppSidebarFooterProps = {
|
||||
collapsed: boolean;
|
||||
@@ -98,6 +99,7 @@ function FooterIconButton({
|
||||
|
||||
export function AppSidebarFooter({ collapsed, onToggleSettingsMode }: AppSidebarFooterProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const workspaces = useAppStore((s) => s.workspaces);
|
||||
const workspaceId = workspaces.activeId;
|
||||
const activeWorkspace = workspaces.items.find((workspace) => workspace.id === workspaceId);
|
||||
@@ -106,6 +108,17 @@ export function AppSidebarFooter({ collapsed, onToggleSettingsMode }: AppSidebar
|
||||
? resolveLastKanbanWorkspace(workspaces.items)
|
||||
: resolveLastOfficeWorkspace(workspaces.items);
|
||||
const settingsMode = useAppStore((s) => s.appSidebar.settingsMode);
|
||||
const enterSettings = () => {
|
||||
// The gear only swaps the sidebar's own content; without also
|
||||
// navigating, the main panel keeps showing whatever the user was on
|
||||
// (e.g. a task session), so the first click looks like a no-op and a
|
||||
// second click (on a tree leaf) is what actually reaches Settings.
|
||||
// Match the Stats/Office buttons: one click gets you there.
|
||||
if (!settingsMode && !isSettingsRoute(pathname)) {
|
||||
router.push("/settings");
|
||||
}
|
||||
onToggleSettingsMode();
|
||||
};
|
||||
const officeEnabled = useFeature("office");
|
||||
const releaseNotes = useReleaseNotes();
|
||||
const [improveOpen, setImproveOpen] = useState(false);
|
||||
@@ -121,7 +134,7 @@ export function AppSidebarFooter({ collapsed, onToggleSettingsMode }: AppSidebar
|
||||
icon={IconSettings}
|
||||
label={settingsMode ? "Close settings" : "Settings"}
|
||||
collapsed={collapsed}
|
||||
onClick={onToggleSettingsMode}
|
||||
onClick={enterSettings}
|
||||
active={settingsMode}
|
||||
testId="sidebar-settings-gear"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isSettingsRoute(pathname: string | null): boolean {
|
||||
return pathname === "/settings" || Boolean(pathname?.startsWith("/settings/"));
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { usePathname } from "@/lib/routing/client-router";
|
||||
import { isSettingsRoute } from "./app-sidebar-route";
|
||||
import { useAppStore } from "@/components/state-provider";
|
||||
import { useEnsureWorkspaceWorkflows } from "@/hooks/use-workflows";
|
||||
import { useInOffice } from "@/hooks/use-in-office";
|
||||
@@ -40,10 +41,6 @@ const SECTION_ROUTE_MAP: Array<{ id: string; matches: (path: string) => boolean
|
||||
{ id: APP_SIDEBAR_SECTION_IDS.agents, matches: (p) => p.startsWith("/office/agents") },
|
||||
];
|
||||
|
||||
function isSettingsRoute(pathname: string | null): boolean {
|
||||
return pathname === "/settings" || Boolean(pathname?.startsWith("/settings/"));
|
||||
}
|
||||
|
||||
type AppSidebarNavigationProps = {
|
||||
collapsed: boolean;
|
||||
inOffice: boolean;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
|
||||
// Settings has no nav "Settings" section; direct settings routes keep the
|
||||
// gear-gated tree open so a hard refresh preserves settings navigation context.
|
||||
@@ -73,6 +74,34 @@ test.describe("Settings sidebar takeover", () => {
|
||||
await expect(takeover).toBeVisible();
|
||||
});
|
||||
|
||||
test("the gear navigates to Settings in one click from a task session view", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
// Regression: from a session view the gear used to only swap the
|
||||
// sidebar's own tree — the main panel kept showing the task, so the
|
||||
// click looked like a no-op. A second click (on a tree leaf) was
|
||||
// needed to actually reach Settings. The gear must now behave like
|
||||
// the sibling Stats/Office footer buttons and navigate in one click.
|
||||
const task = await apiClient.createTask(seedData.workspaceId, "Gear Nav Task", {
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
});
|
||||
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
|
||||
const takeover = testPage.getByTestId("app-sidebar-settings-mode");
|
||||
await expect(takeover).toHaveCount(0);
|
||||
|
||||
await testPage.getByTestId("sidebar-settings-gear").click();
|
||||
|
||||
await expect(testPage).toHaveURL(/\/settings$/);
|
||||
await expect(takeover).toBeVisible();
|
||||
});
|
||||
|
||||
test("a route change cannot undo the first gear click", async ({ testPage }) => {
|
||||
await testPage.goto("/");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user