diff --git a/src/renderer/components/ui/hooks.tsx b/src/renderer/components/ui/hooks.tsx index a849b7e8..1dc8c433 100644 --- a/src/renderer/components/ui/hooks.tsx +++ b/src/renderer/components/ui/hooks.tsx @@ -66,11 +66,33 @@ export function useReducedMotion() { /* ErrorBoundary */ /* ------------------------------------------------------------------ */ -export class ErrorBoundary extends Component<{ children: ReactNode; fallback?: ReactNode }, { hasError: boolean; error: Error | null }> { - constructor(props: { children: ReactNode; fallback?: ReactNode }) { +type ErrorBoundaryResetKey = string | number | null | undefined; + +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode; + resetKey?: ErrorBoundaryResetKey; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + resetKey: ErrorBoundaryResetKey; +} + +export class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { super(props); - this.state = { hasError: false, error: null }; + this.state = { hasError: false, error: null, resetKey: props.resetKey }; } + + static getDerivedStateFromProps(props: ErrorBoundaryProps, state: ErrorBoundaryState) { + if (props.resetKey !== state.resetKey) { + return { hasError: false, error: null, resetKey: props.resetKey }; + } + return null; + } + static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } diff --git a/src/renderer/hooks/useUIController.ts b/src/renderer/hooks/useUIController.ts index d1570e28..d87a5096 100644 --- a/src/renderer/hooks/useUIController.ts +++ b/src/renderer/hooks/useUIController.ts @@ -7,7 +7,7 @@ import { useAppStore } from "../state/store"; import { useFocusTrap, useReducedMotion } from "../components/ui"; import { SquaresFour } from "../components/ui/icons"; import { - SETTINGS_NAV_ITEM_DEF, + COMMAND_PALETTE_NAV_ITEM_DEFS, SIDEBAR_NAV_GROUP_DEFS, type NavItemDef, } from "../navigation"; @@ -44,8 +44,8 @@ export function useUIController() { [sidebarGroups], ); const navItems: NavItem[] = useMemo( - () => sidebarNav.concat(toNavItem(SETTINGS_NAV_ITEM_DEF, t)), - [sidebarNav, t], + () => COMMAND_PALETTE_NAV_ITEM_DEFS.map((item) => toNavItem(item, t)), + [t], ); const { diff --git a/src/renderer/navigation.ts b/src/renderer/navigation.ts index b8db7806..9c02e0e0 100644 --- a/src/renderer/navigation.ts +++ b/src/renderer/navigation.ts @@ -60,55 +60,26 @@ export const SECTION_NAV_IDS = Object.keys(SECTION_NAV_ITEM_DEFS) as SectionId[] export const SIDEBAR_NAV_GROUP_DEFS = [ { - id: "company", + id: "primary", items: [ SECTION_NAV_ITEM_DEFS.overview, SECTION_NAV_ITEM_DEFS.inbox, - SECTION_NAV_ITEM_DEFS.standup, - SECTION_NAV_ITEM_DEFS.performance, - ], - }, - { - id: "organization", - items: [ - SECTION_NAV_ITEM_DEFS.agents, - SECTION_NAV_ITEM_DEFS.hiring, - SECTION_NAV_ITEM_DEFS.goals, - SECTION_NAV_ITEM_DEFS.projects, - ], - }, - { - id: "execution", - items: [ SECTION_NAV_ITEM_DEFS.tasks, SECTION_NAV_ITEM_DEFS.approvals, - SECTION_NAV_ITEM_DEFS.runs, - SECTION_NAV_ITEM_DEFS.costs, - SECTION_NAV_ITEM_DEFS.activity, ], }, { - id: "operations", - items: [ - SECTION_NAV_ITEM_DEFS.workspaces, - SECTION_NAV_ITEM_DEFS.documents, - SECTION_NAV_ITEM_DEFS.knowledge, - SECTION_NAV_ITEM_DEFS.automation, - SECTION_NAV_ITEM_DEFS.connectors, - ], - }, - { - id: "collaboration", + id: "company", items: [ + SECTION_NAV_ITEM_DEFS.agents, SECTION_NAV_ITEM_DEFS.communication, - SECTION_NAV_ITEM_DEFS.meetings, - SECTION_NAV_ITEM_DEFS.sprints, - SECTION_NAV_ITEM_DEFS.social, - SECTION_NAV_ITEM_DEFS.skills, + SECTION_NAV_ITEM_DEFS.documents, ], }, ] as const satisfies readonly NavGroupDef[]; +export const COMMAND_PALETTE_NAV_ITEM_DEFS = SECTION_NAV_IDS.map((id) => SECTION_NAV_ITEM_DEFS[id]); + export const SETTINGS_NAV_ITEM_DEF = SECTION_NAV_ITEM_DEFS.settings; export const DASHBOARD_NAV_TARGETS = [ diff --git a/src/renderer/views/SectionContentView.tsx b/src/renderer/views/SectionContentView.tsx index 8c48a229..4f4a31cb 100644 --- a/src/renderer/views/SectionContentView.tsx +++ b/src/renderer/views/SectionContentView.tsx @@ -38,7 +38,7 @@ export function SectionContentView({ section, error, pageTransition }: SectionCo ) : null}
- + { + it("resets a captured page error when the section changes", () => { + const error = new Error("boom"); + const failedState = { + hasError: true, + error, + resetKey: "runs", + }; + + expect(ErrorBoundary.getDerivedStateFromProps({ children: null, resetKey: "tasks" }, failedState)).toEqual({ + hasError: false, + error: null, + resetKey: "tasks", + }); + }); +}); diff --git a/tests/unit/navigation-model.test.ts b/tests/unit/navigation-model.test.ts index 34587a81..1ce575ce 100644 --- a/tests/unit/navigation-model.test.ts +++ b/tests/unit/navigation-model.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import type { SectionId } from "@shared/types"; import { + COMMAND_PALETTE_NAV_ITEM_DEFS, DASHBOARD_NAV_TARGETS, SECTION_NAV_IDS, SETTINGS_NAV_ITEM_DEF, @@ -12,18 +13,34 @@ function sidebarSectionIds(): SectionId[] { } describe("renderer navigation model", () => { - it("gives every section a stable visible entry", () => { - const visibleIds = [...sidebarSectionIds(), SETTINGS_NAV_ITEM_DEF.id]; - - expect(new Set(visibleIds).size).toBe(SECTION_NAV_IDS.length); - expect(visibleIds).toEqual(expect.arrayContaining(SECTION_NAV_IDS)); + it("keeps the sidebar focused on the primary user journey", () => { + expect(sidebarSectionIds()).toEqual([ + "overview", + "inbox", + "tasks", + "approvals", + "agents", + "communication", + "documents", + ]); }); - it("keeps Dashboard targets reachable from the stable navigation model", () => { - const visibleIds = new Set([...sidebarSectionIds(), SETTINGS_NAV_ITEM_DEF.id]); + it("keeps every section reachable from command navigation", () => { + const commandIds = COMMAND_PALETTE_NAV_ITEM_DEFS.map((item) => item.id); + + expect(new Set(commandIds).size).toBe(SECTION_NAV_IDS.length); + expect(commandIds).toEqual(expect.arrayContaining(SECTION_NAV_IDS)); + }); + + it("keeps Dashboard targets reachable from command navigation", () => { + const commandIds = new Set(COMMAND_PALETTE_NAV_ITEM_DEFS.map((item) => item.id)); for (const sectionId of DASHBOARD_NAV_TARGETS) { - expect(visibleIds.has(sectionId)).toBe(true); + expect(commandIds.has(sectionId)).toBe(true); } }); + + it("keeps settings outside the scrollable sidebar groups", () => { + expect(sidebarSectionIds()).not.toContain(SETTINGS_NAV_ITEM_DEF.id); + }); });