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 c71347930f
commit 360f3e03b3
11 changed files with 293 additions and 5 deletions

View File

@@ -2113,6 +2113,96 @@ describe("TaskStore", () => {
});
});
describe("archiveAllDone", () => {
it("archives multiple done tasks", async () => {
const task1 = await store.createTask({ description: "Test task 1" });
const task2 = await store.createTask({ description: "Test task 2" });
const task3 = await store.createTask({ description: "Test task 3" });
// Move all to done
for (const task of [task1, task2, task3]) {
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
await store.moveTask(task.id, "in-review");
await store.moveTask(task.id, "done");
}
const archived = await store.archiveAllDone();
expect(archived).toHaveLength(3);
expect(archived.every((t) => t.column === "archived")).toBe(true);
});
it("returns empty array when no done tasks exist", async () => {
const result = await store.archiveAllDone();
expect(result).toEqual([]);
});
it("emits task:moved event for each archived task", async () => {
const task1 = await store.createTask({ description: "Test task 1" });
const task2 = await store.createTask({ description: "Test task 2" });
for (const task of [task1, task2]) {
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
await store.moveTask(task.id, "in-review");
await store.moveTask(task.id, "done");
}
const events: any[] = [];
store.on("task:moved", (data) => events.push(data));
await store.archiveAllDone();
expect(events).toHaveLength(2);
expect(events.every((e) => e.from === "done" && e.to === "archived")).toBe(true);
});
it("does not affect tasks in other columns", async () => {
const doneTask = await store.createTask({ description: "Done task" });
await store.moveTask(doneTask.id, "todo");
await store.moveTask(doneTask.id, "in-progress");
await store.moveTask(doneTask.id, "in-review");
await store.moveTask(doneTask.id, "done");
const todoTask = await store.createTask({ description: "Todo task" });
await store.moveTask(todoTask.id, "todo");
const inProgressTask = await store.createTask({ description: "In progress task" });
await store.moveTask(inProgressTask.id, "todo");
await store.moveTask(inProgressTask.id, "in-progress");
await store.archiveAllDone();
const fetchedTodo = await store.getTask(todoTask.id);
const fetchedInProgress = await store.getTask(inProgressTask.id);
expect(fetchedTodo.column).toBe("todo");
expect(fetchedInProgress.column).toBe("in-progress");
});
it("archives only done tasks when mixed columns exist", async () => {
const doneTask1 = await store.createTask({ description: "Done task 1" });
const doneTask2 = await store.createTask({ description: "Done task 2" });
const todoTask = await store.createTask({ description: "Todo task" });
for (const task of [doneTask1, doneTask2]) {
await store.moveTask(task.id, "todo");
await store.moveTask(task.id, "in-progress");
await store.moveTask(task.id, "in-review");
await store.moveTask(task.id, "done");
}
await store.moveTask(todoTask.id, "todo");
const archived = await store.archiveAllDone();
expect(archived).toHaveLength(2);
expect(archived.map((t) => t.id).sort()).toEqual([doneTask1.id, doneTask2.id].sort());
});
});
describe("VALID_TRANSITIONS — invalid archived transitions via moveTask", () => {
it("moveTask from archived → in-progress should fail", async () => {
const task = await store.createTask({ description: "Test task" });