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

The /forges list, its fresh-link page and its remove confirm speak the home page's language #9805

Merged opened this

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

Merged

mason/forge:mason/9798-the-forges-list-its-fresh-link-page-and → main 2 commits fork

  • Merged. mason/9798-the-forges-list-its-fresh-link-page-and was merged into main as 0e50ffc.

Review

Sign in to review this change.
4 files changed +99 −22
docs/design/areas/lists.md +2
@@ -52,3 +52,5 @@ Disks and network, Processes, Main-thread stalls, Telemetry readers, Code page c
5252 `<section class="settings-block">` with one `<h2>`; the lines are `row()`s in a `.list`, the wide figures stay tables. With
5353 nothing to show a part says so in one plain sentence. The live regions (`summary`, `cores`, `mem`, `temps`, `sys-disks`,
5454 `sys-procs`, …) keep their ids, so `system-live.ts` still patches them. `test/system-parts.test.ts` holds the shape.
55
56 `/forges` (#9798) speaks the home page's language and adds no class: "Side by side" and "Forges" are each a `<section class="settings-block">` under an `<h2>` (the list is the same `row()`s in a `.list.setting-list`); with none sent, one headed section ("No forges sent yet") says so in a plain sentence with the New forge button. The fresh-link page, the remove confirm and the stop/remove receipts are a `.settings-block` too, no `.card`; the claim link (`claimLinkBody`) is a bare row list. Those pages stay `cache-control: no-store`. `test/forges-language.test.ts` holds the shape.
src/web/pages/forges.ts +29 −20
@@ -14,7 +14,7 @@ import { html, type Html } from "../html.ts";
1414 import { page } from "../layout.ts";
1515 import { reissueDoor } from "../../spawn/door.ts";
1616 import { readForm } from "../form.ts";
17 import { badge, btn, card, empty, row, time } from "../ui.ts";
17 import { badge, btn, row, time } from "../ui.ts";
1818 import { noRoom } from "../../spawn/door.ts";
1919 import { sizeWords, useWords } from "../../spawn/usage.ts";
2020 import { listChildren } from "../../actions/spawn.ts";
@@ -148,18 +148,21 @@ export async function forgesPage(ctx: Ctx): Promise<Response> {
148148 actions: action,
149149 })}
150150 ${rows.length === 0
151 ? empty({
152 icon: "fork",
153 title: "No forges sent yet",
154 note: "A whole forge for someone: the apps you choose, a crew, and one link to send.",
155 action,
156 })
157 : html`<section id="forges-compare">${sideBySide(rows)}</section>`}
158 ${rows.length === 0
159 ? null
160 : html`<ul class="list setting-list">
161 ${rows.map(forgeRow)}
162 </ul>`}
151 ? html`<section class="settings-block">
152 <h2>No forges sent yet</h2>
153 <p class="muted">A whole forge for someone: the apps you choose, a crew, and one link to send.</p>
154 <p>${action}</p>
155 </section>`
156 : html`<section class="settings-block" id="forges-compare">
157 <h2>Side by side</h2>
158 ${sideBySide(rows)}
159 </section>
160 <section class="settings-block">
161 <h2>Forges</h2>
162 <ul class="list setting-list">
163 ${rows.map(forgeRow)}
164 </ul>
165 </section>`}
163166 <p class="mt"><a href="/fleet/graph">Fleet graph</a></p>
164167 </div>`,
165168 });
@@ -183,12 +186,12 @@ export async function forgeLinkSubmit(ctx: Ctx): Promise<Response> {
183186 title: "A fresh link",
184187 body: html`<div class="narrow-wide" data-forges>
185188 ${pageHead(typeof link === "string" ? `A fresh link for ${name}` : `No fresh link for ${name}`)}
186 ${card(
187 typeof link === "string"
189 <section class="settings-block">
190 ${typeof link === "string"
188191 ? claimLinkBody(link)
189192 : html`<p>${String(door.body["message"] ?? "")}</p>
190 <p class="mt">${btn("Forges you sent", { href: "/forges", size: "sm" })}</p>`,
191 )}
193 <p class="mt">${btn("Forges you sent", { href: "/forges", size: "sm" })}</p>`}
194 </section>
192195 </div>`,
193196 },
194197 door.status,
@@ -211,7 +214,10 @@ function manageSubmit(act: (app: Ctx["app"], name: string) => Promise<Managed>,
211214 title,
212215 body: html`<div class="narrow-wide" data-forges>
213216 ${pageHead(title)}
214 ${card(html`<p role="status">${done.says}</p><p class="mt">${btn("Back to your forges", { href: "/forges", size: "sm" })}</p>`)}
217 <section class="settings-block">
218 <p role="status">${done.says}</p>
219 <p class="mt">${btn("Back to your forges", { href: "/forges", size: "sm" })}</p>
220 </section>
215221 </div>`,
216222 },
217223 done.ok ? 200 : 404,
@@ -235,11 +241,14 @@ export async function forgeRemoveConfirm(ctx: Ctx): Promise<Response> {
235241 title: `Remove ${name}?`,
236242 body: html`<div class="narrow-wide" data-forges>
237243 ${pageHead(`Remove ${name}?`)}
238 ${card(html`<p>This deletes the forge <strong>${name}</strong> at ${child.url.replace(/^https?:\/\//, "")} with everything in it: its repositories, issues, accounts and settings. It cannot be undone.</p>
244 <section class="settings-block">
245 <h2>What removing deletes</h2>
246 <p>This deletes the forge <strong>${name}</strong> at ${child.url.replace(/^https?:\/\//, "")} with everything in it: its repositories, issues, accounts and settings. It cannot be undone.</p>
239247 <form method="post" action="/forges/${name}/remove" class="mt">
240248 ${btn("Remove it for good", { variant: "primary" })}
241249 ${btn("Keep it", { href: "/forges" })}
242 </form>`)}
250 </form>
251 </section>
243252 </div>`,
244253 });
245254 response.headers.set("cache-control", "no-store");
src/web/pages/new-forge.ts +2 −2
@@ -381,12 +381,12 @@ export function claimLinkBody(link: string, note?: string): Html {
381381 <p class="muted small mb">
382382 Send it as you would a password: to them alone, not to a group.
383383 </p>
384 ${card(html`<ul class="list setting-list">${
384 <ul class="list setting-list">${
385385 row({
386386 title: html`<code class="mono" id="claim-link">${link}</code>`,
387387 trail: btn("Copy the link", { type: "button", copy: "claim-link", icon: "copy", variant: "primary", size: "sm" }),
388388 })
389 }</ul>`, { pad: false })}
389 }</ul>
390390 <p class="mt">${btn("Forges you sent", { href: "/forges", size: "sm" })}</p>
391391 ${note === undefined ? null : html`<p class="small mt">${note}</p>`}`;
392392 }
test/forges-language.test.ts added +66
@@ -0,0 +1,66 @@
1 /** /forges, its fresh-link page and the remove confirm speak the home page's language (#9798): headings, no card. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdirSync, writeFileSync } from "node:fs";
4 import { mkdtemp, rm } from "node:fs/promises";
5 import { tmpdir } from "node:os";
6 import { join } from "node:path";
7 import { createApp } from "../src/app.ts";
8 import { createToken } from "../src/core/tokens.ts";
9 import { createUser } from "../src/core/users.ts";
10 import { serve } from "../src/http/server.ts";
11
12 let dir = "";
13 let app: ReturnType<typeof createApp>;
14 let servers: Awaited<ReturnType<typeof serve>>;
15 let auth: Record<string, string> = {};
16
17 beforeAll(async () => {
18 dir = await mkdtemp(join(tmpdir(), "forge-forges-lang-"));
19 app = createApp({ dataDir: dir, port: 0 });
20 servers = await serve(app);
21 const made = await createUser(app.db, {
22 origin: "seed", trusted: true, username: "alice", email: "[email protected]",
23 password: "a-good-passphrase", adminIfFirst: true,
24 });
25 if (!made.ok) throw new Error(made.error);
26 auth = { authorization: `Bearer ${createToken(app.db, made.value.id, "t", "admin").token}` };
27 });
28 afterAll(async () => {
29 servers.stop();
30 app.close();
31 await rm(dir, { recursive: true, force: true });
32 });
33
34 const main = (page: string): string => page.slice(page.indexOf("<main"), page.indexOf("</main>"));
35 const boxed = ['class="card', "card-body", 'class="empty'];
36
37 test("with none sent, /forges is a headed section, not an empty-state box", async () => {
38 const page = main(await (await fetch(servers.url + "/forges", { headers: auth })).text());
39 expect(page).toContain('<section class="settings-block">');
40 expect(page).toContain("<h2>No forges sent yet</h2>");
41 for (const b of boxed) expect(page).not.toContain(b);
42 });
43
44 test("with one sent, /forges has its headings over the table and the rows", async () => {
45 mkdirSync(join(dir, "spawn"), { recursive: true });
46 writeFileSync(join(dir, "spawn", "kid.json"), JSON.stringify({ name: "kid", bodyId: "b-k", done: true, size: "small" }));
47 const res = await fetch(servers.url + "/forges", { headers: auth });
48 const page = main(await res.text());
49 expect(page).toContain("<h2>Side by side</h2>");
50 expect(page).toContain("<h2>Forges</h2>");
51 expect(page).toContain('id="forges-compare"');
52 expect(page).toContain('data-forge="kid"');
53 for (const b of boxed) expect(page).not.toContain(b);
54 });
55
56 test("the remove confirm keeps its words and buttons under a heading, not in a card", async () => {
57 const res = await fetch(servers.url + "/forges/kid/remove", { headers: auth });
58 expect(res.status).toBe(200);
59 expect(res.headers.get("cache-control")).toBe("no-store");
60 const page = main(await res.text());
61 expect(page).toContain("<h2>What removing deletes</h2>");
62 expect(page).toContain("It cannot be undone.");
63 expect(page).toContain("Remove it for good");
64 expect(page).toContain("Keep it");
65 for (const b of boxed) expect(page).not.toContain(b);
66 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs