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:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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 ──────────────────────────────────
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user