feat(FN-3596): unify board/list task ordering, split TaskDetailModal test m
The merge introduces a major TaskDetailModal test refactor (splitting a 6.7K-line monolith into five focused suites), significant merger improvements including autostash race-rescue, deduplication, and advisory logging for destructive operations, a new TUI narrow-mode log-split feature for the dashb Fusion-Task-Id: FN-3596
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
||||
isTaskPriority,
|
||||
normalizeTaskPriority,
|
||||
sortTasksByPriorityThenAgeAndId,
|
||||
compareTaskIdNumeric,
|
||||
sortTasksForDisplayColumn,
|
||||
} from "../task-priority.js";
|
||||
import {
|
||||
DEFAULT_TASK_PRIORITY,
|
||||
@@ -55,9 +57,42 @@ describe("task-priority", () => {
|
||||
expect(compareTasksByPriorityThenAgeAndId(tasks[0], tasks[1])).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("compares numeric IDs with locale fallback", () => {
|
||||
expect(compareTaskIdNumeric("FN-2", "FN-10")).toBeLessThan(0);
|
||||
expect(compareTaskIdNumeric("TASK-B", "TASK-A")).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("applies board/list default ordering semantics by column", () => {
|
||||
const base = {
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
columnMovedAt: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
const todoSorted = sortTasksForDisplayColumn([
|
||||
{ ...base, id: "FN-003", column: "todo", priority: "low" as TaskPriority, createdAt: "2026-01-01T00:00:00.000Z" },
|
||||
{ ...base, id: "FN-001", column: "todo", priority: "urgent" as TaskPriority, createdAt: "2026-01-02T00:00:00.000Z" },
|
||||
{ ...base, id: "FN-002", column: "todo", priority: "high" as TaskPriority, createdAt: "2026-01-01T12:00:00.000Z" },
|
||||
], "todo");
|
||||
expect(todoSorted.map((task) => task.id)).toEqual(["FN-001", "FN-002", "FN-003"]);
|
||||
|
||||
const inReviewSorted = sortTasksForDisplayColumn([
|
||||
{ ...base, id: "FN-010", column: "in-review", status: "review-ready", priority: "urgent" as TaskPriority },
|
||||
{ ...base, id: "FN-011", column: "in-review", status: "merging-fix", priority: "high" as TaskPriority },
|
||||
], "in-review");
|
||||
expect(inReviewSorted.map((task) => task.id)).toEqual(["FN-011", "FN-010"]);
|
||||
|
||||
const doneSorted = sortTasksForDisplayColumn([
|
||||
{ ...base, id: "FN-020", column: "done", priority: "urgent" as TaskPriority, columnMovedAt: "2026-01-01T08:00:00.000Z" },
|
||||
{ ...base, id: "FN-021", column: "done", priority: "low" as TaskPriority, columnMovedAt: "2026-01-01T09:00:00.000Z" },
|
||||
], "done");
|
||||
expect(doneSorted.map((task) => task.id)).toEqual(["FN-021", "FN-020"]);
|
||||
});
|
||||
|
||||
it("re-exports priority helpers from the core index", () => {
|
||||
expect(core.TASK_PRIORITIES).toEqual(TASK_PRIORITIES);
|
||||
expect(core.DEFAULT_TASK_PRIORITY).toBe("normal");
|
||||
expect(core.normalizeTaskPriority("bogus")).toBe(DEFAULT_TASK_PRIORITY);
|
||||
expect(typeof core.sortTasksForDisplayColumn).toBe("function");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -309,8 +309,10 @@ export {
|
||||
compareTaskPriority,
|
||||
compareTasksByPriorityThenAgeAndId,
|
||||
sortTasksByPriorityThenAgeAndId,
|
||||
compareTaskIdNumeric,
|
||||
sortTasksForDisplayColumn,
|
||||
} from "./task-priority.js";
|
||||
export type { TaskPrioritySortable } from "./task-priority.js";
|
||||
export type { TaskPrioritySortable, TaskColumnSortable } from "./task-priority.js";
|
||||
export {
|
||||
mapFeatureToTaskHandoff,
|
||||
mapRoadmapToMissionHandoff,
|
||||
|
||||
@@ -7,6 +7,13 @@ export interface TaskPrioritySortable {
|
||||
priority?: TaskPriority | null;
|
||||
}
|
||||
|
||||
export interface TaskColumnSortable extends TaskPrioritySortable {
|
||||
column: string;
|
||||
status?: string | null;
|
||||
columnMovedAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
const PRIORITY_RANK: Record<TaskPriority, number> = {
|
||||
low: 0,
|
||||
normal: 1,
|
||||
@@ -40,7 +47,7 @@ export function compareTaskPriority(a: unknown, b: unknown): number {
|
||||
return getTaskPriorityRank(b) - getTaskPriorityRank(a);
|
||||
}
|
||||
|
||||
function compareTaskId(a: string, b: string): number {
|
||||
export function compareTaskIdNumeric(a: string, b: string): number {
|
||||
const aNum = Number.parseInt(a.slice(a.lastIndexOf("-") + 1), 10);
|
||||
const bNum = Number.parseInt(b.slice(b.lastIndexOf("-") + 1), 10);
|
||||
|
||||
@@ -65,7 +72,7 @@ export function compareTasksByPriorityThenAgeAndId<T extends TaskPrioritySortabl
|
||||
return a.createdAt.localeCompare(b.createdAt);
|
||||
}
|
||||
|
||||
return compareTaskId(a.id, b.id);
|
||||
return compareTaskIdNumeric(a.id, b.id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,3 +83,47 @@ export function sortTasksByPriorityThenAgeAndId<T extends TaskPrioritySortable>(
|
||||
): T[] {
|
||||
return [...tasks].sort(compareTasksByPriorityThenAgeAndId);
|
||||
}
|
||||
|
||||
function getDoneSortTimestamp(task: TaskColumnSortable): 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";
|
||||
}
|
||||
|
||||
/**
|
||||
* Column-aware default ordering shared by board and list surfaces.
|
||||
*/
|
||||
export function sortTasksForDisplayColumn<T extends TaskColumnSortable>(tasks: readonly T[], column: string): T[] {
|
||||
if (column === "todo") {
|
||||
return sortTasksByPriorityThenAgeAndId(tasks);
|
||||
}
|
||||
|
||||
return [...tasks].sort((a, b) => {
|
||||
if (column === "done") {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user