feat(FN-3889): add bulk delete with dependency conflict detection to ListVi

Bulk delete in ListView is now complete with dependency conflict detection and user-facing recovery options, backed by shared utility logic and tests. The feature touched ListView's core behavior, companion tests, and a small changeset documenting the user-facing recovery behavior.

Fusion-Task-Id: FN-3889
This commit is contained in:
Fusion
2026-05-09 17:50:10 -07:00
committed by gsxdsm
parent ed5c49c461
commit 1563533e14
8 changed files with 322 additions and 96 deletions

View File

@@ -0,0 +1,34 @@
import { describe, expect, it } from "vitest";
import { extractDependencyDeleteConflict } from "../taskDelete";
describe("extractDependencyDeleteConflict", () => {
it("returns dependent ids from details.code payload", () => {
const err = Object.assign(new Error("conflict"), {
details: { code: "TASK_HAS_DEPENDENTS", dependentIds: ["FN-1", "FN-2", 3] },
});
expect(extractDependencyDeleteConflict(err)).toEqual({ dependentIds: ["FN-1", "FN-2"] });
});
it("returns null for missing or invalid details payload", () => {
const missingDetails = new Error("failed");
const invalidDetails = Object.assign(new Error("failed"), {
details: { code: "TASK_HAS_DEPENDENTS", dependentIds: "FN-1" },
});
expect(extractDependencyDeleteConflict(missingDetails)).toBeNull();
expect(extractDependencyDeleteConflict(invalidDetails)).toBeNull();
});
it("falls back to parsing ids from message", () => {
const err = new Error("Cannot delete FN-22 because dependent tasks FN-100 and FN-101 block it; FN-100");
expect(extractDependencyDeleteConflict(err)).toEqual({ dependentIds: ["FN-100", "FN-101"] });
});
it("returns null for non-Error inputs", () => {
expect(extractDependencyDeleteConflict(null)).toBeNull();
expect(extractDependencyDeleteConflict({ message: "FN-1 FN-2" })).toBeNull();
expect(extractDependencyDeleteConflict("boom")).toBeNull();
});
});

View File

@@ -0,0 +1,21 @@
export interface DependencyDeleteConflict {
dependentIds: string[];
}
export function extractDependencyDeleteConflict(err: unknown): DependencyDeleteConflict | null {
if (!(err instanceof Error)) {
return null;
}
const details = (err as { details?: { code?: string; dependentIds?: unknown } }).details;
if (details?.code === "TASK_HAS_DEPENDENTS" && Array.isArray(details.dependentIds)) {
return { dependentIds: details.dependentIds.filter((id): id is string => typeof id === "string") };
}
const idsInMessage = err.message.match(/[A-Z]+-\d+/g) ?? [];
if (idsInMessage.length > 1) {
return { dependentIds: [...new Set(idsInMessage.slice(1))] };
}
return null;
}