Keep reverted tasks out of completed views and give every task-detail host a revision recovery path. - Exclude reverted tasks from complete columns and group them in dedicated recovery lists. - Provide Delete and Revise actions in cards, lists, docks, modals, and popped-out details. - Add localized copy, documentation, regression coverage, and a patch changeset. Files changed: .changeset/fn-8696-reverted-task-resolution.md | 7 ++++ docs/dashboard-guide.md | 4 ++ packages/dashboard/app/App.tsx | 2 + packages/dashboard/app/components/AppModals.tsx | 2 + packages/dashboard/app/components/Board.tsx | 42 +++++++++++++++++++- packages/dashboard/app/components/DockTaskList.tsx | 21 +++++++++- packages/dashboard/app/components/ListView.tsx | 18 +++++++++ packages/dashboard/app/components/TaskCard.css | 4 ++ packages/dashboard/app/components/TaskCard.tsx | 9 +++++ .../dashboard/app/components/TaskDetailModal.tsx | 18 ++++++++- .../app/components/__tests__/Board.test.tsx | 46 ++++++++++++++++++++++ .../app/components/__tests__/DockTaskList.test.tsx | 29 +++++++++++++- .../app/components/dashboard/MainContent.tsx | 4 ++ .../app/components/overflowViewRegistry.tsx | 3 ++ .../app/components/useRightDockController.tsx | 9 +++++ .../app/utils/__tests__/taskRevert.test.ts | 16 +++++++- packages/dashboard/app/utils/taskRevert.ts | 18 +++++++++ packages/i18n/locales/en/app.json | 3 ++ 18 files changed, 249 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-8696 Fusion-Task-Lineage: 7c9550fb-ef8e-4422-8ec1-0de1665413a3 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
84 lines
4.0 KiB
TypeScript
84 lines
4.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { Task } from "@fusion/core";
|
|
import { isTaskReverted, findOpenUndoTaskForSource, partitionRevertedTasks } from "../taskRevert";
|
|
|
|
describe("isTaskReverted", () => {
|
|
it.each([
|
|
[undefined, false],
|
|
[{}, false],
|
|
[{ revertedAt: "" }, false],
|
|
[{ revertedAt: " " }, false],
|
|
[{ revertedAt: 123 }, false],
|
|
[{ revertedAt: true }, false],
|
|
[{ revertedAt: "2026-07-16T00:00:00.000Z" }, true],
|
|
] as const)("returns %s for revertedAt metadata", (sourceMetadata, expected) => {
|
|
expect(isTaskReverted(sourceMetadata as Task["sourceMetadata"] | undefined)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("partitionRevertedTasks", () => {
|
|
const task = (id: string, revertedAt?: unknown): Task => ({
|
|
id, column: "done", title: id, description: "", createdAt: "", updatedAt: "", dependencies: [], steps: [],
|
|
sourceMetadata: revertedAt === undefined ? {} : { revertedAt },
|
|
} as unknown as Task);
|
|
|
|
it("keeps invalid markers normal and returns valid markers once", () => {
|
|
const reverted = task("KB-2", "2026-08-01T00:00:00.000Z");
|
|
const result = partitionRevertedTasks([task("KB-1", " "), reverted, reverted, task("KB-3", 42)]);
|
|
expect(result.normal.map(({ id }) => id)).toEqual(["KB-1", "KB-3"]);
|
|
expect(result.reverted.map(({ id }) => id)).toEqual(["KB-2"]);
|
|
});
|
|
});
|
|
|
|
/*
|
|
FNXC:WorkflowResolvedColumns 2026-07-31-23:30:
|
|
THE UNDO-TASK LOOKUP CLASSIFIED A NEIGHBOUR'S COLUMN BY ID.
|
|
|
|
`findOpenUndoTaskForSource` skips candidates that are finished, so a done/archived prior undo attempt
|
|
never renders as an active "Undo task" link. Keyed on `done`/`archived`, a board that renames those
|
|
lanes matched neither: a FINISHED undo task kept rendering as an open one, which is the stale
|
|
affordance the function's own header says it exists to prevent.
|
|
|
|
The flags are PER-CANDIDATE, keyed by task id. That is what makes this correct and what two earlier
|
|
notes said was unavailable — `TaskDetailModal` has had `columnFlagsByTaskId` as a prop all along and
|
|
already uses it this way for the near-duplicate canonical.
|
|
|
|
Both directions are asserted, and the negative is the load-bearing one: the map is fail-soft, so a
|
|
candidate it does not cover must still be treated as OPEN rather than silently skipped. A conversion
|
|
that skipped unknown candidates would hide live undo links.
|
|
*/
|
|
describe("findOpenUndoTaskForSource resolves each candidate's own lanes", () => {
|
|
const candidate = (id: string, column: string, createdAt: string): Task => ({
|
|
id, column, title: id, description: "", createdAt, updatedAt: createdAt,
|
|
dependencies: [], steps: [], sourceMetadata: { revertOf: "KB-SRC" },
|
|
} as unknown as Task);
|
|
|
|
it("skips an undo task resting in a RENAMED complete lane", () => {
|
|
const tasks = [candidate("KB-UNDO", "shipped", "2026-06-01T00:00:00.000Z")];
|
|
const flags = new Map([["KB-UNDO", { complete: true }]]);
|
|
|
|
expect(findOpenUndoTaskForSource(tasks, "KB-SRC", flags as never)).toBeUndefined();
|
|
});
|
|
|
|
it("still returns an undo task resting in a live lane on that same board", () => {
|
|
const tasks = [candidate("KB-UNDO", "building", "2026-06-01T00:00:00.000Z")];
|
|
const flags = new Map([["KB-UNDO", { countsTowardWip: true }]]);
|
|
|
|
expect(findOpenUndoTaskForSource(tasks, "KB-SRC", flags as never)?.id).toBe("KB-UNDO");
|
|
});
|
|
|
|
it("treats a candidate the map does not cover as OPEN, not skipped", () => {
|
|
/* Fail-soft in the safe direction: an unknown candidate keeps its link rather than losing it. */
|
|
const tasks = [candidate("KB-UNDO", "building", "2026-06-01T00:00:00.000Z")];
|
|
|
|
expect(findOpenUndoTaskForSource(tasks, "KB-SRC", new Map() as never)?.id).toBe("KB-UNDO");
|
|
});
|
|
|
|
it("still skips the legacy ids when no flags are supplied at all", () => {
|
|
/* CONTROL: the parameter is optional, so an unwired caller behaves exactly as before. */
|
|
const tasks = [candidate("KB-UNDO", "done", "2026-06-01T00:00:00.000Z")];
|
|
|
|
expect(findOpenUndoTaskForSource(tasks, "KB-SRC")).toBeUndefined();
|
|
});
|
|
});
|