From 0f1ca9817a0a3f98b8f85473ba8b2f15b7aaed5f Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Wed, 14 Oct 2026 11:37:36 +1100 Subject: [PATCH] fix(canvases): sort column cards oldest first Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: cd29e26b-0e56-48bf-a046-ac749aef86ce --- .../external-plugin-review-board/README.md | 2 + .../public/app.js | 4 +- .github/extensions/open-pr-board/README.md | 4 +- .../extensions/open-pr-board/public/app.js | 4 +- eng/board-column-order.test.mjs | 45 +++++++++++++++++++ 5 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 eng/board-column-order.test.mjs diff --git a/.github/extensions/external-plugin-review-board/README.md b/.github/extensions/external-plugin-review-board/README.md index 82aa3d9fe..d0c5c98b8 100644 --- a/.github/extensions/external-plugin-review-board/README.md +++ b/.github/extensions/external-plugin-review-board/README.md @@ -10,6 +10,7 @@ Open it by asking Copilot to "open the plugin review board" (canvas id `external - **Kanban board** with buckets: Unreviewed → Reviewing → Straight reject / Probably reject / Needs review / Accept → Actioned. Drag cards between buckets (or use the *Bucket* dropdown in the details panel). Dragging a card back to *Unreviewed* clears its AI review so it's picked up by the next review. + Cards in each bucket are sorted oldest to newest by creation date. - **Perform review** asks the agent to review every item without an AI review, using [`review-guidance.md`](./review-guidance.md) and the decisions you've made from the board. Results are recorded with the `record_review` canvas action and move cards into their recommended bucket. @@ -58,6 +59,7 @@ not imported; boards start fresh. ## Regression tests +Run `node --test eng/board-column-order.test.mjs` to check oldest-first column ordering. Run `node --experimental-vm-modules --test eng/board-state-storage.test.mjs` from the repository root to check both canvases' session-scoped persistence without accessing GitHub. Run `node --test eng/board-bulk-actions.test.mjs` to check column confirmation, staged feedback, diff --git a/.github/extensions/external-plugin-review-board/public/app.js b/.github/extensions/external-plugin-review-board/public/app.js index 192accb43..c77c9ca8c 100644 --- a/.github/extensions/external-plugin-review-board/public/app.js +++ b/.github/extensions/external-plugin-review-board/public/app.js @@ -179,7 +179,9 @@ function renderBoard() { ); const columns = board.columns.map((column) => { - const items = board.items.filter((item) => item.column === column.id && matchesFilter(item)); + const items = board.items + .filter((item) => item.column === column.id && matchesFilter(item)) + .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt) || a.number - b.number); const cards = h( "div", { class: "column-cards" }, diff --git a/.github/extensions/open-pr-board/README.md b/.github/extensions/open-pr-board/README.md index 098f12fbb..08e33e0c2 100644 --- a/.github/extensions/open-pr-board/README.md +++ b/.github/extensions/open-pr-board/README.md @@ -10,7 +10,8 @@ authenticated `gh` CLI. - **Buckets** — `Unreviewed`, `Reviewing`, `Close`, `Request changes`, `Needs insight`, `Approve`, `Actioned`. The AI recommendation picks the bucket; - dragging a card overrides it. + dragging a card overrides it. Cards in each bucket are sorted oldest to newest + by creation date. - **AI reviews** — "Review pending" asks the agent to review every PR that has never been reviewed or whose review is stale. The agent records structured results (rationale, repo fit, compliance, differentiation, risks, validation, confidence, @@ -90,6 +91,7 @@ Results arriving after a manual move, reset, or replacement review are rejected. ## Regression tests +Run `node --test eng/board-column-order.test.mjs` to check oldest-first column ordering. Run `node --test eng/open-pr-board.test.mjs` from the repository root. GitHub writes are tested with injected in-process fakes, never the authenticated CLI. Run `node --test eng/board-bulk-actions.test.mjs` to check column confirmation, staged feedback, diff --git a/.github/extensions/open-pr-board/public/app.js b/.github/extensions/open-pr-board/public/app.js index 5f16c4b30..1ae8cd60c 100644 --- a/.github/extensions/open-pr-board/public/app.js +++ b/.github/extensions/open-pr-board/public/app.js @@ -280,7 +280,9 @@ function renderBoard() { for (const list of el.board.querySelectorAll(".column-cards")) scroll.set(list.dataset.column, list.scrollTop); const columns = board.columns.map((column) => { - const items = board.items.filter((item) => item.column === column.id && matchesFilter(item)); + const items = board.items + .filter((item) => item.column === column.id && matchesFilter(item)) + .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt) || a.number - b.number); const list = h( "div", { diff --git a/eng/board-column-order.test.mjs b/eng/board-column-order.test.mjs new file mode 100644 index 000000000..26586547b --- /dev/null +++ b/eng/board-column-order.test.mjs @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import vm from "node:vm"; +import { test } from "node:test"; + +for (const boardName of ["open-pr-board", "external-plugin-review-board"]) { + test(`${boardName}: columns render oldest to newest without changing board state`, async () => { + const source = await readFile( + new URL(`../.github/extensions/${boardName}/public/app.js`, import.meta.url), "utf8", + ); + const items = [ + { number: 50, column: "approve", createdAt: "2026-10-14T00:00:00Z", updatedAt: "2026-10-14T00:00:00Z" }, + { number: 30, column: "approve", createdAt: "2026-10-01T00:00:00Z", updatedAt: "2026-10-14T01:00:00Z" }, + { number: 10, column: "close", createdAt: "2026-09-01T00:00:00Z" }, + { number: 20, column: "approve", createdAt: "2026-10-01T00:00:00Z" }, + { number: 5, column: "approve", createdAt: "2026-08-01T00:00:00Z", hidden: true }, + { number: 40, column: "close", createdAt: "2026-10-10T00:00:00Z" }, + ]; + const original = structuredClone(items); + const rendered = []; + const node = { querySelectorAll: () => [], replaceChildren() {} }; + const ui = { + board: node, title: {}, summary: {}, meta: {}, review: {}, + }; + const context = vm.createContext({ + board: { items, columns: [{ id: "approve" }, { id: "close" }, { id: "empty" }] }, + el: ui, ui, filter: "", + matchesFilter: (item) => !item.hidden, + renderCard: (item) => { rendered.push(item.number); return {}; }, + h: () => ({}), + relativeTime: () => "now", + pendingReviewCount: () => 0, + COLUMN_HINTS: {}, + }); + vm.runInContext(source.slice(source.indexOf("function renderBoard()"), source.indexOf("function columnItems(")), context); + context.renderBoard(); + assert.deepEqual(rendered, [20, 30, 50, 10, 40]); + assert.deepEqual(items, original); + + items[0].column = "close"; + rendered.length = 0; + context.renderBoard(); + assert.deepEqual(rendered, [20, 30, 10, 40, 50]); + }); +}