FN-6060: add archive column to workflow mode board
Show archived workflow cards in a dedicated board column. - render the selected workflow's archived column instead of filtering archived tasks out of workflow mode - keep the workflow archived column collapsible and preserve archive-all behavior on the done column - extend Board workflow tests to cover archived-column rendering, collapse behavior, and workflows without an archived column Files changed: packages/dashboard/app/components/Board.tsx | 52 +++++++++++++++++++++- .../app/components/__tests__/Board.test.tsx | 48 +++++++++++++++++--- 2 files changed, 92 insertions(+), 8 deletions(-) Fusion-Task-Id: FN-6060 Fusion-Task-Lineage: 9788bac5-9ccc-427c-a0dd-3cc7f818487a
This commit is contained in:
@@ -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 && (
|
||||
<Column
|
||||
key={selectedWorkflowArchivedColumn.id}
|
||||
column={selectedWorkflowArchivedColumn.id as ColumnType}
|
||||
workflowMode
|
||||
workflowId={selectedWorkflow.id}
|
||||
columnDisplayName={selectedWorkflowArchivedColumn.name}
|
||||
columnFlags={selectedWorkflowArchivedColumn.flags}
|
||||
tasks={selectedWorkflowTasksByColumn[selectedWorkflowArchivedColumn.id] ?? []}
|
||||
allTasks={selectedWorkflowTasks}
|
||||
projectId={projectId}
|
||||
maxConcurrent={maxConcurrent}
|
||||
onMoveTask={onMoveTask}
|
||||
onPromote={handlePromote}
|
||||
canDropTask={(taskId) => 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}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -48,10 +48,10 @@ const columnRenderCounts: Record<string, number> = {};
|
||||
|
||||
// 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<string, string> }) => {
|
||||
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<string, string> }) => {
|
||||
columnRenderCounts[column] = (columnRenderCounts[column] ?? 0) + 1;
|
||||
return (
|
||||
<div data-testid={`column-${column}`} data-tasks={JSON.stringify(tasks)} data-has-quick-create={onQuickCreate ? "yes" : "no"} data-has-new-task={onNewTask ? "yes" : "no"} data-has-auto-merge-toggle={onToggleAutoMerge ? "yes" : "no"} data-favorite-providers={JSON.stringify(favoriteProviders ?? [])} data-favorite-models={JSON.stringify(favoriteModels ?? [])} data-has-toggle-favorite={onToggleFavorite ? "yes" : "no"} data-has-toggle-model-favorite={onToggleModelFavorite ? "yes" : "no"} data-is-search-active={isSearchActive ? "true" : "false"} data-workflow-lookup-size={String(workflowStepNameLookup?.size ?? 0)}>
|
||||
<div data-testid={`column-${column}`} data-tasks={JSON.stringify(tasks)} data-collapsed={collapsed ? "true" : "false"} data-has-quick-create={onQuickCreate ? "yes" : "no"} data-has-new-task={onNewTask ? "yes" : "no"} data-has-auto-merge-toggle={onToggleAutoMerge ? "yes" : "no"} data-has-archive-all={onArchiveAllDone ? "yes" : "no"} data-favorite-providers={JSON.stringify(favoriteProviders ?? [])} data-favorite-models={JSON.stringify(favoriteModels ?? [])} data-has-toggle-favorite={onToggleFavorite ? "yes" : "no"} data-has-toggle-model-favorite={onToggleModelFavorite ? "yes" : "no"} data-is-search-active={isSearchActive ? "true" : "false"} data-workflow-lookup-size={String(workflowStepNameLookup?.size ?? 0)}>
|
||||
{onToggleCollapse && <button onClick={onToggleCollapse}>toggle-{column}</button>}
|
||||
</div>
|
||||
);
|
||||
@@ -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)", () => {
|
||||
|
||||
Reference in New Issue
Block a user