feat(KB-069): add Archive All Done feature

- Add archiveAllDone method to TaskStore with filtering and batch archive
- Add POST /tasks/archive-all-done API endpoint with tests
- Add useTasks hook support and API function for archiveAllDone
- Add Archive All button to done column header in Board UI
- Wire up onArchiveAllDone through Board component hierarchy
This commit is contained in:
gsxdsm
2026-03-30 16:50:34 -07:00
parent 107fbe17dc
commit 304506b237
11 changed files with 293 additions and 5 deletions

View File

@@ -30,10 +30,14 @@ vi.mock("../../api", () => ({
mergeTask: vi.fn(),
retryTask: vi.fn(),
updateTask: vi.fn(),
archiveTask: vi.fn(),
unarchiveTask: vi.fn(),
archiveAllDone: vi.fn(),
}));
const mockFetchTasks = vi.mocked(api.fetchTasks);
const mockUpdateTask = vi.mocked(api.updateTask);
const mockArchiveAllDone = vi.mocked(api.archiveAllDone);
// Mock EventSource
class MockEventSource {
@@ -643,4 +647,57 @@ describe("useTasks", () => {
expect(result.current.tasks[0].dependencies).toEqual(["KB-002", "KB-003"]);
});
});
describe("archiveAllDone", () => {
it("archives all done tasks and updates local state", async () => {
const doneTasks = [
createMockTask({ id: "KB-001", column: "done" as Column }),
createMockTask({ id: "KB-002", column: "done" as Column }),
];
const todoTask = createMockTask({ id: "KB-003", column: "todo" as Column });
mockFetchTasks.mockResolvedValueOnce([...doneTasks, todoTask]);
const archivedTasks = [
createMockTask({ id: "KB-001", column: "archived" as Column }),
createMockTask({ id: "KB-002", column: "archived" as Column }),
];
mockArchiveAllDone.mockResolvedValueOnce(archivedTasks);
const { result } = renderHook(() => useTasks());
await waitFor(() => {
expect(result.current.tasks).toHaveLength(3);
});
await act(async () => {
await result.current.archiveAllDone();
});
expect(mockArchiveAllDone).toHaveBeenCalled();
// Done tasks should be archived
expect(result.current.tasks.find((t) => t.id === "KB-001")?.column).toBe("archived");
expect(result.current.tasks.find((t) => t.id === "KB-002")?.column).toBe("archived");
// Todo task should remain unchanged
expect(result.current.tasks.find((t) => t.id === "KB-003")?.column).toBe("todo");
});
it("returns empty array when no done tasks exist", async () => {
const todoTask = createMockTask({ id: "KB-001", column: "todo" as Column });
mockFetchTasks.mockResolvedValueOnce([todoTask]);
mockArchiveAllDone.mockResolvedValueOnce([]);
const { result } = renderHook(() => useTasks());
await waitFor(() => {
expect(result.current.tasks).toHaveLength(1);
});
const archived = await act(async () => {
return await result.current.archiveAllDone();
});
expect(archived).toEqual([]);
expect(result.current.tasks[0].column).toBe("todo");
});
});
});

View File

@@ -214,5 +214,18 @@ export function useTasks() {
return task;
}, []);
return { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, duplicateTask, updateTask, archiveTask, unarchiveTask };
const archiveAllDone = useCallback(async (): Promise<Task[]> => {
const archived = await api.archiveAllDone();
const normalized = archived.map(normalizeTask);
// Update local state by mapping over tasks and updating archived ones
setTasks((prev) =>
prev.map((t) => {
const updated = normalized.find((archived) => archived.id === t.id);
return updated || t;
})
);
return normalized;
}, []);
return { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, duplicateTask, updateTask, archiveTask, unarchiveTask, archiveAllDone };
}