fix(canvases): dismiss previews and enable PR card dragging

Close review board previews after successful item actions and outside clicks without dismissing a different item during in-flight requests. Explicitly enable native dragging on editable PR cards and add regression coverage.

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:25:57 +11:00
co-authored by Copilot App
parent 764b3829bf
commit a10206b7f6
6 changed files with 283 additions and 9 deletions
@@ -26,6 +26,8 @@ Open it by asking Copilot to "open the plugin review board" (canvas id `external
Fast-track never overrides an existing review or a bucket you chose; drag a card to *Unreviewed* to opt it
out and have it reviewed normally. Cards show *First party* / *Third party* pills from the intake labels.
- **Details panel** shows the AI review, the rendered issue body, and all comments.
Clicking outside the panel or successfully submitting an item action closes it;
failed actions leave it open so you can correct or retry them.
- **Quick actions** post `/approve` (with an optional note) or `/reject <reason>` straight to the issue
after a confirm click. The reject reason is prefilled from the AI's suggested comment.
- **Column actions** approve, reject, or re-review every eligible item in a column, including
@@ -60,3 +62,4 @@ Run `node --experimental-vm-modules --test eng/board-state-storage.test.mjs` fro
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,
queue exclusions, and partial failures using in-process fakes.
Run `node --test eng/board-preview.test.mjs` to check preview dismissal and in-flight actions.
@@ -261,6 +261,7 @@ async function bulkAction(columnId, kind) {
}
await api("POST", "/api/decision", { ...item, kind });
completed++;
if (selected === item.number) closeDrawer();
} catch (error) {
failures.push(`#${item.number}: ${error.message}`);
}
@@ -343,6 +344,7 @@ async function moveItem(number, column) {
const hadReview = column === "unreviewed" && Boolean(findItem(number)?.review);
try {
await api("POST", "/api/move", { number, column });
if (selected === number) closeDrawer();
if (hadReview) toast(`Cleared the AI review for #${number}; it will be included in the next review.`);
} catch (error) {
toast(error.message, { error: true });
@@ -616,10 +618,11 @@ async function decide(kind) {
: `Posted ${result.body.split("\n")[0]} on #${item.number}`,
{ link: result.commentUrl },
);
if (selected === item.number) closeDrawer();
} catch (error) {
toast(error.message, { error: true });
} finally {
resetConfirm();
if (selected === item.number) resetConfirm();
}
}
@@ -672,6 +675,9 @@ ui.search.addEventListener("input", () => {
});
ui.drawerClose.addEventListener("click", closeDrawer);
document.addEventListener("pointerdown", (event) => {
if (selected !== null && !ui.drawer.contains(event.target)) closeDrawer();
}, true);
ui.drawerMove.addEventListener("change", () => moveItem(selected, ui.drawerMove.value));
function hideRereview() {
ui.rereviewPanel.hidden = true;
@@ -711,7 +717,7 @@ ui.rereviewPanel.addEventListener("submit", async (event) => {
try {
const result = await api("POST", "/api/rereview", { number, guidance: ui.rereviewGuidance.value });
toast(result.message);
hideRereview();
if (selected === number) closeDrawer();
} catch (error) {
toast(error.message, { error: true });
} finally {
@@ -32,6 +32,8 @@ authenticated `gh` CLI.
- **Detail drawer** — AI review, description, conversation, changed files, and checks.
Keyboard focus stays inside the modal drawer and returns to its card on close.
Use Left/Right arrows or Home/End to switch detail tabs.
Clicking outside the drawer or successfully submitting an item action closes it;
failed actions leave it open so you can correct or retry them.
- **GitHub actions** — submit feedback, request changes, close a PR, or approve it and
enable auto-merge with a merge commit. Each action needs two clicks to confirm and
is also recorded in local board history. Approval is pinned to the board's commit
@@ -92,5 +94,7 @@ Run `node --test eng/open-pr-board.test.mjs` from the repository root. GitHub wr
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,
queue exclusions, and partial failures using in-process fakes.
Run `node --test eng/board-preview.test.mjs` to check preview dismissal and in-flight actions.
Run `node --test eng/open-pr-board-drag.test.mjs` to check native card dragging and transfer data.
Run `node --experimental-vm-modules --test eng/board-state-storage.test.mjs` to check
both canvases' session-scoped persistence without accessing GitHub.
+16 -7
View File
@@ -241,7 +241,7 @@ function renderCard(item) {
"article",
{
class: item.number === selectedNumber ? "card selected" : "card",
draggable: !item.decision,
draggable: item.decision ? "false" : "true",
dataset: { number: String(item.number) },
tabindex: "0",
role: "button",
@@ -402,6 +402,7 @@ async function bulkAction(columnId, kind) {
method: "POST", body: { number: item.number, kind, comment: item.comment },
});
completed++;
if (selectedNumber === item.number) closeDrawer();
if (result.warning) failures.push(`#${item.number}: ${result.warning}`);
} catch (error) {
failures.push(`#${item.number}: ${error.message}`);
@@ -436,12 +437,14 @@ function reviewTab(item) {
{
class: "btn btn-small btn-primary",
type: "button",
onclick: (event) =>
run(
onclick: async (event) => {
const result = await run(
event.currentTarget,
() => api("/api/review", { method: "POST", body: { numbers: [item.number] } }),
(result) => result.message ?? "Review requested.",
),
);
if (result && selectedNumber === item.number) closeDrawer();
},
},
"Review this PR",
),
@@ -728,12 +731,12 @@ async function submitDecision(button, kind) {
api("/api/decision", { method: "POST", body: { number: item.number, kind, comment } }),
);
decisionsInFlight.delete(item.number);
renderDrawerHeader();
if (selectedNumber === item.number) renderDrawerHeader();
button.textContent = DECISION_BUTTONS[kind];
if (result) {
if (result.warning) toast(result.warning, { error: true, url: result.url });
else toast(`#${item.number}: ${result.status}.`, { url: result.url });
if (currentItem()) loadDetail(item.number, { force: true });
if (selectedNumber === item.number) closeDrawer();
}
}
@@ -778,6 +781,10 @@ el.review.addEventListener("click", (event) =>
el.drawerClose.addEventListener("click", closeDrawer);
document.addEventListener("pointerdown", (event) => {
if (selectedNumber !== null && !el.drawer.contains(event.target)) closeDrawer();
}, true);
document.addEventListener("keydown", (event) => {
if (!el.drawer.classList.contains("open")) return;
if (event.key === "Escape") {
@@ -806,6 +813,7 @@ el.drawerColumn.addEventListener("change", async (event) => {
if (column === item.column) return;
try {
await api("/api/move", { method: "POST", body: { number: item.number, column } });
if (selectedNumber === item.number) closeDrawer();
} catch (error) {
toast(error.message, { error: true });
event.target.value = item.column;
@@ -855,9 +863,10 @@ el.rereviewStart.addEventListener("click", async (event) => {
}),
(value) => value.message ?? "Re-review requested.",
);
if (result) {
if (result && selectedNumber === item.number) {
el.rereviewGuidance.value = "";
el.rereviewPanel.hidden = true;
closeDrawer();
}
});