FN-7649: resolve settings landing view to board on project switch

Fixes project-switch hydration so a persisted "settings" view resolves to the Board instead of re-opening Settings.

- Extend resolveLandingTaskView() in useViewState.ts to treat "settings" the same as "command-center", resolving both to "board" for the auto-restored/hydrated landing view only
- Add regression tests covering the settings->board landing resolution in useViewState.test.ts
- Add changeset documenting the patch-level fix

Files changed:
 .changeset/fn-7649-project-switch-board-landing.md |  7 ++
 .../app/hooks/__tests__/useViewState.test.ts       | 74 ++++++++++++++++++++++
 packages/dashboard/app/hooks/useViewState.ts       |  5 +-
 3 files changed, 85 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-7649

Fusion-Task-Lineage: 7179efd6-bb1b-4ea4-a062-479b9b1fffa3

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-07 20:18:59 -07:00
parent 60bfb66c8c
commit ce4f173d8f
3 changed files with 85 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Switching projects now lands on the Board instead of Settings when the last-visited view was Settings.
category: fix
dev: Extended resolveLandingTaskView in useViewState.ts to resolve "settings" (in addition to "command-center") to "board" for the auto-restored/hydrated landing view only; deep links (?view=settings) and explicit navigation still open Settings.

View File

@@ -77,6 +77,55 @@ describe("useViewState", () => {
});
});
// FNXC:ViewState FN-7649: Persisted Settings must not be the auto-restored landing view either; it lands on the Board instead.
it("lands on board when the persisted taskView is settings", async () => {
localStorage.setItem("kb-dashboard-task-view", "settings");
const { result } = renderHook(() => useViewState(createOptions()));
await waitFor(() => {
expect(result.current.taskView).toBe("board");
});
});
it("?view=settings deep link still opens Settings (not routed through the landing guard)", async () => {
const originalSearch = window.location.search;
window.history.replaceState({}, "", "?view=settings");
try {
const { result } = renderHook(() => useViewState(createOptions()));
await waitFor(() => {
expect(result.current.taskView).toBe("settings");
});
} finally {
window.history.replaceState({}, "", originalSearch ? `?${originalSearch.replace(/^\?/, "")}` : "/");
}
});
it("explicit setTaskView/handleChangeTaskView to settings still opens Settings", async () => {
const { result } = renderHook(() => useViewState(createOptions()));
await waitFor(() => {
expect(result.current.taskView).toBe("board");
});
await act(async () => {
result.current.setTaskView("settings");
});
expect(result.current.taskView).toBe("settings");
await act(async () => {
result.current.handleChangeTaskView("board");
});
await act(async () => {
result.current.handleChangeTaskView("settings");
});
expect(result.current.taskView).toBe("settings");
});
it("migrates legacy reliability taskView from localStorage to Command Center", async () => {
localStorage.setItem("kb-dashboard-task-view", "reliability");
@@ -578,6 +627,31 @@ describe("useViewState", () => {
});
});
// FNXC:ViewState FN-7649: Switching projects (rerender with a new currentProject) must not land on Settings when the newly selected project's scoped persisted view is settings; it resolves to board. Mirrors the existing command-center project-switch coverage above.
it("lands on board when switching to a project whose persisted scoped taskView is settings", async () => {
const projectA: ProjectInfo = { ...PROJECT, id: "proj_a", name: "Project A" };
const projectB: ProjectInfo = { ...PROJECT, id: "proj_b", name: "Project B" };
localStorage.setItem("kb:proj_a:kb-dashboard-task-view", "list");
localStorage.setItem("kb:proj_b:kb-dashboard-task-view", "settings");
const { result, rerender } = renderHook(
({ project }) => useViewState(createOptions({ currentProject: project })),
{ initialProps: { project: projectA } },
);
await waitFor(() => {
expect(result.current.taskView).toBe("list");
});
// Switch to project B, whose persisted view is settings - must land on board, not settings.
rerender({ project: projectB });
await waitFor(() => {
expect(result.current.taskView).toBe("board");
});
});
it("no cross-project bleed when switching projects", async () => {
const projectA: ProjectInfo = { ...PROJECT, id: "proj_a", name: "Project A" };
const projectB: ProjectInfo = { ...PROJECT, id: "proj_b", name: "Project B" };

View File

@@ -76,9 +76,12 @@ function normalizeTaskView(value: TaskView): TaskView {
/*
FNXC:ViewState 2026-06-22-15:30:
Fusion must land on the Board on load, never the Command Center "Dashboard" view. A persisted/normalized `command-center` value resolves to `board` for the auto-restored landing view only (initializer + project-hydration effect). Deep links (`?view=command-center`) and explicit user navigation still reach the Command Center — this only governs the restored landing surface.
FNXC:ViewState 2026-07-07-00:00:
FN-7649: an auto-restored/hydrated landing surface must never be Settings either. Once a project's per-project persisted `kb-dashboard-task-view` becomes `settings` (a common state after configuring a project through Settings), switching projects re-hydrates that scoped value and — without this guard — restores straight to Settings instead of the Board. `settings`, like `command-center`, now resolves to `board` for the auto-restored landing view only (initializer + project-hydration effect). Deep links (`?view=settings`) and explicit header/sidebar navigation to Settings still work because the `?view=` URL effect and `setTaskView`/`handleChangeTaskView` paths use `normalizeTaskView`, not this guard.
*/
function resolveLandingTaskView(value: TaskView): TaskView {
return value === "command-center" ? "board" : value;
return value === "command-center" || value === "settings" ? "board" : value;
}
function migrateLegacyRoadmapsView(value: string): TaskView {