Public: anyone can read this; the crew can change it.

The milestones page speaks the home page's language #9698

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/9693-the-milestones-page-speaks-the-home-page → main 3 commits fork 2 threads · 2 unresolved

  • Merged. scout/9693-the-milestones-page-speaks-the-home-page was merged into main as d702ab5.

Review

Sign in to review this change.
6 files changed +59 −45
docs/design/areas/milestones.md +9
@@ -16,3 +16,12 @@ unbroken word as the longest title that can exist; the estimate and trim notes
1616 are built from titles and counts, so no field takes 300. It is an attribute
1717 because the area adds no component class. Every preview of the forge takes
1818 `/preview-admin/sample/milestones` at desk and phone (`PREVIEW_OWNER_LOOKS`).
19
20 ## Rows under headings, not boxes (#9693)
21
22 The milestone list, and every section of an opened milestone (limbs, building
23 now, blocked, cost, merged, issues, re-planned), is a heading over plain rows:
24 a `ul.list.setting-list` with no `.card` around it, and a folded tail is a
25 `details` under the rows, not a card foot. How far it is stands as a plain
26 `#ms-progress` stack. No new component class; `limb.ts` and the re-plan page
27 share the same rows.

Principles 6/7: this says limb.ts and the re-plan page share the same rows, but limb.ts (order of the limbs) and limb_replan.ts (issues the planner is given) still draw a card around theirs. Either convert them or word this to say only the shared helpers changed.

src/web/pages/limb_view.ts +9 −12
@@ -4,7 +4,7 @@
44 */
55
66 import { html, type Html } from "../html.ts";
7 import { card, row } from "../ui.ts";
7 import { row } from "../ui.ts";
88
99 /** An issue as a row: its title, its number, and whatever is known of it. */
1010 export function issueRow(
@@ -25,18 +25,15 @@ export function issueRow(
2525 export const list = (rows: Html[], wrap = false): Html =>
2626 html`<ul class="list${wrap ? " setting-list" : ""}">${rows}</ul>`;
2727
28 /** A card of rows, wrapping. */
29 export const wrappedCard = (rows: Html[]): Html =>
30 card(list(rows, true), { pad: false });
28 /** Wrapping rows under a heading: no box around them. */
29 export const wrappedRows = (rows: Html[]): Html => list(rows, true);
3130
32 /** A card of wrapping rows that shows its first `shown` and folds the rest: a long title wraps on a phone rather than running past its edge. */
31 /** Wrapping rows that show the first `shown` and fold the rest: a long title wraps on a phone rather than running past its edge. */
3332 export function folded(rows: Html[], what: string, shown: number): Html {
34 if (rows.length <= shown) return wrappedCard(rows);
35 return card(list(rows.slice(0, shown), true), {
36 pad: false,
37 foot: html`<details>
38 <summary class="small">${rows.length - shown} more ${what}</summary>
33 if (rows.length <= shown) return wrappedRows(rows);
34 return html`${list(rows.slice(0, shown), true)}
35 <details>
36 <summary class="small muted">${rows.length - shown} more ${what}</summary>
3937 ${list(rows.slice(shown), true)}
40 </details>`,
41 });
38 </details>`;
4239 }
src/web/pages/milestones.ts +16 −22
@@ -59,8 +59,8 @@ import { html, when, type Html } from "../html.ts";
5959 import { field, readForm } from "../form.ts";
6060 import { page, redirect } from "../layout.ts";
6161 import { liveClientJs, liveMount } from "../live.ts";
62 import { badge, btn, card, pill, row, time } from "../ui.ts";
63 import { folded, issueRow, list, wrappedCard } from "./limb_view.ts";
62 import { badge, btn, pill, row, time } from "../ui.ts";
63 import { folded, issueRow, list, wrappedRows } from "./limb_view.ts";
6464
6565 /** Where a milestone is: its own page. */
6666 export const milestoneHref = milestonePath;
@@ -244,7 +244,7 @@ export function milestonesPage(ctx: Ctx): Response {
244244 show. <a href="${base}/map">See the map</a>.
245245 </p>`
246246 : html`<div id="ms-limbs">
247 ${wrappedCard(
247 ${wrappedRows(
248248 plan.limbs.map((limb) =>
249249 limbLine(base, limb, move?.(limb.issue, "top") ?? null),
250250 ),
@@ -325,7 +325,7 @@ function replanChanged(ctx: Ctx, repo: Repo, m: Milestone): Html | null {
325325 <span class="muted small">#${n.number}</span>`;
326326 return html`<section class="mt" id="ms-replanned">
327327 <h2 class="mb">Re-planned ${time(result.at)}</h2>
328 ${wrappedCard(
328 ${wrappedRows(
329329 result.splits.map((s) =>
330330 row({
331331 title: link(s),
@@ -409,14 +409,11 @@ function milestoneBody(
409409 <a href="${base}/milestones">Milestones</a>
410410 </p>
411411 <h1>${m.name}</h1>
412 ${card(
413 html`<div class="stack stack-tight">
414 <span><b>${w.done}</b> of ${w.total} done</span>
415 ${progress(w.done, w.total, `${w.done} of ${w.total} done`)}
416 <span class="muted small">${weightWords(m)}</span>
417 </div>`,
418 m.trim === undefined ? {} : { tone: "attn" },
419 )}
412 <div class="stack stack-tight" id="ms-progress">
413 <span><b>${w.done}</b> of ${w.total} done</span>
414 ${progress(w.done, w.total, `${w.done} of ${w.total} done`)}
415 <span class="muted small">${weightWords(m)}</span>
416 </div>
420417 <p id="ms-estimate">${m.estimate}</p>
421418 ${paceLine(m.pace)}

Principle 8/3: the progress stack dropped the card, which is right, but the attn tone that marked a trim candidate went with it. The p.small line below still carries the words; consider making that one line not muted so it reads as the one thing to act on.

422419 ${when(
@@ -435,7 +432,7 @@ function milestoneBody(
435432 () =>
436433 html`<section class="mt" id="ms-limbs">
437434 <h2 class="mb">Limbs</h2>
438 ${wrappedCard(
435 ${wrappedRows(
439436 m.limbs.map((limb) =>
440437 limbLine(base, limb, move?.(limb.issue, String(m.issue.number)) ?? null),
441438 ),
@@ -447,7 +444,7 @@ function milestoneBody(
447444 () =>
448445 html`<section class="mt" id="ms-building">
449446 <h2 class="mb">Building now</h2>
450 ${wrappedCard(
447 ${wrappedRows(
451448 m.building.map((b) =>
452449 issueRow(
453450 base,
@@ -499,7 +496,7 @@ function milestoneBody(
499496 </p>
500497 ${spend.costliest.length === 0
501498 ? html`<p class="muted small">Nothing has cost anything yet.</p>`
502 : wrappedCard(
499 : wrappedRows(
503500 spend.costliest.map((c) =>
504501 issueRow(base, c.issue, undefined, pill(dollars(c.usd))),
505502 ),
@@ -540,13 +537,10 @@ function milestoneBody(
540537 ${when(
541538 closed.length > 0,
542539 () =>
543 card(
544 html`<details>
545 <summary class="small">${closed.length} closed</summary>
546 ${list(closed.map((leaf) => leafRow(leaf, false)), true)}
547 </details>`,
548 { pad: false },
549 ),
540 html`<details>
541 <summary class="small muted">${closed.length} closed</summary>
542 ${list(closed.map((leaf) => leafRow(leaf, false)), true)}
543 </details>`,
550544 )}
551545 </section>`,
552546 )}`;
test/fixtures/milestones-golden.html +1 −10
@@ -6,14 +6,7 @@
66 </p>
77 </div>
88 <div id="ms-limbs">
9 <div
10 class="card"
11
12
13
14 >
15
16 <ul class="list setting-list"><li>
9 <ul class="list setting-list"><li>
1710 <div class="row"
1811
1912 >
@@ -990,6 +983,4 @@
990983 ></span
991984 ></span></div>
992985 </li></ul>
993
994 </div>
995986 </div>↵
test/flows-all.test.ts +2 −1
@@ -20,7 +20,8 @@ quietLogs();
2020 let root = "";
2121 let app: App;
2222 let servers: Servers;
23 const NOW = Date.parse("2026-11-08T12:00:00Z");
23 // The page reads the real clock, so the seed is offsets from it.
24 const NOW = Date.now();
2425
2526 beforeAll(async () => {
2627 root = mkdtempSync(join(tmpdir(), "forge-flows-all-"));
test/milestones-golden.test.ts +22
@@ -93,3 +93,25 @@ test("a guest is told nothing of a private repository's milestones", async () =>
9393 expect(res.status).toBe(404);
9494 expect(await res.text()).not.toContain("Milestone 0000");
9595 });
96
97 test("the list and an opened milestone are headings over rows, with no card box around them (#9693)", async () => {
98 const signin = await fetch(`${servers.url}/signin`, {
99 method: "POST",
100 headers: { origin: servers.url },
101 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
102 redirect: "manual",
103 });
104 const cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!;
105 const list = await (await fetch(`${servers.url}/alice/forge/milestones`, { headers: { cookie } })).text();
106 const main = list.slice(list.indexOf('id="ms-limbs"'));
107 expect(main).toContain('<ul class="list setting-list">');
108 const section = main.slice(0, main.search(/\n\s*<div\s+id="live"/));
109 expect(section).not.toContain('class="card');
110 const href = /href="(\/alice\/forge\/milestones\/\d+)"/.exec(list)?.[1];
111 expect(href).toBeDefined();
112 const one = await (await fetch(`${servers.url}${href}`, { headers: { cookie } })).text();
113 const body = one.slice(one.indexOf("<h1>"), one.search(/\n\s*<div\s+id="live"/));
114 expect(body).not.toMatch(/class="card[ "]/);
115 expect(body).toContain('id="ms-progress"');
116 expect(body).toContain('<ul class="list setting-list">');
117 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs