feat(FN-2726): add bulk node override support in dashboard workflows

- Extend batch task update APIs and route handlers to accept nodeId overrides alongside model settings
- Update ListView bulk edit UX to load nodes and apply node overrides through batch updates
- Add node routing visibility improvements in task detail/settings UI and align related labels/styles
- Expand dashboard/API test coverage for node override batch updates and bulk edit behavior
This commit is contained in:
Fusion
2026-04-28 13:37:37 -07:00
committed by gsxdsm
parent a59aeab987
commit d8eb39dab7
12 changed files with 445 additions and 163 deletions

View File

@@ -2858,6 +2858,95 @@ describe("POST /tasks/batch-update-models", () => {
expect(res.body.error).toContain("non-empty strings");
});
it("bulk sets nodeId", async () => {
const task1 = { ...FAKE_TASK_DETAIL, id: "FN-001" };
const updated1 = { ...task1, nodeId: "node-abc" };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce(task1);
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce(updated1);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/batch-update-models", JSON.stringify({
taskIds: ["FN-001"],
nodeId: "node-abc",
}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(res.body.count).toBe(1);
expect(res.body.updated[0].nodeId).toBe("node-abc");
expect(store.updateTask).toHaveBeenCalledWith("FN-001", expect.objectContaining({ nodeId: "node-abc" }));
});
it("bulk clears nodeId", async () => {
const task1 = { ...FAKE_TASK_DETAIL, id: "FN-001", nodeId: "node-abc" };
const updated1 = { ...task1, nodeId: undefined };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce(task1);
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce(updated1);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/batch-update-models", JSON.stringify({
taskIds: ["FN-001"],
nodeId: null,
}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(res.body.updated[0].nodeId).toBeUndefined();
expect(store.updateTask).toHaveBeenCalledWith("FN-001", expect.objectContaining({ nodeId: null }));
});
it("accepts nodeId without model fields", async () => {
const task1 = { ...FAKE_TASK_DETAIL, id: "FN-001" };
const updated1 = { ...task1, nodeId: "node-abc" };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce(task1);
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce(updated1);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/batch-update-models", JSON.stringify({
taskIds: ["FN-001"],
nodeId: "node-abc",
}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
});
it("returns 400 for invalid nodeId type", async () => {
const res = await REQUEST(buildApp(), "POST", "/api/tasks/batch-update-models", JSON.stringify({
taskIds: ["FN-001"],
nodeId: 123,
}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(400);
expect(res.body.error).toContain("nodeId must be a string, null, or undefined");
});
it("updates nodeId across multiple tasks", async () => {
const task1 = { ...FAKE_TASK_DETAIL, id: "FN-001" };
const task2 = { ...FAKE_TASK_DETAIL, id: "FN-002" };
const updated1 = { ...task1, nodeId: "node-xyz" };
const updated2 = { ...task2, nodeId: "node-xyz" };
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce(task1).mockResolvedValueOnce(task2);
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValueOnce(updated1).mockResolvedValueOnce(updated2);
const res = await REQUEST(buildApp(), "POST", "/api/tasks/batch-update-models", JSON.stringify({
taskIds: ["FN-001", "FN-002"],
nodeId: "node-xyz",
}), {
"Content-Type": "application/json",
});
expect(res.status).toBe(200);
expect(res.body.count).toBe(2);
expect(res.body.updated).toHaveLength(2);
});
it("returns 400 when no model fields provided", async () => {
const res = await REQUEST(buildApp(), "POST", "/api/tasks/batch-update-models", JSON.stringify({
taskIds: ["FN-001"],