fix(FN-949): prevent bounce-back on Projects nav and show in mobile overflow

clearCurrentProject was triggering auto-reselect in useCurrentProject,
making the Projects button ineffective for single-project users. Added
explicit-clear flag to suppress auto-select. Also changed mobile overflow
menu to show Projects for single-project users (was gated to 2+ projects).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-04 20:30:24 -07:00
parent 44447af7a9
commit 47c805cb18
5 changed files with 24 additions and 19 deletions

View File

@@ -364,7 +364,7 @@ describe("tablet header controls", () => {
fireEvent.click(screen.getByTitle("More header actions"));
const btn = screen.getByTestId("overflow-project-selector-btn");
expect(btn).toBeDefined();
expect(btn.textContent).toContain("Project One");
expect(btn.textContent).toContain("Projects");
});
// ── Desktop still shows everything inline ──────────────────────

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { Settings, Pause, Play, Square, LayoutGrid, List, Terminal, Lightbulb, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Workflow, Bot, ChevronLeft, Target, Building2, ChevronRight, FileCode, Loader2, Grid3X3 } from "lucide-react";
import { Settings, Pause, Play, Square, LayoutGrid, List, Terminal, Lightbulb, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Workflow, Bot, ChevronLeft, Target, ChevronRight, FileCode, Loader2, Grid3X3 } from "lucide-react";
import type { ProjectInfo } from "../api";
import { fetchScripts } from "../api";
import { ProjectSelector } from "./ProjectSelector";
@@ -573,16 +573,16 @@ export function Header({
role="menu"
aria-label="Additional header actions"
>
{/* Switch Project - in overflow on mobile */}
{projects.length > 1 && onViewAllProjects && (
{/* Projects - in overflow on mobile */}
{projects.length >= 1 && onViewAllProjects && (
<button
className="mobile-overflow-item"
onClick={() => handleOverflowAction(onViewAllProjects)}
role="menuitem"
data-testid="overflow-project-selector-btn"
>
<Building2 size={16} />
<span>{currentProject ? currentProject.name : "Switch Project"}</span>
<Grid3X3 size={16} />
<span>Projects</span>
</button>
)}
{/* Files - in overflow on mobile */}

View File

@@ -625,8 +625,7 @@ describe("Header", () => {
fireEvent.click(screen.getByTitle("More header actions"));
const btn = screen.getByTestId("overflow-project-selector-btn");
expect(btn).toBeDefined();
// Should show the current project name
expect(btn.textContent).toContain("Project One");
expect(btn.textContent).toContain("Projects");
});
it("overflow project selector calls onViewAllProjects when clicked", () => {
@@ -679,7 +678,7 @@ describe("Header", () => {
expect(projectSvg!.innerHTML).not.toBe(filesSvg!.innerHTML);
});
it("does not show switch project in overflow menu with single project", () => {
it("shows projects in overflow menu with single project", () => {
const projects = [
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
];
@@ -692,7 +691,7 @@ describe("Header", () => {
/>
);
fireEvent.click(screen.getByTitle("More header actions"));
expect(screen.queryByTestId("overflow-project-selector-btn")).toBeNull();
expect(screen.queryByTestId("overflow-project-selector-btn")).not.toBeNull();
});
it("missions overflow menu item calls onOpenMissions when clicked", () => {

View File

@@ -143,13 +143,13 @@ describe("useCurrentProject", () => {
result.current.clearCurrentProject();
});
// With available projects, it re-defaults to first active and saves to localStorage
// After explicit clear, should stay null (no auto-select) so user can view overview
await waitFor(() => {
expect(result.current.currentProject?.id).toBe("proj_1");
expect(result.current.currentProject).toBeNull();
});
// After re-defaulting, localStorage should have the default project
expect(localStorage.getItem("kb-dashboard-current-project")).toContain("proj_1");
// localStorage should be cleared
expect(localStorage.getItem("kb-dashboard-current-project")).toBeNull();
});
it("handles localStorage errors gracefully", async () => {

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useRef } from "react";
import type { ProjectInfo } from "../api";
const STORAGE_KEY = "kb-dashboard-current-project";
@@ -8,7 +8,7 @@ export interface UseCurrentProjectResult {
currentProject: ProjectInfo | null;
/** Set the current project */
setCurrentProject: (project: ProjectInfo | null) => void;
/** Clear the current project selection */
/** Clear the current project selection (suppresses auto-select) */
clearCurrentProject: () => void;
/** Whether we're still loading from localStorage */
loading: boolean;
@@ -21,6 +21,9 @@ export interface UseCurrentProjectResult {
export function useCurrentProject(availableProjects: ProjectInfo[]): UseCurrentProjectResult {
const [currentProject, setCurrentProjectState] = useState<ProjectInfo | null>(null);
const [loading, setLoading] = useState(true);
// When true, the user explicitly cleared the project (e.g. clicked "Projects")
// and we should not auto-select until they pick one manually.
const explicitlyClearedRef = useRef(false);
// Load from localStorage on mount
useEffect(() => {
@@ -50,15 +53,16 @@ export function useCurrentProject(availableProjects: ProjectInfo[]): UseCurrentP
setCurrentProjectState(firstActive || availableProjects[0] || null);
return;
}
// Persist to localStorage
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(currentProject));
} catch {
// Ignore localStorage errors
}
} else if (availableProjects.length > 0) {
} else if (availableProjects.length > 0 && !explicitlyClearedRef.current) {
// No selection but projects available - default to first active
// Skip if user explicitly cleared (navigated to overview)
const firstActive = availableProjects.find((p) => p.status === "active");
if (firstActive) {
setCurrentProjectState(firstActive);
@@ -67,6 +71,7 @@ export function useCurrentProject(availableProjects: ProjectInfo[]): UseCurrentP
}, [currentProject, availableProjects, loading]);
const setCurrentProject = useCallback((project: ProjectInfo | null) => {
explicitlyClearedRef.current = false;
setCurrentProjectState(project);
if (project) {
try {
@@ -84,6 +89,7 @@ export function useCurrentProject(availableProjects: ProjectInfo[]): UseCurrentP
}, []);
const clearCurrentProject = useCallback(() => {
explicitlyClearedRef.current = true;
setCurrentProjectState(null);
try {
localStorage.removeItem(STORAGE_KEY);