Public: anyone can read this; the crew can change it.
No notes: the standing explanations on search, machines, crew, forges and pinned leave the first view #10693
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/10688-no-notes-the-standing-explanations-on-se → main 4 commits fork 2 threads · 2 unresolved
7 files
changed +77
−12
src/web/pages/crew.ts +1 −1
| @@ -610,7 +610,7 @@ function crewView( | ||
| 610 | 610 | ${agents.length === 0 |
| 611 | 611 | ? html`<section class="mb" id="crew-empty"> |
| 612 | 612 | <h2 class="mb">No agents yet</h2> |
| 613 | <p class="muted">An agent is an account that works on a repository while you are elsewhere. Make one below, then put it on a repository's crew in that repository's settings.</p> | |
| 613 | <p class="muted">Make one below.</p> | |
| 614 | 614 | </section>` |
| 615 | 615 | : panel( |
| 616 | 616 | html`<div class="table-scroll"> |
src/web/pages/forges.ts −1
| @@ -152,7 +152,6 @@ export async function forgesPage(ctx: Ctx): Promise<Response> { | ||
| 152 | 152 | ${rows.length === 0 |
| 153 | 153 | ? html`<section class="settings-block"> |
| 154 | 154 | <h2>No forges sent yet</h2> |
| 155 | <p class="muted">A whole forge for someone: the apps you choose, a crew, and one link to send.</p> | |
| 156 | 155 | <p>${action}</p> |
| 157 | 156 | </section>` |
| 158 | 157 | : html`<section class="settings-block" id="forges-compare"> |
src/web/pages/machines.ts −1
| @@ -86,7 +86,6 @@ export function machineRows(repo: Repo, list: readonly Machine[], states: Readon | ||
| 86 | 86 | function render(ctx: Ctx, repo: Repo, list: readonly Machine[], sessions: readonly SessionRecord[], states: ReadonlyMap<string, MachineState>): Response { |
| 87 | 87 | const machines = html`<section class="settings-block"> |
| 88 | 88 | <h2>Machines</h2> |
| 89 | <p class="muted">A machine is where the crew shell runs; each writer gets a directory and home of their own on it.</p> | |
| 90 | 89 | <ul class="list setting-list" id="machines"> |
| 91 | 90 | ${machineRows(repo, list, states)} |
| 92 | 91 | </ul> |
src/web/pages/pinned.ts +1 −1
| @@ -59,6 +59,6 @@ function forge(p: PinnedView, f: PinnedFacts): Html { | ||
| 59 | 59 | |
| 60 | 60 | export function pinnedListPage(forges: PinnedView[], f: PinnedFacts): Html { |
| 61 | 61 | return html`<div class="page-head"><h1>Pinned forges</h1></div> |
| 62 | <p class="muted">Forges you put on your own cloud, each on one version that changes only when you update it. <a href="/deploy-to">Deploy a pinned forge</a>.</p> | |
| 62 | <p><a href="/deploy-to">Deploy a pinned forge</a></p> | |
| 63 | 63 | ${forges.length === 0 ? html`<p role="status">No pinned forge yet. Deploy one to a cloud account that allows writes.</p>` : forges.map((p) => forge(p, f))}`; |
| 64 | 64 | } |
src/web/pages/search.ts +1 −2
| @@ -398,7 +398,7 @@ export async function searchPage(ctx: Ctx): Promise<Response> { | ||
| 398 | 398 | type="search" |
| 399 | 399 | name="q" |
| 400 | 400 | value="${a.query}" |
| 401 | placeholder="Search apps, repositories, issues, pull requests and code" | |
| 401 | placeholder="Apps, repos, issues, pulls, code" | |
| 402 | 402 | aria-label="Search" |
| 403 | 403 | autofocus |
| 404 | 404 | data-search-input |
| @@ -420,7 +420,6 @@ export async function searchPage(ctx: Ctx): Promise<Response> { | ||
| 420 | 420 | title: "Search", |
| 421 | 421 | styleCss: SEARCH_CSS, |
| 422 | 422 | body: html`<div data-search data-palette-auto>${pageHead("Search")} ${form} |
| 423 | <p class="muted">Search this forge: an app or repository by name or description, a word from what was told or changed, or a line of the code itself — across everything you can see, here and on every origin you have connected.</p> | |
| 424 | 423 | </div>`, |
| 425 | 424 | }); |
| 426 | 425 | notePhase(ctx.request, "render", performance.now() - rendering); |
test/no-standing-notes.test.ts added +67
| @@ -0,0 +1,67 @@ | ||
| 1 | /** DESIGN.md rule 2 (#10688): the standing explanations on five pages stay gone; the actions stay. Rendered, not grepped. */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp } from "../src/app.ts"; | |
| 7 | import { createToken } from "../src/core/tokens.ts"; | |
| 8 | import { serve } from "../src/http/server.ts"; | |
| 9 | import { seedAdmin } from "./helpers/admin.ts"; | |
| 10 | ||
| 11 | let dir = ""; | |
| 12 | let app: ReturnType<typeof createApp>; | |
| 13 | let servers: Awaited<ReturnType<typeof serve>>; | |
| 14 | let bearer = ""; | |
| 15 | const get = async (path: string) => | |
outdated
These tests read page source files and check for strings. A rename or a moved sentence passes them while the page still shows the note. Render the machines, crew and forges pages (or their view functions) and assert the sentence is absent from the HTML, as the pinned test does. |
||
| 16 | (await fetch(servers.url + path, { headers: { authorization: `Bearer ${bearer}` } })).text(); | |
| 17 | ||
| 18 | beforeAll(async () => { | |
| 19 | dir = await mkdtemp(join(tmpdir(), "forge-no-notes-")); | |
| 20 | app = createApp({ dataDir: dir, port: 0 }); | |
| 21 | servers = await serve(app); | |
| 22 | const made = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "a-good-passphrase" }); | |
| 23 | if (!made.ok) throw new Error(made.error); | |
| 24 | bearer = createToken(app.db, made.value.id, "t", "admin").token; | |
| 25 | await fetch(servers.url + "/api/v1/repos", { | |
| 26 | method: "POST", | |
| 27 | headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" }, | |
| 28 | body: JSON.stringify({ name: "zephyr", visibility: "public" }), | |
| 29 | }); | |
| 30 | }); | |
| 31 | ||
| 32 | afterAll(async () => { | |
| 33 | servers.stop(); | |
| 34 | app.close(); | |
| 35 | await rm(dir, { recursive: true, force: true }); | |
| 36 | }); | |
| 37 | ||
| 38 | const gone: [string, string][] = [ | |
| 39 | ["/search", "Search this forge:"], | |
| 40 | ["/alice/zephyr/settings/machines", "A machine is where the crew shell runs"], | |
| 41 | ["/settings/crew", "An agent is an account that works"], | |
| 42 | ["/forges/new", "A whole forge for someone:"], | |
| 43 | ["/pinned", "on one version that changes only"], | |
| 44 | ]; | |
| 45 | ||
| 46 | for (const [path, sentence] of gone) { | |
| 47 | test(`${path} renders without its standing explanation`, async () => { | |
| 48 | const html = await get(path); | |
| 49 | expect(html).toContain("<main"); | |
| 50 | expect(html).not.toContain("<h1>404</h1>"); | |
| 51 | expect(html).not.toContain(sentence); | |
| 52 | }); | |
| 53 | } | |
| 54 | ||
| 55 | test("search shows the short placeholder", async () => { | |
| 56 | expect(await get("/search")).toContain('placeholder="Apps, repos, issues, pulls, code"'); | |
| 57 | }); | |
| 58 | ||
| 59 | test("pinned keeps its action", async () => { | |
| 60 | expect(await get("/pinned")).toContain('href="/deploy-to"'); | |
| 61 | }); | |
| 62 | ||
| 63 | test("an empty crew says what to do in one clause", async () => { | |
| 64 | const html = await get("/settings/crew"); | |
| 65 | expect(html).toContain("No agents yet"); | |
| 66 | expect(html).toContain("Make one below."); | |
| 67 | }); |
test/search-language.test.ts +7 −6
| @@ -47,10 +47,11 @@ test("results are sections of rows with no card", async () => { | ||
| 47 | 47 | expect(html).toContain('<ul class="list">'); |
| 48 | 48 | }); |
| 49 | 49 | |
| 50 | test("an empty query and no results are one plain sentence", async () => { | |
| 51 | for (const path of ["/search?q=", "/search?q=qqqnothingqqq"]) { | |
| 52 | const html = main(await get(path)); | |
| 53 | expect(html).not.toContain('class="card'); | |
| 54 | expect(html).toContain('<p class="muted">'); | |
| 55 | } | |
| 50 | test("an empty query is the bare form and no results are one plain sentence", async () => { | |
| 51 | const empty = main(await get("/search?q=")); | |
| 52 | expect(empty).not.toContain('class="card'); | |
| 53 | expect(empty).not.toContain('<p class="muted">'); | |
| 54 | const none = main(await get("/search?q=qqqnothingqqq")); | |
| 55 | expect(none).not.toContain('class="card'); | |
| 56 | expect(none).toContain('<p class="muted">'); | |
| 56 | 57 | }); |
Principle 1: with the paragraph gone, the empty state is a heading with nothing under it, and "No agents yet" no longer says what to do. Either keep one clause beside the form (the issue allows it), such as "Make one below.", or make the empty state the form itself.