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

The plan map at /:owner/:name/map speaks the home page's language #9767

Merged opened this

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

Merged

scout/forge:scout/9760-the-plan-map-at-owner-name-map-speaks-th → main 4 commits fork 1 thread · 1 unresolved

  • Merged. scout/9760-the-plan-map-at-owner-name-map-speaks-th was merged into main as a538bdd.

Review

Sign in to review this change.
4 files changed +61 −15
docs/design/areas/map.md +8
@@ -27,3 +27,11 @@ is `data-state` on `.map-node`, not a class.
2727 - `.map-ask` — a pillar row's lead-time figures in the side column, inline, with the hover title saying what they measure.
2828
2929 The Ready now list's **First** button (a writer's `method="post"` form in the row's trail) and its "first, by <who>" meta line add no class: they are `row`'s `trail` and `meta` and the outline `btn`. The list is `.setting-list`, so the meta wraps and under 640px the trail drops to its own line.
30
31 ## Rows and space, not boxes (#9760)
32
33 The side column is two headed lists, Ready now first, then By pillar and Blocked on: each a `.card.bare` list, one row an
34 issue on the page's one background, no frame and no badge. A row says its word (ready, blocked, waiting, held, parked), what
35 stands in front of it (`waits for N`) and what it holds up; a pillar row says its counts as muted text. `.map-canvas` has
36 no border: the drawing sits on the page. The First button, its same-origin POST form and the 390px single column are
37 unchanged. No new class.
src/web/pages/map.ts +20 −14
@@ -51,7 +51,7 @@ import {
5151 import { liveRegionsJs } from "../client.ts";
5252 import { html, type Html } from "../html.ts";
5353 import { page } from "../layout.ts";
54 import { badge, btn, card, empty, row } from "../ui.ts";
54 import { btn, card, empty, row } from "../ui.ts";
5555 import {
5656 parkedTwice,
5757 struckOut,
@@ -989,7 +989,7 @@ function rankedList(
989989 }),
990990 )}
991991 </ul>`,
992 { pad: false },
992 { pad: false, extra: "bare" },
993993 )}
994994 </section>`;
995995 }
@@ -1093,7 +1093,7 @@ function pillarList(repo: Repo, rows: readonly PillarRow[]): Html {
10931093 >#${i.number}</a
10941094 >${" "}`,
10951095 )}${(r.issues ?? []).length > 8 ? "…" : ""}`,
1096 trail: badge(counts(r)),
1096 trail: html`<span class="muted small">${counts(r)}</span>`,
10971097 data: { pillar: "" },
10981098 })
10991099 : row({
@@ -1114,12 +1114,12 @@ function pillarList(repo: Repo, rows: readonly PillarRow[]): Html {
11141114 >
11151115 ${hours(r.askToLive.slowest.hours)}`}</span
11161116 >`,
1117 trail: badge(counts(r)),
1117 trail: html`<span class="muted small">${counts(r)}</span>`,
11181118 data: { pillar: r.name },
11191119 }),
11201120 )}
11211121 </ul>`,
1122 { pad: false },
1122 { pad: false, extra: "bare" },
11231123 )}
11241124 </section>`;
11251125 }
@@ -1130,7 +1130,7 @@ export const mapCss = /* css */ `
11301130 .map-main { min-width: 0; }
11311131
11321132 /* ---- the canvas ---- */
1133 .map-canvas { position: relative; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); touch-action: pan-y; user-select: none; -webkit-user-select: none; }
1133 .map-canvas { position: relative; overflow: hidden; border: 0; border-radius: 0; background: var(--bg); touch-action: pan-y; user-select: none; -webkit-user-select: none; }
11341134 .map-canvas.is-view { cursor: grab; }
11351135 .map-canvas.is-drag { cursor: grabbing; }
11361136 .map-canvas.is-cut::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 36px; background: linear-gradient(to bottom, transparent, var(--bg)); pointer-events: none; }
@@ -1418,6 +1418,11 @@ export async function mapPage(ctx: Ctx): Promise<Response> {
14181418 // still noted, by hand, as their total, for readers of the header.
14191419 notePhase(rq, "map", performance.now() - reading);
14201420 const rendering = performance.now();
1421 /** A row's pillar, as the words lead with it: "Name · ", or nothing. */
1422 const pillarOf = (r: Ranked): string => {
1423 const name = pillars?.of.get(r.issue.id);
1424 return name === undefined || name === null ? "" : `${name} · `;
1425 };
14211426 const body = html`${repoHeader(ctx, repo, "map")}
14221427 <div class="page-head mt">
14231428 <h1>Map</h1>
@@ -1444,12 +1449,6 @@ export async function mapPage(ctx: Ctx): Promise<Response> {
14441449 ${placed.nodes.length === 0 && counts.done === 0 ? null : countsLine(counts, { base: `${repoBase(repo)}/map`, showDone })}
14451450 </div>
14461451 <aside class="stack">
1447 ${pillars === undefined
1448 ? null
1449 : pillarList(
1450 repo,
1451 pillarRows(repo, graph, readySet, marks, pillars, ask),
1452 )}
14531452 ${graph.issues.length === 0
14541453 ? null
14551454 : rankedList(
@@ -1457,7 +1456,7 @@ export async function mapPage(ctx: Ctx): Promise<Response> {
14571456 "Ready now",
14581457 ready,
14591458 (r) =>
1460 `${graph.flow?.has(r.issue.id) ? "flow · " : ""}${
1459 `ready · ${pillarOf(r)}${graph.flow?.has(r.issue.id) ? "flow · " : ""}${
14611460 r.holdsUp === 0
14621461 ? "holds nothing up"
14631462 : `finishing it frees ${r.holdsUp}`
@@ -1472,11 +1471,18 @@ export async function mapPage(ctx: Ctx): Promise<Response> {
14721471 by: new Map([...firstMarksInRepo(db, repo.id)].map(([id, m]) => [id, m.by])),
14731472 },
14741473 )}
1474 ${pillars === undefined
1475 ? null
1476 : pillarList(
1477 repo,
1478 pillarRows(repo, graph, readySet, marks, pillars, ask),
1479 )}
14751480 ${rankedList(
14761481 repo,
14771482 "Blocked on",
14781483 blocked,
1479 (r) => `holds up ${r.holdsUp}`,
1484 (r) =>
1485 `${wordFor(r.issue, readySet, marks)} · ${pillarOf(r)}${r.waitsFor > 0 ? `waits for ${r.waitsFor} · ` : ""}holds up ${r.holdsUp}`,
14801486 "Nothing open holds anything up.",
14811487 tellHref,
14821488 )}
test/map-page.test.ts +27
@@ -92,3 +92,30 @@ test("?done=1 draws the closed issues and offers Open only", async () => {
9292 expect(body).toMatch(/<a\s+class="map-toggle"\s+href="\/alice\/big\/map"\s*>Open only/);
9393 expect(body).not.toContain("Show done");
9494 });
95
96 test("the map's lists are rows under headings, not boxed cards, ready first, nothing lost", async () => {
97 const body = await get("/alice/big/map");
98 const aside = body.slice(body.indexOf("<aside"), body.indexOf("</aside>"));
99 expect(aside).not.toMatch(/class="card(?![^"]*\bbare\b)[^"]*"/);
100 expect(aside).not.toContain('class="badge"');
101 expect(aside.indexOf("<h2>Ready now</h2>")).toBeGreaterThan(-1);
102 expect(aside.indexOf("<h2>Ready now</h2>")).toBeLessThan(aside.indexOf("<h2>Blocked on</h2>"));
103 expect(aside).toMatch(/ready · (holds nothing up|finishing it frees)/);
104 expect(aside).toContain('method="post"');
105 const rule = body.match(/\.map-canvas \{([^}]*)\}/);
106 expect(rule).not.toBeNull();
107 expect(rule![1]).not.toMatch(/border:\s*1px/);
108 });
109
110 test("a Blocked-on row says its word and what it holds up, and says nothing of waiting when it waits for nothing", async () => {
111 const alice = { ...(await import("../src/core/users.ts")).getUserByName(app.db, "alice")! };
112 const repo = (await import("../src/core/repos.ts")).getRepo(app.db, "alice", "big")!;
113 const a = await openIssue(app, alice, repo, { title: "Front one" });
114 const b = await openIssue(app, alice, repo, { title: "Behind one" });
115 if (!a.ok || !b.ok) throw new Error("open");
116 addDependency(app.db, b.value, a.value, alice.id);
117 const body = await get("/alice/big/map");
118 const blockedRows = body.slice(body.indexOf("<h2>Blocked on</h2>"));
119 expect(blockedRows.slice(0, 600)).toContain("ready · holds up 1");
120 expect(blockedRows.slice(0, 600)).not.toContain("waits for");
121 });
test/pillars.test.ts +6 −1
@@ -401,7 +401,12 @@ describe("pillars", () => {
401401 );
402402 expect(page).toContain("3 open · 2 ready · 1 blocked");
403403 expect(page).toMatch(/class="row-title"\s*>no pillar<\/span/);
404 expect(page).toMatch(/class="badge"\s*>3 open · 2 ready · 1 blocked<\/span/);
404 expect(page).toMatch(/class="muted small"\s*>3 open · 2 ready · 1 blocked<\/span/);
405 const pillarsBlock = page.slice(page.indexOf('id="map-pillars"'), page.indexOf("</section>", page.indexOf('id="map-pillars"')));
406 expect(pillarsBlock).not.toContain('class="badge"');
407 // A Ready now row leads with its pillar's name.
408 const readyBlock = page.slice(page.indexOf("<h2>Ready now</h2>"), page.indexOf("</section>", page.indexOf("<h2>Ready now</h2>")));
409 expect(readyBlock).toMatch(/#\d+ · ready · P[01] [A-Za-z]+ · /);
405410 // §5's rows draw the section; the old names are gone (#4292).
406411 expect(page).not.toMatch(/class="[^"]*\bmap-list\b/);
407412 expect(page).not.toMatch(/class="[^"]*\bmap-empty\b/);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs