feat(FN-3364): sort done tasks by most recent completion
- Sort Done column and list views by most recent completion timestamp to keep recent work visible first - Update dashboard and task management docs to document done-column recency ordering behavior - Preserve merge-active task state across verification bounce paths and board/list rendering updates - Forward removeDependencyReferences in deleteTask flows and treat finish_reason=repeat as a soft-stop in engine executor Fusion-Task-Id: FN-3364
This commit is contained in:
@@ -16,6 +16,7 @@ Features:
|
||||
- Inline quick entry creation
|
||||
- PR/issue badges with live updates
|
||||
- GitHub provenance marker on task cards imported from GitHub (`sourceType: github_import`), shown alongside existing footer metadata like timers
|
||||
- Column ordering semantics: `triage`, `todo`, `in-progress`, `in-review`, and `archived` stay priority-ordered; `done` is ordered by most recent completion first (`columnMovedAt`, then `updatedAt`, then `createdAt` fallback)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -86,6 +86,10 @@ Fusion task columns:
|
||||
5. **done** — merged/finalized
|
||||
6. **archived** — preserved history, optionally cleaned from filesystem
|
||||
|
||||
Board ordering behavior:
|
||||
- Active work columns (`triage`, `todo`, `in-progress`, `in-review`) remain priority-ordered.
|
||||
- The `done` column is recency-ordered by completion time (newest first), using `columnMovedAt` as primary and falling back to `updatedAt` then `createdAt` for legacy tasks.
|
||||
|
||||
### Lifecycle commands
|
||||
|
||||
```bash
|
||||
|
||||
@@ -82,8 +82,24 @@ function compareTaskIdNumeric(a: string, b: string): number {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
function getDoneSortTimestamp(task: Task): number {
|
||||
const timestamp = task.columnMovedAt ?? task.updatedAt ?? task.createdAt;
|
||||
const parsed = Date.parse(timestamp);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function sortTasksForColumn(tasks: Task[], column: ColumnType): Task[] {
|
||||
return [...tasks].sort((a, b) => {
|
||||
if (column === "done") {
|
||||
const timestampCmp = getDoneSortTimestamp(b) - getDoneSortTimestamp(a);
|
||||
if (timestampCmp !== 0) {
|
||||
return timestampCmp;
|
||||
}
|
||||
|
||||
// Deterministic tie-breaker when completion timestamps match.
|
||||
return compareTaskIdNumeric(a.id, b.id);
|
||||
}
|
||||
|
||||
// In the in-review column, merging tasks stay pinned above non-merging tasks.
|
||||
if (column === "in-review") {
|
||||
const aIsMerging = a.status === "merging" || a.status === "merging-pr" || a.status === "merging-fix";
|
||||
@@ -93,7 +109,7 @@ function sortTasksForColumn(tasks: Task[], column: ColumnType): Task[] {
|
||||
}
|
||||
}
|
||||
|
||||
// Primary sort: priority descending (urgent → high → normal → low).
|
||||
// Primary sort for non-done columns: priority descending (urgent → high → normal → low).
|
||||
// compareTaskPriority normalizes missing/invalid values to `normal`.
|
||||
const priorityCmp = compareTaskPriority(a.priority, b.priority);
|
||||
if (priorityCmp !== 0) {
|
||||
|
||||
@@ -292,12 +292,99 @@ describe("Board", () => {
|
||||
});
|
||||
|
||||
describe("sortTasksForColumn priority ordering", () => {
|
||||
it("orders tasks by priority descending (urgent → high → normal → low)", () => {
|
||||
it("orders done tasks by most recent completion regardless of priority", () => {
|
||||
const tasks: Task[] = [
|
||||
createTask({ id: "FN-003", description: "Low task", column: "todo", priority: "low" }),
|
||||
createTask({ id: "FN-001", description: "Urgent task", column: "todo", priority: "urgent" }),
|
||||
createTask({ id: "FN-004", description: "Normal task", column: "todo", priority: "normal" }),
|
||||
createTask({ id: "FN-002", description: "High task", column: "todo", priority: "high" }),
|
||||
createTask({
|
||||
id: "FN-003",
|
||||
description: "Older urgent done task",
|
||||
column: "done",
|
||||
priority: "urgent",
|
||||
columnMovedAt: "2024-01-01T09:00:00.000Z",
|
||||
}),
|
||||
createTask({
|
||||
id: "FN-001",
|
||||
description: "Newest low-priority done task",
|
||||
column: "done",
|
||||
priority: "low",
|
||||
columnMovedAt: "2024-01-01T11:00:00.000Z",
|
||||
}),
|
||||
createTask({
|
||||
id: "FN-002",
|
||||
description: "Middle high-priority done task",
|
||||
column: "done",
|
||||
priority: "high",
|
||||
columnMovedAt: "2024-01-01T10:00:00.000Z",
|
||||
}),
|
||||
];
|
||||
|
||||
renderBoard({ tasks });
|
||||
|
||||
const doneTasks = JSON.parse(screen.getByTestId("column-done").getAttribute("data-tasks") || "[]") as Task[];
|
||||
expect(doneTasks.map((t: Task) => t.id)).toEqual(["FN-001", "FN-002", "FN-003"]);
|
||||
});
|
||||
|
||||
it("falls back to updatedAt and createdAt for legacy done tasks missing columnMovedAt", () => {
|
||||
const tasks: Task[] = [
|
||||
createTask({
|
||||
id: "FN-010",
|
||||
description: "Has updatedAt fallback",
|
||||
column: "done",
|
||||
updatedAt: "2024-01-01T10:30:00.000Z",
|
||||
}),
|
||||
createTask({
|
||||
id: "FN-011",
|
||||
description: "Has createdAt fallback",
|
||||
column: "done",
|
||||
createdAt: "2024-01-01T10:45:00.000Z",
|
||||
}),
|
||||
createTask({
|
||||
id: "FN-012",
|
||||
description: "Has real completion timestamp",
|
||||
column: "done",
|
||||
columnMovedAt: "2024-01-01T11:00:00.000Z",
|
||||
}),
|
||||
];
|
||||
|
||||
const taskWithCreatedAtOnly = tasks[1];
|
||||
delete taskWithCreatedAtOnly.columnMovedAt;
|
||||
delete taskWithCreatedAtOnly.updatedAt;
|
||||
|
||||
renderBoard({ tasks });
|
||||
|
||||
const doneTasks = JSON.parse(screen.getByTestId("column-done").getAttribute("data-tasks") || "[]") as Task[];
|
||||
expect(doneTasks.map((t: Task) => t.id)).toEqual(["FN-012", "FN-011", "FN-010"]);
|
||||
});
|
||||
|
||||
it("keeps non-done columns priority-ordered even when recency differs", () => {
|
||||
const tasks: Task[] = [
|
||||
createTask({
|
||||
id: "FN-003",
|
||||
description: "Low but newest",
|
||||
column: "todo",
|
||||
priority: "low",
|
||||
columnMovedAt: "2024-01-01T12:00:00.000Z",
|
||||
}),
|
||||
createTask({
|
||||
id: "FN-001",
|
||||
description: "Urgent but older",
|
||||
column: "todo",
|
||||
priority: "urgent",
|
||||
columnMovedAt: "2024-01-01T10:00:00.000Z",
|
||||
}),
|
||||
createTask({
|
||||
id: "FN-004",
|
||||
description: "Normal",
|
||||
column: "todo",
|
||||
priority: "normal",
|
||||
columnMovedAt: "2024-01-01T11:00:00.000Z",
|
||||
}),
|
||||
createTask({
|
||||
id: "FN-002",
|
||||
description: "High",
|
||||
column: "todo",
|
||||
priority: "high",
|
||||
columnMovedAt: "2024-01-01T09:00:00.000Z",
|
||||
}),
|
||||
];
|
||||
|
||||
renderBoard({ tasks, searchQuery: "task" });
|
||||
|
||||
Reference in New Issue
Block a user