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

Telling the crew from an app's page is a plain field and button, not a boxed card #10067

Merged opened this

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

Merged

scout/forge:scout/10052-telling-the-crew-from-an-app-s-page-is-a → main 3 commits fork 1 thread · 1 unresolved

  • Merged. scout/10052-telling-the-crew-from-an-app-s-page-is-a was merged into main as e2e14f1.

Review

Sign in to review this change.
4 files changed +80 −26
docs/design/areas/tell.md +5
@@ -19,3 +19,8 @@ A muted line on a client's app row, `Finished <time>: <title>`, for the app's
1919 newest merged pull. It sits with `client-app-about`, `client-app-agents` and
2020 `client-app-doing`; it uses `muted` and adds no colour or size. The
2121 terminal says the same words.
22
23 ## App page: the tell line
24
25 The owner's tell on an app's page is one `.bar.tell-box` line, a field and a Tell button, as on home. It adds no
26 class and no `card`; a stranger gets no form.
src/web/studio/app.ts +13 −25
@@ -51,7 +51,6 @@ import {
5151 icon,
5252 listCard,
5353 row,
54 userTag,
5554 } from "../ui.ts";
5655 import { changesHref, tellsHref } from "./nouns.ts";
5756 import { answersRegion } from "./answers.ts";
@@ -328,36 +327,25 @@ export async function studioApp(
328327 // stranger sees the app, not who is behind it.
329328 const tell = !ownerFacts
330329 ? null
331 : html`<form
332 class="card comment-form"
333 method="post"
334 action="${base}/tell"
335 >
330 : html`<form class="bar tell-box" method="post" action="${base}/tell">
336331 <label class="sr-only" for="tell">Tell the crew</label>
337 <textarea
332 <input
338333 class="input"
339334 id="tell"
340335 name="text"
341 rows="3"
342336 required
343337 maxlength="${MAX_BODY}"
344 placeholder="${builders.length === 0
345 ? "Add a crew member under Settings → Crew, then tell them what you want."
346 : "What should change? One thing at a time works best."}"
347 ></textarea>
348 <div class="bar">
349 <span class="muted small">
350 ${builders.length === 0
351 ? html`Nobody on the crew yet.`
352 : html`${builders
353 .map((b) => userTag(b.user.username))
354 .reduce((a, b) => html`${a} ${b}`)}
355 ${builders.length === 1 ? "takes it" : "share the work"}`}
356 </span>
357 <span class="spacer"></span>
358 ${btn("Tell the crew", { variant: "primary", type: "submit" })}
359 </div>
360 </form>`;
338 placeholder="Tell the crew…"
339 />
outdated

Principle 8 (plain words, and not hidden): this long sentence is the only way a crew-less owner learns what to do, and a one-line input clips it on a phone. Keep a short placeholder and show this as a muted line with a link to Settings → Crew.

340 ${btn("Tell", { variant: "primary", type: "submit" })}
341 </form>
342 ${builders.length === 0
343 ? html`<p class="muted small">
344 Nobody on the crew yet.
345 <a href="${base}/settings#crew">Add a crew member under Settings → Crew</a>,
346 then tell them what you want.
347 </p>`
348 : null}`;
361349
362350 // ---- now: one row per thing, in the five words (studio/now.ts) ----
363351 const { card: now } = await timePhaseAsync(req, "now", () =>
test/app-tell-line.test.ts added +61
@@ -0,0 +1,61 @@
1 /** The owner's tell on an app's page is a plain field and button, not a boxed card (#10052). */
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 { newApp } from "../src/actions/apps.ts";
7 import { createApp, type App } from "../src/app.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { serve, type Servers } from "../src/http/server.ts";
10 import { quietLogs } from "./helpers/globals.ts";
11
12 quietLogs();
13
14 let dataDir: string;
15 let app: App;
16 let servers: Servers;
17 let cookie = "";
18
19 beforeAll(async () => {
20 dataDir = await mkdtemp(join(tmpdir(), "forge-app-tell-line-"));
21 app = createApp({ dataDir, port: 0, host: "forge.test" });
22 servers = await serve(app);
23 const made = await createUser(app.db, {
24 origin: "seed", trusted: true, username: "alice", email: "[email protected]",
25 password: "a-good-passphrase", adminIfFirst: true,
26 });
27 if (!made.ok) throw new Error(made.error);
28 const r = await newApp(app, made.value, { name: "open", description: "x", visibility: "public" });
29 if (!r.ok) throw new Error(String(r.error));
30 const res = await fetch(`${servers.url}/signin`, {
31 method: "POST",
32 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
33 redirect: "manual",
34 });
35 cookie = (res.headers.get("set-cookie") ?? "").split(";")[0]!;
36 }, 120_000);
37
38 afterAll(async () => {
39 servers.stop();
40 app.close();
41 await rm(dataDir, { recursive: true, force: true });
42 });
43
44 const tellForm = (body: string) => /<form[^>]*action="[^"]*\/tell"[^>]*>/.exec(body)?.[0];
45
46 test("a stranger sees no tell form; the owner sees a plain one", async () => {
47 const out = await (await fetch(`${servers.url}/alice/open/app`)).text();
48 expect(tellForm(out)).toBeUndefined();
49 const mine = await (await fetch(`${servers.url}/alice/open/app`, { headers: { cookie } })).text();
50 const form = tellForm(mine);
51 expect(form).toBeDefined();
52 expect(form).not.toMatch(/class="[^"]*\bcard\b/);
53 expect(form).toContain("/alice/open/tell");
54 const whole = /<form[^>]*action="[^"]*\/tell"[^>]*>[\s\S]*?<\/form>/.exec(mine)![0];
55 expect(whole).toMatch(/<input[^>]*name="text"/);
56 expect(whole).toMatch(/<button[^>]*>\s*Tell\s*<\/button>|<button[\s\S]*?Tell[\s\S]*?<\/button>/);
57 expect(whole).not.toContain("<textarea");
58 // No crew: the hint is a muted line with a link, not a long placeholder.
59 expect(whole).toContain('placeholder="Tell the crew…"');
60 expect(mine).toContain('href="/alice/open/settings#crew"');
61 });
test/studio.test.ts +1 −1
@@ -272,7 +272,7 @@ describe("the studio", () => {
272272 }
273273 const studio = await (await get("/alice/notes")).text();
274274 expect(studio).toMatch(/<div class="page-head">\s*<div class="stack stack-tight">\s*<h1 class="bar">/);
275 expect(studio).toContain('class="card comment-form"');
275 expect(studio).toContain('class="bar tell-box"');
276276 });
277277
278278 test("the home and an app's page lay main and rail with .split's wide rail; no dash class is left (#4748)", async () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs