Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 16 additions & 19 deletions src/renderer/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -3806,13 +3810,6 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
</div>
) : null}

{branchRows.length ? (
<div style={{ marginTop: 16, borderTop: '1px solid #f0f0f1', paddingTop: 16 }}>
<div style={{ fontWeight: 600, fontSize: 13, color: '#1d2327' }}>Other tickets on this site</div>
{renderBranchRows(true)}
</div>
) : null}

<div style={{ marginTop: 16, borderTop: '1px solid #f0f0f1', paddingTop: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<div style={{ fontWeight: 600, fontSize: 13, color: '#1d2327' }}>Linked pull requests</div>
Expand Down Expand Up @@ -3948,12 +3945,6 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
<div style={{ marginTop: 4, fontSize: 13, color: '#3c434a' }}>
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.
</div>
{branchRows.length ? (
<div style={{ marginTop: 12 }}>
<div style={{ fontWeight: 600, fontSize: 13, color: '#1d2327' }}>Your tickets on this site</div>
{renderBranchRows(false)}
</div>
) : null}
<div style={{ marginTop: 12, display: 'flex', alignItems: 'flex-start', gap: 8, flexWrap: 'wrap' }}>
<div style={{ minWidth: 260 }}>
<TextControl
Expand Down Expand Up @@ -3998,6 +3989,12 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
)}
</div>
) : null}
{skipInit && ticketsCard ? (
<div style={{ padding: 20, border: '1px solid #dcdcde', borderRadius: 12, background: '#fff' }}>
<div style={{ fontWeight: 600, fontSize: 16, color: '#1d2327' }}>{ticketsCard.heading}</div>
{renderBranchRows(Boolean(tracTicket))}
</div>
) : null}
{skipInit ? (
<div style={{ padding: 20, border: '1px solid #dcdcde', borderRadius: 12, background: '#fff' }}>
<div style={{ fontWeight: 600, fontSize: 16, color: '#1d2327' }}>Apply a patch or PR</div>
Expand Down
27 changes: 26 additions & 1 deletion src/renderer/ticket-branch-list.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
44 changes: 43 additions & 1 deletion test/ticket-branch-list.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"', () => {
Expand Down
Loading