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"));
|
||||
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 ──────────────────────
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user