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}