feat(KB-659): add agents view toggle and merge dashboard updates
- Add the agents view toggle to the dashboard header and App view state - Preserve multi-project dashboard behavior while resolving App merge conflicts - Merge Header and App tests for agents view, search visibility, and deep-link handling - Keep scripts, changed-files modal, and project overview integrations working together
This commit is contained in:
@@ -59,11 +59,11 @@ function AppInner() {
|
||||
const [autoMerge, setAutoMerge] = useState(true);
|
||||
const [globalPaused, setGlobalPaused] = useState(false);
|
||||
const [enginePaused, setEnginePaused] = useState(false);
|
||||
|
||||
|
||||
// Multi-project state
|
||||
const { projects, loading: projectsLoading, register, update: updateProject, unregister: unregisterProject } = useProjects();
|
||||
const { projects, loading: projectsLoading, update: updateProject, unregister: unregisterProject } = useProjects();
|
||||
const { currentProject, setCurrentProject, clearCurrentProject, loading: currentProjectLoading } = useCurrentProject(projects);
|
||||
|
||||
|
||||
// View state: "overview" for all projects, "project" for single project task view
|
||||
const [viewMode, setViewMode] = useState<"overview" | "project">(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -74,7 +74,7 @@ function AppInner() {
|
||||
}
|
||||
return "overview";
|
||||
});
|
||||
|
||||
|
||||
// Task view state (only meaningful when viewMode="project")
|
||||
const [taskView, setTaskView] = useState<"board" | "list" | "agents">(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -85,14 +85,14 @@ function AppInner() {
|
||||
}
|
||||
return "board";
|
||||
});
|
||||
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [githubTokenConfigured, setGithubTokenConfigured] = useState(false);
|
||||
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
||||
|
||||
|
||||
// Setup wizard state
|
||||
const [setupWizardOpen, setSetupWizardOpen] = useState(false);
|
||||
|
||||
|
||||
// Tasks hook with project context
|
||||
const { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, updateTask, duplicateTask, archiveTask, unarchiveTask, archiveAllDone } = useTasks(
|
||||
currentProject ? { projectId: currentProject.id } : undefined
|
||||
@@ -105,14 +105,14 @@ function AppInner() {
|
||||
useEffect(() => {
|
||||
// Wait for both loading states to complete before making decision
|
||||
if (projectsLoading || currentProjectLoading) return;
|
||||
|
||||
|
||||
// Don't open if wizard is already open
|
||||
if (setupWizardOpen) return;
|
||||
|
||||
|
||||
// Don't open if we have projects OR a saved current project
|
||||
// (currentProject from localStorage means user was previously viewing a project)
|
||||
if (projects.length > 0 || currentProject) return;
|
||||
|
||||
|
||||
// Only open when truly no projects exist and no project is being restored
|
||||
const timer = setTimeout(() => {
|
||||
setSetupWizardOpen(true);
|
||||
@@ -131,7 +131,7 @@ function AppInner() {
|
||||
useEffect(() => {
|
||||
// Wait for both loading states to complete before syncing
|
||||
if (projectsLoading || currentProjectLoading) return;
|
||||
|
||||
|
||||
// If we have a restored current project but viewMode is overview, sync to project view
|
||||
if (currentProject && viewMode === "overview") {
|
||||
setViewMode("project");
|
||||
|
||||
@@ -35,6 +35,7 @@ vi.mock("../../api", async (importOriginal) => {
|
||||
logoutProvider: vi.fn(() => Promise.resolve({ success: true })),
|
||||
fetchModels: vi.fn(() => Promise.resolve([])),
|
||||
fetchGitRemotes: vi.fn(() => Promise.resolve([])),
|
||||
fetchAgents: vi.fn(() => Promise.resolve([])),
|
||||
fetchTaskDetail: vi.fn((id: string) => Promise.resolve({
|
||||
id,
|
||||
title: `Task ${id}`,
|
||||
@@ -512,15 +513,67 @@ describe("App view switching", () => {
|
||||
localStorage.removeItem("kb-dashboard-view-mode");
|
||||
});
|
||||
|
||||
it("shows view toggle buttons in header", async () => {
|
||||
it("shows view toggle buttons in header including agents", async () => {
|
||||
render(<App />);
|
||||
|
||||
// Wait for the header to render with view toggle
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTitle("Board view")).toBeTruthy();
|
||||
expect(screen.getByTitle("List view")).toBeTruthy();
|
||||
expect(screen.getByTitle("Agents view")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders AgentsView when agents view is selected", async () => {
|
||||
render(<App />);
|
||||
|
||||
// Wait for the header to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTitle("Agents view")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Click to switch to agents view
|
||||
fireEvent.click(screen.getByTitle("Agents view"));
|
||||
|
||||
// Agents view should be rendered (it has a agents-view container)
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(".agents-view")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Should NOT show board or list view
|
||||
expect(document.querySelector(".board")).toBeNull();
|
||||
expect(document.querySelector(".list-view")).toBeNull();
|
||||
});
|
||||
|
||||
it("persists agents view preference to localStorage", async () => {
|
||||
localStorage.removeItem("kb-dashboard-view");
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTitle("Agents view")).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTitle("Agents view"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(localStorage.getItem("kb-dashboard-view")).toBe("agents");
|
||||
});
|
||||
});
|
||||
|
||||
it("initializes agents view from localStorage if saved", async () => {
|
||||
localStorage.setItem("kb-dashboard-view", "agents");
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector(".agents-view")).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(screen.getByTitle("Agents view").className).toContain("active");
|
||||
|
||||
localStorage.removeItem("kb-dashboard-view");
|
||||
});
|
||||
});
|
||||
|
||||
describe("App GitHub import", () => {
|
||||
|
||||
@@ -251,6 +251,47 @@ describe("Header", () => {
|
||||
expect(boardBtn.getAttribute("aria-pressed")).toBe("false");
|
||||
});
|
||||
|
||||
// ── Search Visibility by View ─────────────────────────────────────
|
||||
|
||||
it("shows search input when view is 'board'", () => {
|
||||
const onSearchChange = vi.fn();
|
||||
render(
|
||||
<Header
|
||||
view="board"
|
||||
onChangeView={vi.fn()}
|
||||
searchQuery=""
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByPlaceholderText("Search tasks...")).toBeDefined();
|
||||
});
|
||||
|
||||
it("hides search input when view is 'list'", () => {
|
||||
const onSearchChange = vi.fn();
|
||||
render(
|
||||
<Header
|
||||
view="list"
|
||||
onChangeView={vi.fn()}
|
||||
searchQuery=""
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
|
||||
});
|
||||
|
||||
it("hides search input when view is 'agents'", () => {
|
||||
const onSearchChange = vi.fn();
|
||||
render(
|
||||
<Header
|
||||
view="agents"
|
||||
onChangeView={vi.fn()}
|
||||
searchQuery=""
|
||||
onSearchChange={onSearchChange}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
|
||||
});
|
||||
|
||||
// ── Terminal Button ─────────────────────────────────────────────
|
||||
|
||||
it("renders terminal button with correct title", () => {
|
||||
@@ -647,7 +688,7 @@ describe("Header", () => {
|
||||
onViewAllProjects={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
// Open selector
|
||||
fireEvent.click(screen.getByTestId("project-selector-trigger"));
|
||||
// Click on a project in the dropdown
|
||||
@@ -668,7 +709,7 @@ describe("Header", () => {
|
||||
onViewAllProjects={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
expect(screen.getByText("Project One")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user