diff --git a/src/renderer/index.jsx b/src/renderer/index.jsx index 31deddd..08fe5be 100644 --- a/src/renderer/index.jsx +++ b/src/renderer/index.jsx @@ -32,7 +32,7 @@ import { pickLatest } from '../latest-patch.cjs'; import { beginSetup, adoptSetupPath, discardSetup, rowPathAfterStatus } from './pending-setup.cjs'; import { parsePrRef } from '../patch-sources.cjs'; import { ticketUrl, attachUrl } from './trac-ticket.cjs'; -import { ticketBranchRows } from './ticket-branch-list.cjs'; +import { ticketBranchRows, ticketListCard } from './ticket-branch-list.cjs'; import { describeSwitchProgress } from '../switch-progress.cjs'; import { highlightDiff, hasDiffLines } from './diff-highlight.cjs'; import { carryTestMode } from './github-account.cjs'; @@ -2231,16 +2231,20 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit // eslint-disable-next-line no-alert -- see the note above onRename. const confirmAnd = async (m,a)=>{ if(window.confirm(m)) await a(); }; - // The tickets with work on this site (#108), rendered in both states of the - // Trac ticket panel. The sentence differs — an unlinked panel offers to - // continue, a linked one points out the other open tickets — but the rows, - // the ordering and the delete action are the same list. + // The tickets with work on this site (#108), in a card of their own (#240) + // between the Trac ticket card and the patch one — which ticket am I on, + // which of my tickets do I want, bring in work from elsewhere. The sentence + // differs with the state — with no ticket linked the rows offer to continue, + // with one linked they point out the other open tickets — but the rows, the + // ordering and the delete action are the same list, and it lives in the one + // card in both states rather than jumping somewhere else on unlink. // // Switch and delete are checkouts of the same working directory that an // install, a build or a trunk update is using, so they block on the long // operations as well as on each other — the same trio every destructive - // control in this panel guards on. + // control in the ticket panel guards on. const branchRows = ticketBranchRows({ branches: ticketBranches.branches, current: ticketBranches.current, tracTicket, now: Date.now() }); + const ticketsCard = ticketListCard({ rowCount: branchRows.length, linked: Boolean(tracTicket) }); // What the switch is doing, while it does it (#173). Gated on the busy flag // rather than merely cleared by it: the last sends can land after the invoke // has already answered, which would flash a sentence under an idle panel. @@ -3806,13 +3810,6 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit ) : null} - {branchRows.length ? ( -
-
Other tickets on this site
- {renderBranchRows(true)} -
- ) : null} -
Linked pull requests
@@ -3948,12 +3945,6 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
Tell the app which ticket you are working on. It is stored with the site, so it survives restarts, and you can change or remove it at any time.
- {branchRows.length ? ( -
-
Your tickets on this site
- {renderBranchRows(false)} -
- ) : null}
) : null} + {skipInit && ticketsCard ? ( +
+
{ticketsCard.heading}
+ {renderBranchRows(Boolean(tracTicket))} +
+ ) : null} {skipInit ? (
Apply a patch or PR
diff --git a/src/renderer/ticket-branch-list.cjs b/src/renderer/ticket-branch-list.cjs index c0de99b..1d4a382 100644 --- a/src/renderer/ticket-branch-list.cjs +++ b/src/renderer/ticket-branch-list.cjs @@ -96,7 +96,32 @@ function ticketBranchRows({ branches, current, tracTicket, now }) { })); } +/** + * Whether the site's tickets get a card of their own, and under what heading + * (#240). The list left the Trac ticket card because only one of its sections + * described the ticket in front of you — this one lists everywhere else you + * could be. Its heading still changes with the state: with a ticket linked the + * rows are the *other* tickets, with none linked they are *your* tickets and + * the primary way to start. + * + * Returns null when there are no rows — an empty card with nothing but a + * heading is worse than no card, and unlike the input field it used to share a + * card with, this card has nothing else to justify the space. + * + * @param {Object} input + * @param {number} input.rowCount How many rows ticketBranchRows produced. + * @param {boolean} input.linked Whether a ticket is linked to the site. + * @return {?{heading: string}} What the card says, or null for no card. + */ +function ticketListCard({ rowCount, linked }) { + if (!rowCount) return null; + return { + heading: linked ? 'Other tickets on this site' : 'Your tickets on this site' + }; +} + module.exports = { relativeTimeLabel, - ticketBranchRows + ticketBranchRows, + ticketListCard }; diff --git a/test/ticket-branch-list.test.cjs b/test/ticket-branch-list.test.cjs index ad1646e..3bad924 100644 --- a/test/ticket-branch-list.test.cjs +++ b/test/ticket-branch-list.test.cjs @@ -2,7 +2,9 @@ const test = require('node:test'); const assert = require('node:assert'); -const { relativeTimeLabel, ticketBranchRows } = require('../src/renderer/ticket-branch-list.cjs'); +const fs = require('node:fs'); +const path = require('node:path'); +const { relativeTimeLabel, ticketBranchRows, ticketListCard } = require('../src/renderer/ticket-branch-list.cjs'); const MINUTE_MS = 60 * 1000; const HOUR_MS = 60 * MINUTE_MS; @@ -117,6 +119,46 @@ test('rows: each row carries the ref to act on and the label to show', () => { assert.deepStrictEqual(rows, [{ ref: 'ticket/59234', ticketId: 59234, timeLabel: 'edited 2 days ago' }]); }); +// --- ticketListCard --------------------------------------------------------- + +test('card: the heading follows the state — other tickets when linked, your tickets when not (issue #240)', () => { + assert.deepStrictEqual(ticketListCard({ rowCount: 2, linked: true }), { heading: 'Other tickets on this site' }); + assert.deepStrictEqual(ticketListCard({ rowCount: 2, linked: false }), { heading: 'Your tickets on this site' }); +}); + +test('card: no rows means no card, not an empty one', () => { + assert.strictEqual(ticketListCard({ rowCount: 0, linked: true }), null); + assert.strictEqual(ticketListCard({ rowCount: 0, linked: false }), null); +}); + +// The card's position is the whole point of #240, and no test renders the +// DOM, so the layout is pinned at the source: the rows render once, from a +// card of their own that sits between the Trac ticket card and the patch one. +// Reading order is a behaviour here — which ticket am I on, which of my +// tickets do I want, bring in work from elsewhere. +test('card: the list renders once, in its own card between the ticket card and the patch card (issue #240)', () => { + const source = fs.readFileSync(path.join(__dirname, '..', 'src', 'renderer', 'index.jsx'), 'utf8'); + + // The headings come from ticketListCard, so the card cannot say one thing + // while the tested module says another. Restated literals in index.jsx + // would be the two-copies drift this module exists to prevent. + assert.ok(!source.includes('Other tickets on this site'), 'index.jsx restates the linked heading instead of using ticketListCard'); + assert.ok(!source.includes('Your tickets on this site'), 'index.jsx restates the unlinked heading instead of using ticketListCard'); + + // One call site. Two was the old shape — one per state of the ticket card — + // and going back to two is the list mounting twice, or quietly moving back + // inside the card it just left. Counted as a call rather than as the bare + // name so that a comment naming the helper is not a red suite. + assert.strictEqual(source.split('renderBranchRows(').length - 1, 1, 'expected exactly one renderBranchRows( call: the single card that renders the list'); + + // Between the two cards it used to sit inside of and above. + const ticketCard = source.indexOf('>Trac ticket<'); + const listCard = source.indexOf('{ticketsCard.heading}'); + const patchCard = source.indexOf('>Apply a patch or PR<'); + assert.ok(ticketCard !== -1 && listCard !== -1 && patchCard !== -1, 'one of the three card headings is missing from index.jsx'); + assert.ok(ticketCard < listCard && listCard < patchCard, 'the tickets card is not between the Trac ticket card and the patch card'); +}); + // --- relativeTimeLabel ------------------------------------------------------ test('time: under a minute is "just now"', () => {