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

The forge's following page speaks the home page's language #9777

Merged opened this

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

Merged

mason/forge:mason/9714-the-forge-s-following-page-speaks-the-ho → main 2 commits fork

  • Merged. mason/9714-the-forge-s-following-page-speaks-the-ho was merged into main as 433a101.

Review

Sign in to review this change.
2 files changed +46 −51
src/web/pages/chain.ts +42 −51
@@ -19,7 +19,7 @@ import { loadRepo, notFoundPage, pageHead, repoBase, signInFirst } from "../chro
1919 import { field, readForm } from "../form.ts";
2020 import { html, type Html } from "../html.ts";
2121 import { flashOrJson, page, redirect } from "../layout.ts";
22 import { badge, btn, card, row } from "../ui.ts";
22 import { badge, btn, row } from "../ui.ts";
2323
2424 /** The events that change what the chain page says. */
2525 export const CHAIN_LIVE = "forge.severed,forge.proposal,forge.follow_mode,forge.upstream,forge.updates_taken";
@@ -45,8 +45,8 @@ export function modeForm(base: string, current: FollowMode | null, legend: strin
4545 </form>`;
4646 }
4747
48 function modeCard(base: string, current: FollowMode | null): Html {
49 return card(modeForm(base, current, "How this forge follows its parent"), { head: html`<h2>Follow mode</h2>` });
48 function modeSection(base: string, current: FollowMode | null): Html {
49 return html`<h2 class="mt">Follow mode</h2>${modeForm(base, current, "How this forge follows its parent")}`;
5050 }
5151
5252 const MODE_WORDS = { "tell-me": "tell me", "take-updates": "take updates (as a pull you merge)" } as const;
@@ -79,31 +79,27 @@ async function carriesChain(ctx: Ctx): Promise<boolean> {
7979
8080 const STATE_WORDS = { open: "open", merged: "merged", closed: "closed" } as const;
8181
82 function proposalCard(base: string, proposals: readonly ForgeProposal[], writer: boolean): Html {
83 return card(
84 html`${proposals.length === 0
85 ? html`<p class="muted">Nothing proposed to the parent forge yet.</p>`
86 : html`<ul class="list" aria-label="Proposals to the parent forge">
87 ${proposals.map(
88 (p) => html`<li>
89 <div class="row">
90 <span class="row-main">
91 <a class="row-title" href="${p.url}" rel="noopener">#${p.number} ${p.title}</a>
92 <span class="row-meta"
93 >${STATE_WORDS[p.state]}${p.reachable ? null : html` · parent unreachable`}</span
94 >
95 </span>
96 </div>
97 </li>`,
98 )}
99 </ul>`}
100 ${writer
101 ? html`<form method="post" action="${base}/following/propose" class="mt">
102 <div class="form-actions">${btn("Propose to the parent forge", { type: "submit", icon: "merge" })}</div>
103 </form>`
104 : null}`,
105 { head: html`<h2>Proposals</h2>` },
106 );
82 function proposalSection(base: string, proposals: readonly ForgeProposal[], writer: boolean): Html {
83 return html`<h2 class="mt">Proposals</h2>
84 ${proposals.length === 0
85 ? html`<p class="muted">Nothing proposed to the parent forge yet.</p>`
86 : html`<ul class="list" aria-label="Proposals to the parent forge">
87 ${proposals.map(
88 (p) => html`<li>
89 <div class="row">
90 <span class="row-main">
91 <a class="row-title" href="${p.url}" rel="noopener">#${p.number} ${p.title}</a>
92 <span class="row-meta">${STATE_WORDS[p.state]}${p.reachable ? null : html` · parent unreachable`}</span>
93 </span>
94 </div>
95 </li>`,
96 )}
97 </ul>`}
98 ${writer
99 ? html`<form method="post" action="${base}/following/propose" class="mt">
100 <div class="form-actions">${btn("Propose to the parent forge", { type: "submit", icon: "merge" })}</div>
101 </form>`
102 : null}`;
107103 }
108104
109105 // GET /:owner/:name/following
@@ -137,12 +133,9 @@ export async function chainPage(ctx: Ctx): Promise<Response> {
137133 data-live="${CHAIN_LIVE}"
138134 data-live-repo="${repo.ownerName}/${repo.name}"
139135 >
140 ${card(
141 html`<ul class="list" aria-label="Forges, root first">
142 ${view.lines.map((l) => lineRow(l, l.here && l.mode !== null ? upstreamWords(state) : undefined))}
143 </ul>`,
144 { pad: false },
145 )}
136 <ul class="list" aria-label="Forges, root first">
137 ${view.lines.map((l) => lineRow(l, l.here && l.mode !== null ? upstreamWords(state) : undefined))}
138 </ul>
146139 ${writer && view.severedAt === null
147140 ? html`<form method="post" action="${base}/following/check" class="mt">
148141 <div class="form-actions">${btn("Check the parent now", { type: "submit" })}</div>
@@ -155,29 +148,27 @@ export async function chainPage(ctx: Ctx): Promise<Response> {
155148 </div>
156149 </form>`
157150 : null}
158 ${owner && view.severedAt === null ? modeCard(base, view.lines[view.lines.length - 1]?.mode ?? null) : null}
159 ${proposing ? proposalCard(base, proposals, writer) : null}
151 ${owner && view.severedAt === null ? modeSection(base, view.lines[view.lines.length - 1]?.mode ?? null) : null}
152 ${proposing ? proposalSection(base, proposals, writer) : null}
160153 ${view.severedAt !== null
161154 ? html`<p class="muted" id="severed">
162155 This forge follows nothing since ${view.severedAt.slice(0, 10)}. Its code and history are
163156 all still here.
164157 </p>`
165158 : owner
166 ? card(
167 html`<p>
168 Severing cuts this forge's link to the one above it. Nothing is deleted and
169 nothing leaves; the fork stays yours, and the tree says it follows nothing
170 from today.
171 </p>
172 <form method="post" action="${base}/following/sever" class="mt">
173 <label class="form-field"
174 ><span>Type sever to continue</span>
175 <input name="confirm" required autocomplete="off" class="mono"
176 /></label>
177 <div class="form-actions">${btn("Sever", { variant: "danger", type: "submit" })}</div>
178 </form>`,
179 { head: html`<h2>Sever</h2>` },
180 )
159 ? html`<h2 class="mt">Sever</h2>
160 <p>
161 Severing cuts this forge's link to the one above it. Nothing is deleted and
162 nothing leaves; the fork stays yours, and the tree says it follows nothing
163 from today.
164 </p>
165 <form method="post" action="${base}/following/sever" class="mt">
166 <label class="form-field"
167 ><span>Type sever to continue</span>
168 <input name="confirm" required autocomplete="off" class="mono"
169 /></label>
170 <div class="form-actions">${btn("Sever", { variant: "danger", type: "submit" })}</div>
171 </form>`
181172 : null}
182173 </div>
183174 </div>`,
test/spawn-chain-mode.test.ts +4
@@ -129,6 +129,10 @@ describe("choosing how a forge follows its parent", () => {
129129 expect(html).toContain('action="/kid/forge/following/mode"');
130130 expect(html).toMatch(/value="tell-me" checked/);
131131 expect(html).not.toContain("<script>");
132 // Rows and headings, not boxes.
133 expect(html).not.toContain('class="card');
134 expect(html).toContain('<ul class="list" aria-label="Forges, root first">');
135 expect(html).toContain("<h2");
132136
133137 expect((await form(cookie, "take-updates")).status).toBe(303);
134138 expect((await forgeLinkOf(child))?.mode).toBe("take-updates");

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs