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
This commit is contained in:
Aaron Powell
2026-10-14 11:37:36 +11:00
co-authored by Copilot App
parent a10206b7f6
commit 0f1ca9817a
5 changed files with 56 additions and 3 deletions
@@ -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,
@@ -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" },
+3 -1
View File
@@ -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,
@@ -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",
{
+45
View File
@@ -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]);
});
}