Public: anyone can read this; the crew can change it.
The roadmap's linked rows carry nav, so j/k move through them and enter opens one #10667
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/10533-the-roadmap-s-linked-rows-carry-nav-so-j → main 2 commits fork
2 files
changed +44
−0
src/web/pages/roadmap.ts +2
| @@ -32,6 +32,7 @@ function needs(base: string, rows: readonly NeedsYou[]): Html | null { | ||
| 32 | 32 | ${rows.map((r) => |
| 33 | 33 | row({ |
| 34 | 34 | title: r.text, |
| 35 | nav: true, | |
| 35 | 36 | href: `${base}/${r.pull ? "pulls" : "issues"}/${r.number}`, |
| 36 | 37 | trail: html`${r.action}`, |
| 37 | 38 | }), |
| @@ -47,6 +48,7 @@ function issueRow(base: string, i: RoadmapIssue): Html { | ||
| 47 | 48 | return row({ |
| 48 | 49 | ...(i.assignee === null ? {} : { lead: avatar(i.assignee) }), |
| 49 | 50 | title: html`#${i.number} ${i.title}${i.moved ? html`<span class="rm-moved">moved</span>` : null}`, |
| 51 | nav: true, | |
| 50 | 52 | href: `${base}/${i.pull === undefined ? "issues" : "pulls"}/${i.pull ?? i.number}`, |
| 51 | 53 | ...(bits.length === 0 ? {} : { meta: html`${bits.join(" · ")}` }), |
| 52 | 54 | }); |
test/keys-roadmap.test.ts added +42
| @@ -0,0 +1,42 @@ | ||
| 1 | /** The roadmap's linked rows join the shell's j/k traversal (#10533): one data-nav per needs-row and issue-row, none on a pillar fold. */ | |
| 2 | import { expect, test } from "bun:test"; | |
| 3 | import { roadmapBody } from "../src/web/pages/roadmap.ts"; | |
| 4 | import type { Roadmap, RoadmapIssue, RoadmapPillar } from "../src/web/roadmap.ts"; | |
| 5 | ||
| 6 | const issue = (number: number, extra: Partial<RoadmapIssue> = {}): RoadmapIssue => ({ | |
| 7 | number, title: `Issue ${number}`, assignee: null, at: "2026-11-01T00:00:00Z", moved: false, ...extra, | |
| 8 | }); | |
| 9 | ||
| 10 | const pillar = (short: string, building: RoadmapIssue[], upNext: RoadmapIssue[]): RoadmapPillar => ({ | |
| 11 | short, name: `${short} Name`, umbrella: null, state: "current", done: 0, total: building.length + upNext.length, | |
| 12 | merged: 0, movedCount: 0, building, next: null, lastLanding: null, etaDays: null, etaAt: null, | |
| 13 | groups: { building, upNext, waiting: [], blocked: [], doneThisWeek: [] }, | |
| 14 | }); | |
| 15 | ||
| 16 | const road = (over: Partial<Roadmap>): Roadmap => ({ | |
| 17 | generatedAt: "2026-11-01T00:00:00Z", | |
| 18 | pace: { landed24h: 0, landed7d: 0, days: 7, current: null, etaDays: null, etaAt: null }, | |
| 19 | needsYou: [], | |
| 20 | pillars: [], | |
| 21 | ...over, | |
| 22 | }); | |
| 23 | ||
| 24 | const count = (s: string) => (s.match(/data-nav/g) ?? []).length; | |
| 25 | ||
| 26 | test("each needs-row and issue-row is one data-nav row; a pillar row is none", () => { | |
| 27 | const out = roadmapBody("/acme/forge", road({ | |
| 28 | needsYou: [ | |
| 29 | { kind: "hold", text: "Hold A", action: "Release", number: 5, pull: false }, | |
| 30 | { kind: "close", text: "Close B", action: "Close", number: 6, pull: true }, | |
| 31 | ], | |
| 32 | pillars: [pillar("P1", [issue(1), issue(2, { pull: 9 })], [issue(3)])], | |
| 33 | })).toString(); | |
| 34 | expect(count(out)).toBe(5); | |
| 35 | const pillarRow = out.split("<li").find((li) => li.includes('id="pillar-p1"'))!; | |
| 36 | expect(pillarRow.split("<li")[0]).not.toContain("data-nav"); | |
| 37 | expect(out).toContain("/acme/forge/pulls/6"); | |
| 38 | }); | |
| 39 | ||
| 40 | test("the empty page draws no data-nav row", () => { | |
| 41 | expect(count(roadmapBody("/acme/forge", road({})).toString())).toBe(0); | |
| 42 | }); |