Files
fusion/packages/dashboard/app/components/taskSorting.ts
gsxdsm 5b668d297f FN-7238: add Done column sort modes
Add Done-column controls for choosing descending completion-date or task-ID order.

- Add a Done-only sort selector shared by legacy and workflow complete columns.
- Extend task display sorting with completion-date and task-ID descending modes.
- Cover board, column, and sorting behavior with regression tests and docs.
- Add a minor changeset for the published CLI package.

Files changed:
 .changeset/fn-7238-done-column-sort.md             |   7 +
 docs/dashboard-guide.md                            |   5 +-
 packages/dashboard/app/components/Board.tsx        |  28 +++-
 packages/dashboard/app/components/Column.tsx       |  33 ++++-
 .../app/components/__tests__/Board.test.tsx        | 133 ++++++++++++++++-
 .../app/components/__tests__/Column.test.tsx       | 158 +++++++++++++++++++++
 .../app/components/__tests__/taskSorting.test.ts   | 114 +++++++++++++++
 packages/dashboard/app/components/taskSorting.ts   |  39 ++++-
 packages/dashboard/app/styles.css                  |  35 +++++
 9 files changed, 538 insertions(+), 14 deletions(-)

Fusion-Task-Id: FN-7238

Fusion-Task-Lineage: 143722e9-0b59-41e5-954d-810263f18ba9

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-06-29 15:46:35 -07:00

94 lines
3.1 KiB
TypeScript

import type { Task, Column } from "@fusion/core";
export type DoneColumnSortMode = "completion-date-desc" | "task-id-desc";
function getTaskPriorityRank(priority: Task["priority"] | null | undefined): number {
if (priority === "urgent") return 3;
if (priority === "high") return 2;
if (priority === "low") return 0;
return 1;
}
function compareTaskPriority(a: Task["priority"] | null | undefined, b: Task["priority"] | null | undefined): number {
return getTaskPriorityRank(b) - getTaskPriorityRank(a);
}
function getTaskIdNumericToken(id: string): number | null {
const token = id.slice(id.lastIndexOf("-") + 1);
if (!/^\d+$/.test(token)) return null;
const parsed = Number.parseInt(token, 10);
return Number.isFinite(parsed) ? parsed : null;
}
function compareTaskIdNumeric(a: string, b: string): number {
const aNum = getTaskIdNumericToken(a);
const bNum = getTaskIdNumericToken(b);
if (aNum !== null && bNum !== null && aNum !== bNum) {
return aNum - bNum;
}
return a.localeCompare(b);
}
function compareTaskIdNumericDesc(a: string, b: string): number {
const aNum = getTaskIdNumericToken(a);
const bNum = getTaskIdNumericToken(b);
if (aNum !== null && bNum !== null && aNum !== bNum) {
return bNum - aNum;
}
return b.localeCompare(a);
}
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 isMergeActiveStatus(status: string | null | undefined): boolean {
return status === "merging" || status === "merging-pr" || status === "merging-fix";
}
export function sortTasksForDisplayColumn(
tasks: readonly Task[],
column: Column,
doneSortMode: DoneColumnSortMode = "completion-date-desc",
): Task[] {
if (column === "todo") {
return [...tasks].sort((a, b) => {
const priorityCmp = compareTaskPriority(a.priority, b.priority);
if (priorityCmp !== 0) return priorityCmp;
if (a.createdAt !== b.createdAt) return a.createdAt.localeCompare(b.createdAt);
return compareTaskIdNumeric(a.id, b.id);
});
}
return [...tasks].sort((a, b) => {
if (column === "done") {
/*
FNXC:DoneColumnSorting 2026-06-29-14:48:
Done keeps completion-date descending as the default for existing board, lane, and list callers while supporting an explicit task-id descending mode for users who need newest FN ids first.
*/
if (doneSortMode === "task-id-desc") {
return compareTaskIdNumericDesc(a.id, b.id);
}
const timestampCmp = getDoneSortTimestamp(b) - getDoneSortTimestamp(a);
if (timestampCmp !== 0) return timestampCmp;
return compareTaskIdNumeric(a.id, b.id);
}
if (column === "in-review") {
const aIsMerging = isMergeActiveStatus(a.status);
const bIsMerging = isMergeActiveStatus(b.status);
if (aIsMerging !== bIsMerging) return aIsMerging ? -1 : 1;
}
const priorityCmp = compareTaskPriority(a.priority, b.priority);
if (priorityCmp !== 0) return priorityCmp;
return compareTaskIdNumeric(a.id, b.id);
});
}