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:
|
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
|
2. **todo** — ready for scheduling
|
||||||
3. **in-progress** — executor active in isolated worktree
|
3. **in-progress** — executor active in isolated worktree
|
||||||
4. **in-review** — implementation complete; awaiting finalization
|
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 });
|
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 () => {
|
it("returns tasks for search query", async () => {
|
||||||
(store.searchTasks as ReturnType<typeof vi.fn>).mockResolvedValueOnce([FAKE_TASK_DETAIL]);
|
(store.searchTasks as ReturnType<typeof vi.fn>).mockResolvedValueOnce([FAKE_TASK_DETAIL]);
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
TASK_PRIORITIES,
|
TASK_PRIORITIES,
|
||||||
VALID_TRANSITIONS,
|
VALID_TRANSITIONS,
|
||||||
computeContentFingerprint,
|
computeContentFingerprint,
|
||||||
|
isColumn,
|
||||||
isTaskPriority,
|
isTaskPriority,
|
||||||
REPO_OVERRIDE_RE,
|
REPO_OVERRIDE_RE,
|
||||||
resolveTitleSummarizerSettingsModel,
|
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 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 q = typeof req.query.q === "string" ? req.query.q.trim() : undefined;
|
||||||
const includeArchived = req.query.includeArchived === "1" || req.query.includeArchived === "true";
|
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)) {
|
if (limit !== undefined && (!Number.isFinite(limit) || limit < 0)) {
|
||||||
throw badRequest("limit must be a non-negative integer");
|
throw badRequest("limit must be a non-negative integer");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offset !== undefined && (!Number.isFinite(offset) || offset < 0)) {
|
if (offset !== undefined && (!Number.isFinite(offset) || offset < 0)) {
|
||||||
throw badRequest("offset must be a non-negative integer");
|
throw badRequest("offset must be a non-negative integer");
|
||||||
}
|
}
|
||||||
|
if (columnParam && !column) {
|
||||||
|
throw badRequest(`column must be one of: ${COLUMNS.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
let tasks;
|
let tasks;
|
||||||
if (q && q.length > 0) {
|
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
|
// Board-view list: omit the heavy agent log payload and exclude
|
||||||
// archived tasks unless explicitly requested. Full task detail still loads via
|
// 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.
|
// 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);
|
res.json(tasks);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ describe("desktop release workflow wiring", () => {
|
|||||||
expect(workflow).toContain("--x64");
|
expect(workflow).toContain("--x64");
|
||||||
expect(workflow).toContain("--arm64");
|
expect(workflow).toContain("--arm64");
|
||||||
expect(workflow).toContain("Fusion-*-linux-arm64.AppImage");
|
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("name: fusion-desktop-linux");
|
||||||
expect(workflow).toContain("packages/desktop/dist-electron/latest-linux.yml");
|
expect(workflow).toContain("packages/desktop/dist-electron/latest-linux.yml");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user