fix(dashboard): Lane and ListView sorted every column with the LEGACY role defaults (wrong card order on renamed boards) (#3016)

`sortTasksForDisplayColumn` takes four role answers and defaults each to
the legacy id. Its own header names the callers that never supplied
them:

> *"defaults to the legacy id so the callers that do not resolve flags
(Lane, ListView) keep today's behaviour exactly."*

On a renamed board, today's behaviour is the **wrong order**, silently:

| lane | what is lost |
|---|---|
| hold | priority-then-FIFO queue order — an urgent card is no longer
visibly next |
| complete | completion-date ordering |
| review | the merging card no longer floats to the top |

Nothing throws, nothing logs. The cards are simply in the wrong order —
which is exactly why this survived every existing test in these files:
their fixtures use the built-in ids, where the defaults happen to be
right.

`Board.tsx` already resolves these from `column.flags`. Mirrored here
rather than answered a second way, including its `complete && !archived`
done-like rule.

## Reverted

All **3** new `Lane` cases fail. Each picks inputs where the role order
and the generic fallback **disagree**:

- hold — equal priority, so role order is created-at and the fallback is
task-id
- complete — `columnMovedAt` DESC vs task-id ascending
- review — a `merging` card, which the fallback ignores entirely

**My first draft asserted urgent-first and passed with the fix
reverted.** The generic sort also puts urgent first, so the assertion
discriminated nothing. Recording that because it is the second time this
shape has caught me: an assertion that is *true* is not the same as an
assertion that is *load-bearing*.

## Coverage I do not have

`ListView`'s identical wiring has **no component test**. Its harness
stubs `fetchBoardWorkflows` with a never-resolving promise, and
`listColumns` derives from the resolved workflow — so a renamed board is
not drivable there without reworking that stub, which several other
tests in the file depend on. The call site is covered structurally by
the lane-wiring ratchet (baseline 19 → 17) and by the helper's own unit
tests, but that is a structural guarantee, not a behavioural one. I
would rather say so than imply the two callers are equally proven.

## Verification

Lane + ListView + taskSorting + Board **357 passed** · `pnpm test:gate`
161 + 13 + 487 + 71 · lint · lifecycle census `--strict` · lane-wiring ·
fnxc-dates (TZ=UTC) · changesets — green.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Fixed task sorting in lanes and list views after workflow columns are
renamed.
* Preserved correct ordering for completed, on-hold, archived,
merge-blocked, and review tasks.
* Ensured task ordering reflects each column’s configured role rather
than its previous identifier.
  * Maintained consistent ordering across board and list views.

* **Tests**
* Added coverage for renamed workflow columns and their expected task
ordering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-07-31 01:55:08 -07:00
committed by GitHub
parent 6a5bd7a86f
commit 35729699b8
5 changed files with 106 additions and 4 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Board lanes and the list view now sort by each column's role, so renamed boards keep their card order.
category: fix
dev: `Lane` and `ListView` pass the resolved `isArchivedColumn`/`isHoldColumn`/`isCompleteColumn`/`isReviewColumn` traits to `sortTasksForDisplayColumn`, mirroring Board.tsx; previously they used the helper's legacy-id defaults.

View File

@@ -104,7 +104,27 @@ function LaneComponent(props: LaneProps) {
(grouped[task.column] ??= []).push(task);
}
for (const col of workflow.columns) {
grouped[col.id] = sortTasksForDisplayColumn(grouped[col.id] ?? [], task_legacyKey(col.id));
/*
FNXC:WorkflowResolvedColumns 2026-07-31-08:20 (the two callers taskSorting.ts names as unconverted):
`sortTasksForDisplayColumn` defaults its four role questions to the LEGACY ids, and its own header
says the callers that do not resolve flags — Lane and ListView — keep today's behaviour. Today's
behaviour on a renamed board is: the hold lane loses its priority-then-FIFO queue order, the
complete lane loses completion-date sorting, and merging cards stop floating to the top of review.
Nothing fails; the cards are just in the wrong order.
Board.tsx already resolves exactly this from `column.flags`; mirrored here rather than answered a
second way. `doneSortMode` stays defaulted because this lane has no operator setting for it.
*/
const isDoneLikeColumn = col.flags.complete === true && col.flags.archived !== true;
grouped[col.id] = sortTasksForDisplayColumn(
grouped[col.id] ?? [],
task_legacyKey(col.id),
undefined,
col.flags.archived === true,
col.flags.hold === true,
isDoneLikeColumn,
col.flags.mergeBlocker === true || col.flags.humanReview === true,
);
}
return grouped;
}, [tasks, workflow.columns]);

View File

@@ -1166,7 +1166,18 @@ export function ListView({
for (const column of listColumns) {
const columnId = column.id;
if (!sortField) {
groups[columnId] = sortTasksForDisplayColumn(groups[columnId], columnId as Column);
/* FNXC:WorkflowResolvedColumns 2026-07-31-08:22: same conversion as Lane.tsx — the sort's four
role questions default to legacy ids, so a renamed board sorted every lane generically. */
const isDoneLikeColumn = column.flags.complete === true && column.flags.archived !== true;
groups[columnId] = sortTasksForDisplayColumn(
groups[columnId],
columnId as Column,
undefined,
column.flags.archived === true,
column.flags.hold === true,
isDoneLikeColumn,
column.flags.mergeBlocker === true || column.flags.humanReview === true,
);
continue;
}

View File

@@ -206,3 +206,69 @@ describe("Lane", () => {
expect(screen.queryByTestId("column-inline-feedback")).toBeNull();
});
});
/*
FNXC:WorkflowResolvedColumns 2026-07-31-08:30:
Lane sorted every column with `sortTasksForDisplayColumn`'s LEGACY defaults, which taskSorting.ts's
own header calls out: the callers that do not resolve flags keep today's behaviour. Today's behaviour
on a renamed board is the wrong ORDER — the hold lane loses priority-then-FIFO, so a card an operator
marked urgent no longer shows up next. Nothing throws; the queue is just wrong.
The board below is the built-in vocabulary renamed and nothing else, which is why every case above
still passes: `todo` satisfies the legacy default.
*/
const RENAMED_WORKFLOW: BoardWorkflowDefinition = {
id: "custom:renamed",
name: "Renamed",
columns: [
{ id: "drafting", name: "Drafting", flags: { hold: true } },
{ id: "building", name: "Building", flags: { countsTowardWip: true } },
{ id: "checking", name: "Checking", flags: { humanReview: true } },
{ id: "shipped", name: "Shipped", flags: { complete: true } },
],
};
describe("Lane on a RENAMED board", () => {
/* Each case picks inputs where the ROLE order and the generic fallback order DISAGREE. My first
draft asserted urgent-first, which the generic sort also produces — it passed with the fix
reverted and proved nothing. */
function renderedIds() {
return screen.getAllByTestId(/^task-FN-/).map((node) => node.getAttribute("data-id"));
}
it("orders the hold lane by created-at, not task id", () => {
/* Equal priority: hold order is FIFO by createdAt, the generic fallback is task-id ascending. */
const tasks = [
mkTask({ id: "FN-1", column: "drafting", priority: "normal", createdAt: "2024-03-01T00:00:00.000Z" }),
mkTask({ id: "FN-9", column: "drafting", priority: "normal", createdAt: "2024-01-01T00:00:00.000Z" }),
];
render(<Lane {...baseProps()} workflow={RENAMED_WORKFLOW} tasks={tasks} />);
expect(renderedIds()).toEqual(["FN-9", "FN-1"]);
});
it("orders the complete lane newest-first by completion time", () => {
/* Complete sorts by columnMovedAt DESC; the generic fallback would put FN-1 first on id. */
const tasks = [
mkTask({ id: "FN-1", column: "shipped", priority: "normal", columnMovedAt: "2024-01-01T00:00:00.000Z" }),
mkTask({ id: "FN-9", column: "shipped", priority: "normal", columnMovedAt: "2024-03-01T00:00:00.000Z" }),
];
render(<Lane {...baseProps()} workflow={RENAMED_WORKFLOW} tasks={tasks} />);
expect(renderedIds()).toEqual(["FN-9", "FN-1"]);
});
it("floats a merging card to the top of the review lane", () => {
/* The "what is merging right now" ordering; the generic fallback ignores status entirely. */
const tasks = [
mkTask({ id: "FN-1", column: "checking", priority: "normal" }),
mkTask({ id: "FN-9", column: "checking", priority: "normal", status: "merging" }),
];
render(<Lane {...baseProps()} workflow={RENAMED_WORKFLOW} tasks={tasks} />);
expect(renderedIds()).toEqual(["FN-9", "FN-1"]);
});
});

View File

@@ -8,8 +8,6 @@
"packages/engine/src/project-engine.ts": 1,
"packages/engine/src/runtimes/in-process-runtime.ts": 1,
"packages/dashboard/src/routes/register-task-workflow-routes.ts": 1,
"packages/dashboard/app/components/Lane.tsx": 1,
"packages/dashboard/app/components/ListView.tsx": 1,
"packages/dashboard/app/hooks/useBlockerFanout.ts": 1,
"packages/cli/src/commands/dashboard-tui/app.tsx": 1,
"packages/cli/src/commands/dashboard-tui/bucket-mapping.ts": 1,