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

@@ -705,6 +705,63 @@ describe("POST /tasks/:id/unarchive", () => {
});
});
describe("POST /tasks/archive-all-done", () => {
let store: TaskStore;
beforeEach(() => {
store = createMockStore({
archiveAllDone: vi.fn(),
});
});
function buildApp() {
const app = express();
app.use(express.json());
app.use("/api", createApiRoutes(store));
return app;
}
it("archives all done tasks and returns the archived array", async () => {
const archivedTasks = [
{ ...FAKE_TASK_DETAIL, id: "KB-001", column: "archived" },
{ ...FAKE_TASK_DETAIL, id: "KB-002", column: "archived" },
];
(store.archiveAllDone as ReturnType<typeof vi.fn>).mockResolvedValue(archivedTasks);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/archive-all-done", JSON.stringify({}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(res.body.archived).toHaveLength(2);
expect(res.body.archived[0].column).toBe("archived");
expect(res.body.archived[1].column).toBe("archived");
expect(store.archiveAllDone).toHaveBeenCalled();
});
it("returns empty array when no done tasks exist", async () => {
(store.archiveAllDone as ReturnType<typeof vi.fn>).mockResolvedValue([]);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/archive-all-done", JSON.stringify({}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(res.body.archived).toEqual([]);
});
it("returns 500 on unexpected errors", async () => {
(store.archiveAllDone as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("Database error"));
const res = await REQUEST(buildApp(), "POST", "/api/tasks/archive-all-done", JSON.stringify({}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(500);
expect(res.body.error).toContain("Database error");
});
});
describe("PATCH /tasks/:id", () => {
let store: TaskStore;

View File

@@ -790,6 +790,16 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
}
});
// Archive all done tasks
router.post("/tasks/archive-all-done", async (req, res) => {
try {
const archived = await store.archiveAllDone();
res.json({ archived });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
// Upload attachment
router.post("/tasks/:id/attachments", upload.single("file"), async (req, res) => {
try {