From 35729699b8d57ccf362662bf0408a945d643ee0c Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 01:55:08 -0700 Subject: [PATCH] fix(dashboard): Lane and ListView sorted every column with the LEGACY role defaults (wrong card order on renamed boards) (#3016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. ## 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. --- .changeset/lane-listview-sort-traits.md | 7 ++ packages/dashboard/app/components/Lane.tsx | 22 ++++++- .../dashboard/app/components/ListView.tsx | 13 +++- .../app/components/__tests__/Lane.test.tsx | 66 +++++++++++++++++++ scripts/lib/lane-wiring-baseline.json | 2 - 5 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 .changeset/lane-listview-sort-traits.md diff --git a/.changeset/lane-listview-sort-traits.md b/.changeset/lane-listview-sort-traits.md new file mode 100644 index 0000000000..758ccb1489 --- /dev/null +++ b/.changeset/lane-listview-sort-traits.md @@ -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. diff --git a/packages/dashboard/app/components/Lane.tsx b/packages/dashboard/app/components/Lane.tsx index 4615751b3b..6882b5685e 100644 --- a/packages/dashboard/app/components/Lane.tsx +++ b/packages/dashboard/app/components/Lane.tsx @@ -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]); diff --git a/packages/dashboard/app/components/ListView.tsx b/packages/dashboard/app/components/ListView.tsx index 216df7636e..6f0e822100 100644 --- a/packages/dashboard/app/components/ListView.tsx +++ b/packages/dashboard/app/components/ListView.tsx @@ -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; } diff --git a/packages/dashboard/app/components/__tests__/Lane.test.tsx b/packages/dashboard/app/components/__tests__/Lane.test.tsx index 41d21e9d06..5f7e6d0296 100644 --- a/packages/dashboard/app/components/__tests__/Lane.test.tsx +++ b/packages/dashboard/app/components/__tests__/Lane.test.tsx @@ -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(); + + 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(); + + 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(); + + expect(renderedIds()).toEqual(["FN-9", "FN-1"]); + }); +}); diff --git a/scripts/lib/lane-wiring-baseline.json b/scripts/lib/lane-wiring-baseline.json index 1e631f0633..9f071d7593 100644 --- a/scripts/lib/lane-wiring-baseline.json +++ b/scripts/lib/lane-wiring-baseline.json @@ -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,