From 036c52b94eee8ce3812c6ef94ada44cac239afb8 Mon Sep 17 00:00:00 2001 From: JuanMa Date: Mon, 10 Aug 2026 15:38:01 +0200 Subject: [PATCH] Colour a linked pull request by its state, in GitHub's own colours MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The state rendered as grey text the same weight as the date beside it and separated by a middle dot, so the one word saying what happened to the work read as part of the timestamp. It now wears the pill the "Latest" marker already uses, in GitHub's three colours: green open, purple merged, red closed. Merged is a third state the parser used to throw away — GitHub's `state` only ever says open or closed, and the merge shows in `pull_request.merged_at`, which the search response the app already fetches carries. Keeping it costs no second request against the shared unauthenticated quota. The colour accompanies the word, never replaces it, and the closed pill is GitHub's red rather than this app's error pair: a closed pull request is not a failure, so it must not be dressed as one. Part of #227. Co-Authored-By: Claude Opus 5 (1M context) --- src/patch-sources.cjs | 21 +++++++++-- src/renderer/index.jsx | 19 ++++++++-- src/renderer/pr-state.cjs | 45 ++++++++++++++++++++++++ test/patch-sources.test.cjs | 25 ++++++++++++++ test/pr-state.test.cjs | 69 +++++++++++++++++++++++++++++++++++++ 5 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 src/renderer/pr-state.cjs create mode 100644 test/pr-state.test.cjs 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}