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

Settings › Labels speaks the home page's language #9809

Merged opened this

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

Merged

scout/forge:scout/9747-settings-labels-speaks-the-home-page-s-l → main 2 commits fork

  • Merged. scout/9747-settings-labels-speaks-the-home-page-s-l was merged into main as 69ab45f.

Review

Sign in to review this change.
4 files changed +66 −31
docs/design/areas/settings.md +4 −5
@@ -22,12 +22,11 @@ the text wrap and gives the control a place that is always visible. Use `.row-ac
2222
2323 ## Labels
2424
25 - `.preview` — the label chip at the head of a label's edit row on a repository's Labels settings, kept from
26 shrinking. Written by `labelEditor()` in `src/web/pages/labels.ts`; styled in `src/web/style/components.ts` as
27 `.edit-row > .preview`.
25 Each label is a `row()` in a `.setting-list`: its chip is the title, `N open` the meta, and a `.row-fields` form (name,
26 colour, Save, Delete) the trail — `labelRow()` in `src/web/pages/labels.ts`. The new-label form sits in a `.settings-block`.
2827
29 - `.label-open` — a label's `N open` link beside its chip in `labelEditor()` (`unused` stays a `.muted` span). Underlined so it
30 reads as a link; at the phone breakpoint a 44px tap target. Styled under `.edit-row` in `src/web/style/components.ts`.
28 - `.label-open` — a label's `N open` link in its row's meta (`unused` stays a `.muted` span). Underlined so it
29 reads as a link; at the phone breakpoint a 44px tap target. Styled in `src/web/style/components.ts`.
3130
3231 The terminal's Labels screen (`src/tui/screens/labels.ts`) shows the same counts to a repository admin: `N open` or `unused` after each hex, and its delete confirm says how many open issues and pulls carry the label. A reader sees neither.
3332
src/web/pages/labels.ts +27 −22
@@ -14,7 +14,7 @@ import { repoBase, repoNotFound } from "../chrome.ts";
1414 import { field, type Form } from "../form.ts";
1515 import { html, type Html } from "../html.ts";
1616 import { flashTo } from "../layout.ts";
17 import { btn, card, confirmAction, empty, labelChip, labelCss } from "../ui.ts";
17 import { btn, confirmAction, empty, labelChip, labelCss, row } from "../ui.ts";
1818 import { adminForm, settingsPageResponse, timedAdminRepo } from "./settings.ts";
1919
2020 /** `<input type="color">` submits `#rrggbb`; the store keeps six hex digits. */
@@ -46,21 +46,23 @@ function usage(base: string, label: Label, open: number): Html {
4646 : html`<span class="muted">unused</span>`;
4747 }
4848
49 function labelEditor(base: string, label: Label, open: number): Html {
50 return html`<li>
51 <form
52 class="edit-row"
49 /** One label: its chip and open count at the head, the edit form in the trail. */
50 function labelRow(base: string, label: Label, open: number): Html {
51 return row({
52 id: `label-${label.id}`,
53 title: labelChip(label),
54 meta: usage(base, label, open),
55 trail: html`<form
56 class="row-fields edit-row"
5357 method="post"
5458 action="${base}/settings/labels/${label.id}"
5559 >
56 <span class="preview">${labelChip(label)}</span>
57 ${usage(base, label, open)}
5860 <input
5961 class="input"
6062 type="text"
6163 name="name"
6264 value="${label.name}"
63 aria-label="Label name"
65 aria-label="Name of the label ${label.name}"
6466 required
6567 />
6668 ${colorField(label.color, `Colour of the label ${label.name}`)}
@@ -69,9 +71,10 @@ function labelEditor(base: string, label: Label, open: number): Html {
6971 action: `${base}/settings/labels/${label.id}/delete`,
7072 verb: "Delete",
7173 size: "sm",
74 label: `Delete the label ${label.name}`,
7275 })}
73 </form>
74 </li>`;
76 </form>`,
77 });
7578 }
7679
7780 export function labelsPage(ctx: Ctx): Response {
@@ -89,14 +92,17 @@ export function labelsPage(ctx: Ctx): Response {
8992 title: "No labels yet",
9093 note: "Labels are this repository's work queue — a handful is usually enough.",
9194 })
92 : html`<div class="card">
93 <ul class="list">
94 ${labels.map((l) => labelEditor(base, l, counts.get(l.id) ?? 0))}
95 </ul>
96 </div>`;
95 : html`<ul class="list setting-list">
96 ${labels.map((l) => labelRow(base, l, counts.get(l.id) ?? 0))}
97 </ul>`;
9798
98 const create = card(
99 html`<form class="edit-row" method="post" action="${base}/settings/labels">
99 const create = html`<section class="settings-block">
100 <h2>New label</h2>
101 <form
102 class="row-fields edit-row mt"
103 method="post"
104 action="${base}/settings/labels"
105 >
100106 <input
101107 class="input"
102108 type="text"
@@ -107,12 +113,11 @@ export function labelsPage(ctx: Ctx): Response {
107113 />
108114 ${colorField("6b7280", "Colour of the new label")}
109115 ${btn("Add label", { variant: "primary", size: "sm" })}
110 </form>`,
111 { head: html`<h2>New label</h2>`, pad: false },
112 );
116 </form>
117 </section>`;
113118
114 const body = html`${list}
115 <div class="mt">${create}</div>`;
119 const body = html`<section class="settings-block">${list}</section>
120 ${create}`;
116121 const response = settingsPageResponse(
117122 ctx,
118123 repo,
src/web/style/components.ts +4 −4
@@ -524,11 +524,11 @@ details[open] > summary.row > .row-lead { transform: rotate(90deg); }
524524 display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
525525 padding: 8px 14px;
526526 }
527 .edit-row > .preview { flex: none; }
527 .row-trail > .edit-row, .settings-block > .edit-row { padding: 0; }
528528 .edit-row > .input { flex: 1 1 160px; width: auto; min-width: 0; }
529529 .edit-row .btn { flex: none; }
530 .edit-row .label-open { color: var(--muted-fg); font-size: 13px; text-decoration: underline; text-underline-offset: 2px; }
531 .edit-row .label-open:hover { color: var(--fg); }
530 .label-open { color: var(--muted-fg); font-size: 13px; text-decoration: underline; text-underline-offset: 2px; }
531 .label-open:hover { color: var(--fg); }
532532 /* A swatch and its hex, as one control-shaped thing. */
533533 .color-field {
534534 display: inline-flex; align-items: center; gap: 8px; flex: none;
@@ -550,7 +550,7 @@ details[open] > summary.row > .row-lead { transform: rotate(90deg); }
550550 .edit-row { align-items: stretch; }
551551 .edit-row > .input { flex-basis: 100%; }
552552 .edit-row .color-field, .edit-row .btn { flex: 1 1 auto; justify-content: center; }
553 .edit-row .label-open { display: inline-flex; align-items: center; min-height: 44px; padding: 0 8px; }
553 .label-open { display: inline-flex; align-items: center; min-height: 44px; padding: 0 8px; }
554554 }
555555
556556 /* ---- table ---- */
test/labels-page-counts.test.ts +31
@@ -94,3 +94,34 @@ describe("labels page counts", () => {
9494 expect(mcp.value.items.find((l: any) => l.id === bugId).open).toBe(2);
9595 });
9696 });
97
98 describe("labels page rows", () => {
99 test("each label is a setting-list row with a plain POST form and a confirm link", async () => {
100 const dir = await mkdtemp(join(tmpdir(), "forge-label-rows-"));
101 const app = createApp({ dataDir: dir, port: 0 });
102 const servers = await serve(app);
103 try {
104 const cookie = await adminCookie(app, { username: "olive", email: "[email protected]", password: "label-rows-password" });
105 await fetch(`${servers.url}/new`, {
106 method: "POST",
107 headers: { cookie },
108 body: new URLSearchParams({ name: "proj", visibility: "private", description: "" }),
109 redirect: "manual",
110 });
111 const repo = app.db.query<{ id: string }, []>("SELECT id FROM repo").get()!;
112 const made = createLabel(app.db, repo.id, "bug", "d73a4a");
113 if (!made.ok) throw new Error("label");
114 const text = await (await fetch(`${servers.url}/olive/proj/settings/labels`, { headers: { cookie } })).text();
115 expect(text).toContain("setting-list");
116 expect(text).toContain(`id="label-${made.value.id}"`);
117 expect(text).toContain(`action="/olive/proj/settings/labels/${made.value.id}"`);
118 expect(text).toContain(`value="#d73a4a"`);
119 expect(text).toContain("/-/confirm?to=");
120 expect(text).not.toContain('class="card"><ul');
121 } finally {
122 servers.stop();
123 app.close();
124 await rm(dir, { recursive: true, force: true });
125 }
126 }, 30_000);
127 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs