feat(FN-2296): move project management into selector dropdown

- Move the project management entry into the desktop project selector dropdown UI
- Simplify Header rendering and remove redundant standalone project management navigation controls
- Update Header and multi-project flow tests to validate the new dropdown interaction pattern
- Refresh header styles to support the revised selector layout and menu behavior
This commit is contained in:
Fusion
2026-04-23 11:16:11 -07:00
committed by gsxdsm
parent 807c1f6137
commit e77dfffc9b
5 changed files with 93 additions and 138 deletions

View File

@@ -1011,73 +1011,49 @@ describe("Header", () => {
});
});
describe("Back to All Projects button", () => {
describe("Manage Projects action", () => {
const singleProject = [
{ id: "1", name: "Test Project", path: "/path/to/project", status: "active" as const },
];
it("renders Back to All Projects button when currentProject is set", () => {
it("renders project selector trigger on desktop with a single project", () => {
renderHeader({
projects: singleProject,
currentProject: singleProject[0],
onViewAllProjects: noop,
}, "desktop");
expect(screen.getByTestId("back-to-projects-btn")).toBeDefined();
expect(screen.getByTestId("project-selector-trigger")).toBeDefined();
});
it("calls onViewAllProjects when Back button is clicked", () => {
it("shows Manage Projects action in dropdown and calls onViewAllProjects", () => {
const onViewAllProjects = vi.fn();
renderHeader({
projects: singleProject,
currentProject: singleProject[0],
onViewAllProjects,
}, "desktop");
fireEvent.click(screen.getByTestId("back-to-projects-btn"));
fireEvent.click(screen.getByTestId("project-selector-trigger"));
fireEvent.click(screen.getByTestId("manage-projects-action"));
expect(onViewAllProjects).toHaveBeenCalled();
expect(screen.queryByTestId("project-selector-dropdown")).toBeNull();
});
it("does not render Back button when no currentProject", () => {
renderHeader({
projects: singleProject,
currentProject: null,
onViewAllProjects: noop,
}, "desktop");
expect(screen.queryByTestId("back-to-projects-btn")).toBeNull();
});
it("does not render Back button on mobile", () => {
renderHeader({
projects: singleProject,
currentProject: singleProject[0],
onViewAllProjects: noop,
}, "mobile");
expect(screen.queryByTestId("back-to-projects-btn")).toBeNull();
});
it("does not render Back button on tablet", () => {
renderHeader({
projects: singleProject,
currentProject: singleProject[0],
onViewAllProjects: noop,
}, "tablet");
expect(screen.queryByTestId("back-to-projects-btn")).toBeNull();
});
it("does not render Back button when onViewAllProjects is not provided", () => {
renderHeader({
projects: singleProject,
currentProject: singleProject[0],
}, "desktop");
expect(screen.queryByTestId("back-to-projects-btn")).toBeNull();
});
it("renders Back button with correct title", () => {
it("does not render separate back button on desktop", () => {
renderHeader({
projects: singleProject,
currentProject: singleProject[0],
onViewAllProjects: noop,
}, "desktop");
expect(screen.getByTitle("Back to All Projects")).toBeDefined();
expect(screen.queryByTestId("back-to-projects-btn")).toBeNull();
});
it("does not render project selector when onViewAllProjects is not provided", () => {
renderHeader({
projects: singleProject,
currentProject: singleProject[0],
}, "desktop");
expect(screen.queryByTestId("project-selector-trigger")).toBeNull();
});
});

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, Monitor, Server, Workflow, Bot, ChevronLeft, Target, ChevronRight, FileCode, Loader2, Grid3X3, Mail, MessageSquare, ChevronDown, Check, Map, Zap, Sparkles, FileText, Brain } from "lucide-react";
import { Settings, Pause, Play, Square, LayoutGrid, List, Terminal, Lightbulb, Search, X, Activity, MoreHorizontal, Clock, Folder, History, GitBranch, Monitor, Server, Workflow, Bot, Target, ChevronRight, FileCode, Loader2, Grid3X3, Mail, MessageSquare, ChevronDown, Check, Map, Zap, Sparkles, FileText, Brain } from "lucide-react";
import type { ProjectInfo } from "../api";
import type { NodeConfig, ProjectStatus } from "@fusion/core";
import { fetchScripts } from "../api";
@@ -20,8 +20,7 @@ const PROJECT_STATUS_CONFIG: Record<ProjectStatus, { color: string }> = {
/**
* ProjectSelector - A component for project navigation.
* Shows "Back to All Projects" button with ChevronLeft icon when currentProject is set.
* Shows project dropdown for switching when 2+ projects exist.
* Shows project dropdown for switching projects and navigating to project management.
*/
function ProjectSelector({
projects,
@@ -71,21 +70,7 @@ function ProjectSelector({
return (
<div className="project-selector" ref={dropdownRef}>
{/* Back to All Projects button - shown when a project is selected */}
{currentProject && (
<button
className="header-back-button"
onClick={onViewAll}
title="Back to All Projects"
data-testid="back-to-projects-btn"
>
<ChevronLeft size={14} />
<span>Back to All Projects</span>
</button>
)}
{/* Project dropdown - shown when 2+ projects exist */}
{projects.length > 1 && (
{projects.length > 0 && (
<>
<button
className={`project-selector-trigger${isOpen ? " project-selector-trigger--open" : ""}`}
@@ -130,6 +115,17 @@ function ProjectSelector({
</button>
);
})}
<div className="project-selector-divider" role="presentation" />
<button
className="project-selector-manage"
onClick={() => {
onViewAll();
setIsOpen(false);
}}
data-testid="manage-projects-action"
>
Manage Projects
</button>
</div>
)}
</>

View File

@@ -1497,19 +1497,11 @@ describe("Header", () => {
// ── Project Selector ────────────────────────────────────
it("shows back to projects button when currentProject is set", () => {
it("does not render back to projects button when currentProject is set", () => {
const projects = [
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
];
render(<Header projects={projects} currentProject={projects[0]} onViewAllProjects={vi.fn()} />);
expect(screen.getByTestId("back-to-projects-btn")).toBeDefined();
});
it("does not show back button when no currentProject", () => {
const projects = [
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
];
render(<Header projects={projects} currentProject={null} onViewAllProjects={vi.fn()} />);
expect(screen.queryByTestId("back-to-projects-btn")).toBeNull();
});
@@ -1526,12 +1518,11 @@ describe("Header", () => {
onViewAllProjects={vi.fn()}
/>
);
// ProjectSelector should be inside header-left
const headerLeft = container.querySelector(".header-left");
expect(headerLeft).not.toBeNull();
const projectSelector = headerLeft!.querySelector(".project-selector");
expect(projectSelector).not.toBeNull();
expect(projectSelector!.querySelector("[data-testid='back-to-projects-btn']")).not.toBeNull();
expect(projectSelector!.querySelector("[data-testid='project-selector-trigger']")).not.toBeNull();
});
it("does not show project selector when no projects", () => {
@@ -1547,7 +1538,15 @@ describe("Header", () => {
expect(container.querySelector(".project-selector")).toBeNull();
});
it("shows project dropdown arrow only when 2+ projects", () => {
it("shows project dropdown trigger 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: "" },
];
render(<Header projects={projects} currentProject={projects[0]} onViewAllProjects={vi.fn()} />);
expect(screen.getByTestId("project-selector-trigger")).toBeDefined();
});
it("shows project dropdown trigger when multiple projects exist", () => {
const projects = [
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
{ id: "proj_2", name: "Project Two", path: "/path/2", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
@@ -1556,31 +1555,6 @@ describe("Header", () => {
expect(screen.getByTestId("project-selector-trigger")).toBeDefined();
});
it("does not show project dropdown arrow 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: "" },
];
render(<Header projects={projects} currentProject={projects[0]} onViewAllProjects={vi.fn()} />);
expect(screen.queryByTestId("project-selector-trigger")).toBeNull();
});
it("back button calls onViewAllProjects when clicked", () => {
const projects = [
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
{ id: "proj_2", name: "Project Two", path: "/path/2", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
];
const onViewAllProjects = vi.fn();
render(
<Header
projects={projects}
currentProject={projects[0]}
onViewAllProjects={onViewAllProjects}
/>
);
fireEvent.click(screen.getByTestId("back-to-projects-btn"));
expect(onViewAllProjects).toHaveBeenCalled();
});
it("calls onSelectProject when project selected from dropdown", () => {
const projects = [
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
@@ -1596,30 +1570,33 @@ describe("Header", () => {
/>
);
// Open dropdown
fireEvent.click(screen.getByTestId("project-selector-trigger"));
// Click on a project in the dropdown
fireEvent.click(screen.getByText("Project Two"));
expect(onSelectProject).toHaveBeenCalledWith(projects[1]);
});
it("shows 'Back to All Projects' text in back button", () => {
it("shows Manage Projects action and calls onViewAllProjects when clicked", () => {
const projects = [
{ id: "proj_1", name: "Project One", path: "/path/1", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
{ id: "proj_2", name: "Project Two", path: "/path/2", status: "active" as const, isolationMode: "in-process" as const, createdAt: "", updatedAt: "" },
];
const onViewAllProjects = vi.fn();
render(
<Header
projects={projects}
currentProject={projects[0]}
onSelectProject={vi.fn()}
onViewAllProjects={vi.fn()}
onViewAllProjects={onViewAllProjects}
/>
);
// Back button should show "Back to All Projects"
const backBtn = screen.getByTestId("back-to-projects-btn");
expect(backBtn.textContent).toContain("Back to All Projects");
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const manageProjectsAction = screen.getByTestId("manage-projects-action");
expect(manageProjectsAction.textContent).toContain("Manage Projects");
fireEvent.click(manageProjectsAction);
expect(onViewAllProjects).toHaveBeenCalledOnce();
expect(screen.queryByTestId("project-selector-dropdown")).toBeNull();
});
// ── Modal Overlay Visibility ──────────────────────────────────

View File

@@ -111,7 +111,7 @@ describe("MultiProjectFlow", () => {
expect(storage[taskViewKey]).toBe("board");
});
describe("Back to All Projects button navigation", () => {
describe("Manage Projects dropdown navigation", () => {
const singleProject: ProjectInfo = {
id: "proj_1",
name: "Solo Project",
@@ -122,7 +122,7 @@ describe("MultiProjectFlow", () => {
updatedAt: "2026-01-01T00:00:00.000Z",
};
it("shows Back to All Projects button and navigates to overview on click", () => {
it("navigates to overview from Manage Projects action", () => {
mockDesktopMatchMedia();
let viewMode: "overview" | "project" = "project";
@@ -144,17 +144,17 @@ describe("MultiProjectFlow", () => {
/>
);
// The Back to All Projects button should be visible when currentProject is set
const backBtn = screen.getByTestId("back-to-projects-btn");
expect(backBtn).toBeDefined();
fireEvent.click(screen.getByTestId("project-selector-trigger"));
const manageProjectsAction = screen.getByTestId("manage-projects-action");
expect(manageProjectsAction.textContent).toContain("Manage Projects");
// Clicking should trigger navigation to overview
fireEvent.click(backBtn);
fireEvent.click(manageProjectsAction);
expect(handleViewAllProjects).toHaveBeenCalled();
expect(viewMode).toBe("overview");
expect(screen.queryByTestId("project-selector-dropdown")).toBeNull();
});
it("shows Back to All Projects button text when currentProject is set", () => {
it("does not render a separate back to projects header button", () => {
mockDesktopMatchMedia();
render(
@@ -171,10 +171,7 @@ describe("MultiProjectFlow", () => {
/>
);
// The back button should show "Back to All Projects"
const backBtn = screen.getByTestId("back-to-projects-btn");
expect(backBtn).toBeDefined();
expect(backBtn.textContent).toContain("Back to All Projects");
expect(screen.queryByTestId("back-to-projects-btn")).toBeNull();
});
});
});