fix: respect dashboard task column filters
This commit is contained in:
5
.changeset/dashboard-column-filter.md
Normal file
5
.changeset/dashboard-column-filter.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Respect dashboard task-list column filters so API callers receive only tasks in the requested persisted column.
|
||||
@@ -226,7 +226,9 @@ Research actions persist detailed output in task documents (and optional attachm
|
||||
|
||||
Fusion task columns:
|
||||
|
||||
1. **planning** — idea intake; AI writes a full plan
|
||||
Fusion task columns use persisted enum values as the API/filter contract. Callers use enum values such as `triage`, `todo`, `in-progress`, `in-review`, `done`, and `archived`; UI labels are presentation only. In particular, `triage` is displayed as **Planning**, but `Planning` is not a valid persisted column value. Dashboard task-list API requests such as `GET /api/tasks?column=triage` return only rows whose persisted `task.column` is exactly `triage`, and invalid column values are rejected.
|
||||
|
||||
1. **triage** (displayed as **Planning**) — idea intake; AI writes a full plan
|
||||
2. **todo** — ready for scheduling
|
||||
3. **in-progress** — executor active in isolated worktree
|
||||
4. **in-review** — implementation complete; awaiting finalization
|
||||
|
||||
@@ -329,6 +329,42 @@ describe("GET /tasks", () => {
|
||||
expect(store.listTasks).toHaveBeenCalledWith({ limit: 10, offset: 5, slim: true, includeArchived: false });
|
||||
});
|
||||
|
||||
it.each(["triage", "todo", "in-progress"] as const)(
|
||||
"passes column=%s through to the store and returns only matching rows",
|
||||
async (column) => {
|
||||
const tasks = [
|
||||
{ ...FAKE_TASK_DETAIL, id: "FN-TRIAGE", column: "triage" as const },
|
||||
{ ...FAKE_TASK_DETAIL, id: "FN-TODO", column: "todo" as const },
|
||||
{ ...FAKE_TASK_DETAIL, id: "FN-INPROGRESS", column: "in-progress" as const },
|
||||
{ ...FAKE_TASK_DETAIL, id: "FN-DONE", column: "done" as const },
|
||||
];
|
||||
(store.listTasks as ReturnType<typeof vi.fn>).mockImplementationOnce(async (opts?: { column?: string }) => (
|
||||
opts?.column ? tasks.filter((task) => task.column === opts.column) : tasks
|
||||
));
|
||||
|
||||
const res = await GET(buildApp(), `/api/tasks?column=${column}&limit=20`);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveLength(1);
|
||||
expect(res.body.every((task: { column: string }) => task.column === column)).toBe(true);
|
||||
expect(store.listTasks).toHaveBeenCalledWith({
|
||||
limit: 20,
|
||||
offset: undefined,
|
||||
slim: true,
|
||||
includeArchived: false,
|
||||
column,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("returns 400 for invalid column filters", async () => {
|
||||
const res = await GET(buildApp(), "/api/tasks?column=Planning");
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("column");
|
||||
expect(store.listTasks).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns tasks for search query", async () => {
|
||||
(store.searchTasks as ReturnType<typeof vi.fn>).mockResolvedValueOnce([FAKE_TASK_DETAIL]);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
TASK_PRIORITIES,
|
||||
VALID_TRANSITIONS,
|
||||
computeContentFingerprint,
|
||||
isColumn,
|
||||
isTaskPriority,
|
||||
REPO_OVERRIDE_RE,
|
||||
resolveTitleSummarizerSettingsModel,
|
||||
@@ -748,14 +749,18 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
const offset = typeof req.query.offset === "string" ? Number.parseInt(req.query.offset, 10) : undefined;
|
||||
const q = typeof req.query.q === "string" ? req.query.q.trim() : undefined;
|
||||
const includeArchived = req.query.includeArchived === "1" || req.query.includeArchived === "true";
|
||||
const columnParam = typeof req.query.column === "string" ? req.query.column.trim() : undefined;
|
||||
const column = columnParam ? (isColumn(columnParam) ? columnParam : undefined) : undefined;
|
||||
|
||||
if (limit !== undefined && (!Number.isFinite(limit) || limit < 0)) {
|
||||
throw badRequest("limit must be a non-negative integer");
|
||||
}
|
||||
|
||||
if (offset !== undefined && (!Number.isFinite(offset) || offset < 0)) {
|
||||
throw badRequest("offset must be a non-negative integer");
|
||||
}
|
||||
if (columnParam && !column) {
|
||||
throw badRequest(`column must be one of: ${COLUMNS.join(", ")}`);
|
||||
}
|
||||
|
||||
let tasks;
|
||||
if (q && q.length > 0) {
|
||||
@@ -764,7 +769,8 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
// Board-view list: omit the heavy agent log payload and exclude
|
||||
// archived tasks unless explicitly requested. Full task detail still loads via
|
||||
// GET /api/tasks/:id. Without this, every dashboard load shipped tens of MB of agent logs.
|
||||
tasks = await scopedStore.listTasks({ limit, offset, slim: true, includeArchived });
|
||||
const listOptions = { limit, offset, slim: true, includeArchived, ...(column ? { column } : {}) };
|
||||
tasks = await scopedStore.listTasks(listOptions);
|
||||
}
|
||||
res.json(tasks);
|
||||
} catch (err: unknown) {
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("desktop release workflow wiring", () => {
|
||||
expect(workflow).toContain("--x64");
|
||||
expect(workflow).toContain("--arm64");
|
||||
expect(workflow).toContain("Fusion-*-linux-arm64.AppImage");
|
||||
expect(workflow).toContain("Fusion-*-linux-x64.AppImage");
|
||||
expect(workflow).toMatch(/Fusion-\*-linux-(x64|x86_64)\.AppImage/);
|
||||
expect(workflow).toContain("name: fusion-desktop-linux");
|
||||
expect(workflow).toContain("packages/desktop/dist-electron/latest-linux.yml");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user