diff --git a/src/patch-sources.cjs b/src/patch-sources.cjs
index 72c482f..489b5fe 100644
--- a/src/patch-sources.cjs
+++ b/src/patch-sources.cjs
@@ -71,6 +71,18 @@ function bodyCitesTicket(body, ticketId) {
return re.test(body);
}
+/**
+ * What happened to one pull request, from a `search/issues` item: open, merged
+ * or closed-unmerged.
+ *
+ * @param {Object} item
+ * @return {'open'|'merged'|'closed'}
+ */
+function prState(item) {
+ if (item.pull_request && item.pull_request.merged_at) return 'merged';
+ return item.state === 'closed' ? 'closed' : 'open';
+}
+
/**
* Reduces a GitHub `search/issues` response to the PRs that cite the ticket.
* Returns newest-first — for a moving target like a PR the freshest is the one
@@ -78,7 +90,7 @@ function bodyCitesTicket(body, ticketId) {
*
* @param {Object} searchJson
* @param {number|string} ticketId
- * @return {Array<{number: number, title: string, state: string, updatedAt: string, url: string}>}
+ * @return {Array<{number: number, title: string, state: 'open'|'merged'|'closed', updatedAt: string, url: string}>}
*/
function parseLinkedPrs(searchJson, ticketId) {
const items = searchJson && Array.isArray(searchJson.items) ? searchJson.items : [];
@@ -94,7 +106,12 @@ function parseLinkedPrs(searchJson, ticketId) {
prs.push({
number: item.number,
title: typeof item.title === 'string' ? item.title : '',
- state: item.state === 'closed' ? 'closed' : 'open',
+ // Merged is a third state, not a flavour of closed: `state` only ever
+ // says open or closed, and the merge shows in `pull_request.merged_at`
+ // — which this same search response already carries, so keeping the
+ // distinction costs no second request against the shared
+ // unauthenticated quota this file is careful with.
+ state: prState(item),
updatedAt: item.updated_at || item.created_at || '',
url: item.html_url || `https://github.com/WordPress/wordpress-develop/pull/${item.number}`
});
diff --git a/src/renderer/index.jsx b/src/renderer/index.jsx
index 08fe5be..bce936b 100644
--- a/src/renderer/index.jsx
+++ b/src/renderer/index.jsx
@@ -31,6 +31,7 @@ import { trunkAgeInfo, planUpdateSteps, updateStepStatuses, SKIP_INSTALL_MESSAGE
import { pickLatest } from '../latest-patch.cjs';
import { beginSetup, adoptSetupPath, discardSetup, rowPathAfterStatus } from './pending-setup.cjs';
import { parsePrRef } from '../patch-sources.cjs';
+import { prStateBadge } from './pr-state.cjs';
import { ticketUrl, attachUrl } from './trac-ticket.cjs';
import { ticketBranchRows, ticketListCard } from './ticket-branch-list.cjs';
import { describeSwitchProgress } from '../switch-progress.cjs';
@@ -2437,11 +2438,22 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
const patchAttachments = (tracAttachments?.items || []).filter((a) => a.applyable);
const tracAttachmentsRead = tracAttachments
&& (tracAttachments.status === 'ok' || tracAttachments.status === 'no-attachments');
+ // One pill shape, two uses: the "Latest" marker on a patch row and a linked
+ // pull request's state. Only the words and the colours differ.
+ const pillStyle = { display: 'inline-flex', alignItems: 'center', flex: '0 0 auto', padding: '1px 7px', borderRadius: 999, fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.04em' };
const latestPill = (isLatest) => (isLatest ? (
-
+
Latest
) : null);
+ const prStatePill = (state) => {
+ const badge = prStateBadge(state);
+ return (
+
+ {badge.label}
+
+ );
+ };
const finishApply = (message) => {
markTerminalRunning(false);
@@ -3850,8 +3862,9 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
{latestPill(latestPatch?.kind === 'pr' && latestPatch.key === pr.number)}
-
- {pr.state === 'closed' ? 'closed' : 'open'}{pr.updatedAt ? ` · updated ${new Date(pr.updatedAt).toLocaleDateString()}` : ''}
+
+ {prStatePill(pr.state)}
+ {pr.updatedAt ? updated {new Date(pr.updatedAt).toLocaleDateString()} : null}