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

/flow's pipes and Valves read as plain sections under headings, not boxed cards #10025

Merged opened this

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

Merged

scout/forge:scout/10011-flow-s-pipes-and-valves-read-as-plain-se → main 2 commits fork

  • Merged. scout/10011-flow-s-pipes-and-valves-read-as-plain-se was merged into main as 98cfe6c.

Review

Sign in to review this change.
2 files changed +73 −5
src/web/pages/flow.ts +6 −5
@@ -17,7 +17,7 @@ import { pipesSvg } from "../flow_pipes.ts";
1717 import { field, readForm } from "../form.ts";
1818 import { html, when } from "../html.ts";
1919 import { flashTo, page } from "../layout.ts";
20 import { btn, card, icon, row, time } from "../ui.ts";
20 import { btn, icon, row, time } from "../ui.ts";
2121
2222 const money = (usd: number): string => `$${usd.toFixed(2)}`;
2323 const plural = (n: number, one: string, many = `${one}s`): string => `${String(n)} ${n === 1 ? one : many}`;
@@ -84,11 +84,12 @@ export function flowPage(ctx: Ctx): Response {
8484 const p = readPipes(ctx.app.db, user.id);
8585 const body = html`<div class="page-head"><h1>Flow</h1>${btn("Boards", { href: "/boards", variant: "ghost" })}</div>
8686 <p class="mb">Money flows from the left to shipped on the right, as wide as this week's real spend. Every repository has a valve: drag it to give the repository a share of the week, all the way down to shut. The crew does exactly what the valves say, from the next tick, and nothing but you turns one.</p>
87 ${card(html`<div class="card-body">${pipesSvg(p)}
88 <p class="muted small">${money(p.usd)} spent in the last 7 days; ${plural(p.shipped, "pull")} shipped.</p></div>`)}
87 ${pipesSvg(p)}
88 <p class="muted small">${money(p.usd)} spent in the last 7 days; ${plural(p.shipped, "pull")} shipped.</p>
8989 ${p.valves.length === 0
90 ? card(html`<div class="card-body"><p class="muted">No repository has spent, queued or shipped anything this week, so there is no pipe to turn yet.</p></div>`)
91 : card(html`<ul class="list">${p.valves.map(valveRow)}</ul>`, { pad: false, head: html`${icon("activity")}<h2>Valves</h2>` })}`;
90 ? html`<p class="muted">No repository has spent, queued or shipped anything this week, so there is no pipe to turn yet.</p>`
91 : html`<h2>Valves</h2>
92 <ul class="list">${p.valves.map(valveRow)}</ul>`}`;
9293 const rendering = performance.now();
9394 const response = page({ ctx, title: "Flow", body, scriptJs: valveJs });
9495 notePhase(ctx.request, "render", performance.now() - rendering);
test/flow-language.test.ts added +67
@@ -0,0 +1,67 @@
1 /** /flow reads as plain sections under headings, no card boxes (#10011). */
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, type App } from "../src/app.ts";
7 import { createRepo } from "../src/actions/repos.ts";
8 import { createRun } from "../src/core/runs.ts";
9 import { createUser } from "../src/core/users.ts";
10 import { serve, type Servers } from "../src/http/server.ts";
11
12 let dir = "";
13 let app: App;
14 let servers: Servers;
15 let repoId = "";
16
17 const login = async (username: string): Promise<string> => {
18 const res = await fetch(`${servers.url}/signin`, {
19 method: "POST",
20 redirect: "manual",
21 headers: { "content-type": "application/x-www-form-urlencoded", origin: servers.url },
22 body: new URLSearchParams({ username, password: "a-good-passphrase" }),
23 });
24 return (res.headers.get("set-cookie") ?? "").split(";")[0] ?? "";
25 };
26 const get = (cookie: string) => fetch(`${servers.url}/flow`, { headers: { cookie }, redirect: "manual" });
27
28 beforeAll(async () => {
29 dir = await mkdtemp(join(tmpdir(), "forge-flow-language-"));
30 app = createApp({ dataDir: dir, port: 0 });
31 servers = await serve(app);
32 const make = async (username: string, adminIfFirst: boolean) => {
33 const u = await createUser(app.db, { origin: "seed", trusted: true, username, email: `${username}@forge.local`, password: "a-good-passphrase", adminIfFirst });
34 if (!u.ok) throw new Error(u.error);
35 return u.value;
36 };
37 const alice = await make("alice", true);
38 await make("bob", false);
39 const made = await createRepo(app, alice, { name: "shop" });
40 if (!made.ok) throw new Error(String(made.error));
41 repoId = made.value.id;
42 });
43 afterAll(async () => {
44 servers.stop();
45 await rm(dir, { recursive: true, force: true });
46 });
47
48 test("with no valves: pipes, then one muted line, no card", async () => {
49 const text = await (await get(await login("alice"))).text();
50 expect(text).not.toContain('class="card');
51 expect(text).toContain('class="pipes');
52 expect(text).toContain('<p class="muted">No repository has spent');
53 expect(text).not.toContain("<h2>Valves</h2>");
54 });
55
56 test("with valves: an h2 over a plain list whose forms valveJs finds, no card", async () => {
57 createRun(app.db, { repoId, kind: "agent", reason: "assigned" });
58 const text = await (await get(await login("alice"))).text();
59 expect(text).not.toContain('class="card');
60 expect(text).toMatch(/<h2>Valves<\/h2>\s*<ul class="list">/);
61 expect(text).toContain('class="valve-form"');
62 expect(text).toContain('type="range"');
63 });
64
65 test("non-admins still get 404", async () => {
66 expect((await get(await login("bob"))).status).toBe(404);
67 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs