diff --git a/packages/dashboard/app/components/Board.tsx b/packages/dashboard/app/components/Board.tsx index 4f0822c737..2f404eed9f 100644 --- a/packages/dashboard/app/components/Board.tsx +++ b/packages/dashboard/app/components/Board.tsx @@ -391,12 +391,16 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask const selectedWorkflowTasks = useMemo(() => { if (!workflowMode || !boardWorkflows || !selectedWorkflow) return []; return tasks.filter((task) => { - if (task.column === "archived") return false; const workflowId = boardWorkflows.taskWorkflowIds[task.id] ?? boardWorkflows.defaultWorkflowId; return workflowId === selectedWorkflow.id; }); }, [boardWorkflows, selectedWorkflow, tasks, workflowMode]); + const selectedWorkflowArchivedColumn = useMemo(() => { + if (!selectedWorkflow) return null; + return selectedWorkflow.columns.find((column) => column.flags.archived) ?? null; + }, [selectedWorkflow]); + const selectedWorkflowColumns = useMemo(() => { if (!selectedWorkflow) return []; return selectedWorkflow.columns.filter((column) => !column.flags.archived && !column.flags.hiddenFromBoard); @@ -584,9 +588,55 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onPauseTask autoMerge={autoMerge} {...(isCreateColumn ? { onQuickCreate, onNewTask, onPlanningMode, onSubtaskBreakdown } : {})} {...(columnDef.flags.mergeBlocker || columnDef.flags.humanReview ? { onToggleAutoMerge } : {})} + {...(columnDef.id === "done" ? { onArchiveAllDone } : {})} /> ); })} + {selectedWorkflowArchivedColumn && ( + canDropTask(taskId, selectedWorkflowArchivedColumn.id, selectedWorkflow.id)} + getDraggingTaskId={getDraggingTaskId} + onPauseTask={onPauseTask} + onOpenDetail={onOpenDetail} + onOpenGroupModal={onOpenGroupModal} + addToast={addToast} + globalPaused={globalPaused} + onUpdateTask={onUpdateTask} + onRetryTask={onRetryTask} + onArchiveTask={onArchiveTask} + onUnarchiveTask={onUnarchiveTask} + onDeleteTask={onDeleteTask} + availableModels={availableModels} + onOpenDetailWithTab={onOpenDetailWithTab} + favoriteProviders={favoriteProviders} + favoriteModels={favoriteModels} + onToggleFavorite={onToggleFavorite} + onToggleModelFavorite={onToggleModelFavorite} + isSearchActive={isSearchActive} + taskStuckTimeoutMs={taskStuckTimeoutMs} + onOpenMission={onOpenMission} + lastFetchTimeMs={lastFetchTimeMs} + workflowStepNameLookup={workflowStepNameLookup} + taskCardFieldDefs={taskCardFieldDefs} + blockerFanoutMap={blockerFanoutMap} + prAuthAvailable={prAuthAvailable} + autoMerge={autoMerge} + collapsed={archivedCollapsed} + onToggleCollapse={handleToggleArchivedCollapse} + /> + )} ); diff --git a/packages/dashboard/app/components/__tests__/Board.test.tsx b/packages/dashboard/app/components/__tests__/Board.test.tsx index 6e8e7deb43..e1a24c4d3c 100644 --- a/packages/dashboard/app/components/__tests__/Board.test.tsx +++ b/packages/dashboard/app/components/__tests__/Board.test.tsx @@ -48,10 +48,10 @@ const columnRenderCounts: Record = {}; // Mock child components so we only test Board's own rendering vi.mock("../Column", () => ({ - Column: React.memo(({ column, tasks, onToggleCollapse, onQuickCreate, onNewTask, onToggleAutoMerge, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, isSearchActive, workflowStepNameLookup }: { column: string; tasks: Task[]; onToggleCollapse?: () => void; onQuickCreate?: unknown; onNewTask?: unknown; onToggleAutoMerge?: () => void; favoriteProviders?: string[]; favoriteModels?: string[]; onToggleFavorite?: (provider: string) => void; onToggleModelFavorite?: (modelId: string) => void; isSearchActive?: boolean; workflowStepNameLookup?: ReadonlyMap }) => { + Column: React.memo(({ column, tasks, collapsed, onToggleCollapse, onQuickCreate, onNewTask, onToggleAutoMerge, onArchiveAllDone, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, isSearchActive, workflowStepNameLookup }: { column: string; tasks: Task[]; collapsed?: boolean; onToggleCollapse?: () => void; onQuickCreate?: unknown; onNewTask?: unknown; onToggleAutoMerge?: () => void; onArchiveAllDone?: unknown; favoriteProviders?: string[]; favoriteModels?: string[]; onToggleFavorite?: (provider: string) => void; onToggleModelFavorite?: (modelId: string) => void; isSearchActive?: boolean; workflowStepNameLookup?: ReadonlyMap }) => { columnRenderCounts[column] = (columnRenderCounts[column] ?? 0) + 1; return ( -
+
{onToggleCollapse && }
); @@ -1183,13 +1183,14 @@ describe("Board", () => { expect([...selector.options].map((option) => option.value)).toEqual(["builtin:coding", "wf-custom"]); }); - it("excludes archived cards from the selected workflow board", async () => { + it("renders archived cards in the selected workflow archived column", async () => { enableFlag({ "FN-1": "builtin:coding", "FN-9": "builtin:coding" }); renderBoard({ tasks: [mkTask({ id: "FN-1" }), mkTask({ id: "FN-9", column: "archived" })] }); - await waitFor(() => expect(screen.getByTestId("column-todo")).toBeDefined()); - const ids = JSON.parse(screen.getByTestId("column-todo").getAttribute("data-tasks") || "[]").map((task: Task) => task.id); - expect(ids).toEqual(["FN-1"]); - expect(screen.queryByTestId("column-archived")).toBeNull(); + await waitFor(() => expect(screen.getByTestId("column-archived")).toBeDefined()); + const todoIds = JSON.parse(screen.getByTestId("column-todo").getAttribute("data-tasks") || "[]").map((task: Task) => task.id); + expect(todoIds).toEqual(["FN-1"]); + const archivedIds = JSON.parse(screen.getByTestId("column-archived").getAttribute("data-tasks") || "[]").map((task: Task) => task.id); + expect(archivedIds).toEqual(["FN-9"]); }); it("renders selected workflow columns as direct children of the horizontal board", async () => { @@ -1202,8 +1203,41 @@ describe("Board", () => { "column-in-progress", "column-in-review", "column-done", + "column-archived", ]); }); + + it("archived column is collapsible in workflow mode", async () => { + enableFlag({ "FN-9": "builtin:coding" }); + renderBoard({ tasks: [mkTask({ id: "FN-9", column: "archived" })] }); + + const archivedColumn = await screen.findByTestId("column-archived"); + expect(archivedColumn.getAttribute("data-collapsed")).toBe("true"); + + fireEvent.click(screen.getByRole("button", { name: "toggle-archived" })); + expect(screen.getByTestId("column-archived").getAttribute("data-collapsed")).toBe("false"); + + fireEvent.click(screen.getByRole("button", { name: "toggle-archived" })); + expect(screen.getByTestId("column-archived").getAttribute("data-collapsed")).toBe("true"); + }); + + it("workflow without archived column does not render one", async () => { + enableFlag({ "FN-1": CUSTOM_WORKFLOW.id }, [CUSTOM_WORKFLOW]); + renderBoard({ tasks: [mkTask({ id: "FN-1", column: "intake" })] }); + + await waitFor(() => expect(screen.getByTestId("column-intake")).toBeDefined()); + expect(screen.queryByTestId("column-archived")).toBeNull(); + }); + + it("done column in workflow mode receives onArchiveAllDone prop", async () => { + const onArchiveAllDone = vi.fn(); + enableFlag({ "FN-1": "builtin:coding" }); + renderBoard({ tasks: [mkTask({ id: "FN-1", column: "done" })], onArchiveAllDone }); + + await waitFor(() => expect(screen.getByTestId("column-done")).toBeDefined()); + expect(screen.getByTestId("column-done").getAttribute("data-has-archive-all")).toBe("yes"); + expect(screen.getByTestId("column-todo").getAttribute("data-has-archive-all")).toBe("no"); + }); }); describe("workflow:updated SSE invalidation (#1406)", () => {