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:
@@ -364,7 +364,7 @@ describe("tablet header controls", () => {
|
|||||||
fireEvent.click(screen.getByTitle("More header actions"));
|
fireEvent.click(screen.getByTitle("More header actions"));
|
||||||
const btn = screen.getByTestId("overflow-project-selector-btn");
|
const btn = screen.getByTestId("overflow-project-selector-btn");
|
||||||
expect(btn).toBeDefined();
|
expect(btn).toBeDefined();
|
||||||
expect(btn.textContent).toContain("Project One");
|
expect(btn.textContent).toContain("Projects");
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Desktop still shows everything inline ──────────────────────
|
// ── Desktop still shows everything inline ──────────────────────
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
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 type { ProjectInfo } from "../api";
|
||||||
import { fetchScripts } from "../api";
|
import { fetchScripts } from "../api";
|
||||||
import { ProjectSelector } from "./ProjectSelector";
|
import { ProjectSelector } from "./ProjectSelector";
|
||||||
@@ -573,16 +573,16 @@ export function Header({
|
|||||||
role="menu"
|
role="menu"
|
||||||
aria-label="Additional header actions"
|
aria-label="Additional header actions"
|
||||||
>
|
>
|
||||||
{/* Switch Project - in overflow on mobile */}
|
{/* Projects - in overflow on mobile */}
|
||||||
{projects.length > 1 && onViewAllProjects && (
|
{projects.length >= 1 && onViewAllProjects && (
|
||||||
<button
|
<button
|
||||||
className="mobile-overflow-item"
|
className="mobile-overflow-item"
|
||||||
onClick={() => handleOverflowAction(onViewAllProjects)}
|
onClick={() => handleOverflowAction(onViewAllProjects)}
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
data-testid="overflow-project-selector-btn"
|
data-testid="overflow-project-selector-btn"
|
||||||
>
|
>
|
||||||
<Building2 size={16} />
|
<Grid3X3 size={16} />
|
||||||
<span>{currentProject ? currentProject.name : "Switch Project"}</span>
|
<span>Projects</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{/* Files - in overflow on mobile */}
|
{/* Files - in overflow on mobile */}
|
||||||
|
|||||||
@@ -625,8 +625,7 @@ describe("Header", () => {
|
|||||||
fireEvent.click(screen.getByTitle("More header actions"));
|
fireEvent.click(screen.getByTitle("More header actions"));
|
||||||
const btn = screen.getByTestId("overflow-project-selector-btn");
|
const btn = screen.getByTestId("overflow-project-selector-btn");
|
||||||
expect(btn).toBeDefined();
|
expect(btn).toBeDefined();
|
||||||
// Should show the current project name
|
expect(btn.textContent).toContain("Projects");
|
||||||
expect(btn.textContent).toContain("Project One");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("overflow project selector calls onViewAllProjects when clicked", () => {
|
it("overflow project selector calls onViewAllProjects when clicked", () => {
|
||||||
@@ -679,7 +678,7 @@ describe("Header", () => {
|
|||||||
expect(projectSvg!.innerHTML).not.toBe(filesSvg!.innerHTML);
|
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 = [
|
const projects = [
|
||||||
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
|
{ 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"));
|
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", () => {
|
it("missions overflow menu item calls onOpenMissions when clicked", () => {
|
||||||
|
|||||||
@@ -143,13 +143,13 @@ describe("useCurrentProject", () => {
|
|||||||
result.current.clearCurrentProject();
|
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(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.currentProject?.id).toBe("proj_1");
|
expect(result.current.currentProject).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
// After re-defaulting, localStorage should have the default project
|
// localStorage should be cleared
|
||||||
expect(localStorage.getItem("kb-dashboard-current-project")).toContain("proj_1");
|
expect(localStorage.getItem("kb-dashboard-current-project")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles localStorage errors gracefully", async () => {
|
it("handles localStorage errors gracefully", async () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import type { ProjectInfo } from "../api";
|
import type { ProjectInfo } from "../api";
|
||||||
|
|
||||||
const STORAGE_KEY = "kb-dashboard-current-project";
|
const STORAGE_KEY = "kb-dashboard-current-project";
|
||||||
@@ -8,7 +8,7 @@ export interface UseCurrentProjectResult {
|
|||||||
currentProject: ProjectInfo | null;
|
currentProject: ProjectInfo | null;
|
||||||
/** Set the current project */
|
/** Set the current project */
|
||||||
setCurrentProject: (project: ProjectInfo | null) => void;
|
setCurrentProject: (project: ProjectInfo | null) => void;
|
||||||
/** Clear the current project selection */
|
/** Clear the current project selection (suppresses auto-select) */
|
||||||
clearCurrentProject: () => void;
|
clearCurrentProject: () => void;
|
||||||
/** Whether we're still loading from localStorage */
|
/** Whether we're still loading from localStorage */
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -21,6 +21,9 @@ export interface UseCurrentProjectResult {
|
|||||||
export function useCurrentProject(availableProjects: ProjectInfo[]): UseCurrentProjectResult {
|
export function useCurrentProject(availableProjects: ProjectInfo[]): UseCurrentProjectResult {
|
||||||
const [currentProject, setCurrentProjectState] = useState<ProjectInfo | null>(null);
|
const [currentProject, setCurrentProjectState] = useState<ProjectInfo | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
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
|
// Load from localStorage on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -50,15 +53,16 @@ export function useCurrentProject(availableProjects: ProjectInfo[]): UseCurrentP
|
|||||||
setCurrentProjectState(firstActive || availableProjects[0] || null);
|
setCurrentProjectState(firstActive || availableProjects[0] || null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist to localStorage
|
// Persist to localStorage
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(currentProject));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(currentProject));
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore localStorage errors
|
// Ignore localStorage errors
|
||||||
}
|
}
|
||||||
} else if (availableProjects.length > 0) {
|
} else if (availableProjects.length > 0 && !explicitlyClearedRef.current) {
|
||||||
// No selection but projects available - default to first active
|
// 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");
|
const firstActive = availableProjects.find((p) => p.status === "active");
|
||||||
if (firstActive) {
|
if (firstActive) {
|
||||||
setCurrentProjectState(firstActive);
|
setCurrentProjectState(firstActive);
|
||||||
@@ -67,6 +71,7 @@ export function useCurrentProject(availableProjects: ProjectInfo[]): UseCurrentP
|
|||||||
}, [currentProject, availableProjects, loading]);
|
}, [currentProject, availableProjects, loading]);
|
||||||
|
|
||||||
const setCurrentProject = useCallback((project: ProjectInfo | null) => {
|
const setCurrentProject = useCallback((project: ProjectInfo | null) => {
|
||||||
|
explicitlyClearedRef.current = false;
|
||||||
setCurrentProjectState(project);
|
setCurrentProjectState(project);
|
||||||
if (project) {
|
if (project) {
|
||||||
try {
|
try {
|
||||||
@@ -84,6 +89,7 @@ export function useCurrentProject(availableProjects: ProjectInfo[]): UseCurrentP
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const clearCurrentProject = useCallback(() => {
|
const clearCurrentProject = useCallback(() => {
|
||||||
|
explicitlyClearedRef.current = true;
|
||||||
setCurrentProjectState(null);
|
setCurrentProjectState(null);
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
|||||||
Reference in New Issue
Block a user