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

Settings: move the other hover-gated forms (row-actions) onto row-fields #2960

Merged opened this

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

Merged

scout/forge:scout/2918-settings-move-the-other-hover-gated-form → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/2918-settings-move-the-other-hover-gated-form was merged into main as 301109f.

Review

Sign in to review this change.
9 files changed +30 −16
docs/DESIGN.md +1 −1

Diff not shown — a line 2,427 characters wide (limit 2,000). +1 −1

View file
src/web/pages/access.ts +1 −1
@@ -42,7 +42,7 @@ export function accessPage(ctx: Ctx): Response {
4242 sign-up). Tokens, cookies and query strings are never kept; the
4343 hash stands for one caller for one day.
4444 </p>
45 <form class="row-actions" method="get" action="/settings/access">
45 <form class="row-fields" method="get" action="/settings/access">
4646 <input
4747 class="input"
4848 name="ua"
src/web/pages/cloud.ts +5 −5
@@ -57,7 +57,7 @@ function accountsCard(reads: CloudRead[], uses: Map<string, VendorUse[]>, ctx: C
5757 meta: html`${a.summary}<br />${usedFor(uses.get(VENDOR_OF[a.provider]))}<br />${HOME[a.provider]}<br />verified ${a.verifiedAt ? time(a.verifiedAt) : "never"} ·
5858 ${last ? html`last used ${time(last.at)} by ${runWords(last.run)}` : "never used"} ·
5959 ${a.writeAllowed ? "writes allowed" : "read-only"}`,
60 trail: html`<div class="row-actions">
60 trail: html`<div class="row-fields">
6161 <form method="post" action="/settings/cloud/verify">
6262 ${hidden(a.provider, a.name)}${btn("Verify", { size: "sm", label: `Verify ${a.name}` })}
6363 </form>
@@ -66,19 +66,19 @@ function accountsCard(reads: CloudRead[], uses: Map<string, VendorUse[]>, ctx: C
6666 <input type="hidden" name="allowed" value="${a.writeAllowed ? "0" : "1"}" />
6767 ${btn(a.writeAllowed ? "Turn writes off" : "Allow writes", { size: "sm", label: `Write switch for ${a.name}` })}
6868 </form>
69 <form method="post" action="/settings/cloud/audit" class="row-actions">
69 <form method="post" action="/settings/cloud/audit" class="row-fields">
7070 ${hidden(a.provider, a.name)}
7171 <input class="input" type="text" name="repo" placeholder="owner/repo for findings"
7272 aria-label="Repository for audit findings" maxlength="200" required />
7373 ${btn("Audit", { size: "sm", label: `Audit ${a.name}` })}
7474 </form>
75 <form method="post" action="/settings/cloud/rotate" class="row-actions">
75 <form method="post" action="/settings/cloud/rotate" class="row-fields">
7676 ${hidden(a.provider, a.name)}
7777 <input class="input" type="password" name="value" placeholder="New credential"
7878 aria-label="New credential for ${a.name}" autocomplete="off" required />
7979 ${btn("Rotate", { size: "sm", label: `Rotate ${a.name}` })}
8080 </form>
81 <form method="post" action="/settings/cloud/rename" class="row-actions">
81 <form method="post" action="/settings/cloud/rename" class="row-fields">
8282 ${hidden(a.provider, a.name)}
8383 <input class="input mono" type="text" name="to" placeholder="new name"
8484 aria-label="New name for ${a.name}" pattern="[a-z][a-z0-9_]{0,31}" maxlength="32"
@@ -102,7 +102,7 @@ function accountsCard(reads: CloudRead[], uses: Map<string, VendorUse[]>, ctx: C
102102 provider key less powerful than it already is.
103103 </p>
104104 </div>
105 ${when(rows.length > 0, () => html`<ul class="list">${rows}</ul>`)}
105 ${when(rows.length > 0, () => html`<ul class="list setting-list">${rows}</ul>`)}
106106 <form class="edit-row" method="post" action="/settings/cloud">
107107 <select class="input" name="provider" aria-label="Provider" required>
108108 ${PROVIDERS.map((p) => html`<option value="${p}">${LABEL[p]}</option>`)}
src/web/pages/flags.ts +2 −2
@@ -77,7 +77,7 @@ export function flagsPage(ctx: Ctx): Response {
7777 <span class="mono">${decl.default ? "on" : "off"}</span> · owner
7878 ${issueLink(repo, decl.issue)} ·
7979 live: ${summary(v)}<br /><span class="mono">${usesLine(week, month)}</span>`,
80 trail: html`<form class="row-actions flag-form" method="post" action="/settings/flags">
80 trail: html`<form class="row-fields flag-form" method="post" action="/settings/flags">
8181 <input type="hidden" name="name" value="${name}" />
8282 <select class="input" name="forge" aria-label="${name} for the forge">
8383 <option value="" ${v?.forge === undefined ? "selected" : ""}>forge: default</option>
@@ -100,7 +100,7 @@ export function flagsPage(ctx: Ctx): Response {
100100 ? html`<div class="card-body">
101101 <p class="muted small">No flag is declared yet.</p>
102102 </div>`
103 : html`<ul class="list">
103 : html`<ul class="list setting-list">
104104 ${rows}
105105 </ul>`,
106106 {
src/web/pages/people.ts +2 −2
@@ -29,13 +29,13 @@ export function peoplePage(ctx: Ctx): Response {
2929 role === "admin"
3030 ? null
3131 : role === "guest"
32 ? html`<form class="row-actions" method="post" action="${at}">
32 ? html`<form class="row-fields" method="post" action="${at}">
3333 <input type="hidden" name="role" value="member" />
3434 <input class="input" name="usd" inputmode="decimal" autocomplete="off"
3535 aria-label="Starter allowance, dollars a month" placeholder="$ a month, none" size="12" />
3636 ${btn("Make member", { type: "submit", size: "sm", variant: "primary" })}
3737 </form>`
38 : html`<form class="row-actions" method="post" action="${at}">
38 : html`<form class="row-fields" method="post" action="${at}">
3939 <input type="hidden" name="role" value="guest" />
4040 ${btn("Make guest", { type: "submit", size: "sm" })}
4141 </form>`;
src/web/pages/subscriptions.ts +2 −2
@@ -211,7 +211,7 @@ export function subscriptionsPage(ctx: Ctx): Response {
211211 st !== undefined,
212212 () =>
213213 html`<form
214 class="headroom row-actions"
214 class="headroom row-fields"
215215 method="post"
216216 action="${POOL}/${s.id}/headroom"
217217 >
@@ -235,7 +235,7 @@ export function subscriptionsPage(ctx: Ctx): Response {
235235 s.id !== PLATFORM,
236236 () =>
237237 html`<form
238 class="headroom row-actions"
238 class="headroom row-fields"
239239 method="post"
240240 action="${POOL}/${s.id}/budget"
241241 >
src/web/pages/telemetry.ts +2 −2
@@ -30,7 +30,7 @@ export function telemetryPage(ctx: Ctx): Response {
3030 private repositories and sealed values cut out. Revoke at any time;
3131 every read is listed below.
3232 </p></div>
33 <ul class="list">
33 <ul class="list setting-list">
3434 ${agents.length === 0
3535 ? html`<li class="muted small">No agent accounts yet.</li>`
3636 : agents.map((a) =>
@@ -38,7 +38,7 @@ export function telemetryPage(ctx: Ctx): Response {
3838 lead: icon("bot"),
3939 title: html`${a.username} <span class="muted small mono">telemetry:read</span>`,
4040 meta: html`${a.granted ? "Granted" : "Not granted"}`,
41 trail: html`<form class="row-actions" method="post" action="/settings/telemetry">
41 trail: html`<form class="row-fields" method="post" action="/settings/telemetry">
4242 <input type="hidden" name="agent" value="${a.username}" />
4343 <input type="hidden" name="on" value="${a.granted ? "0" : "1"}" />
4444 ${btn(a.granted ? "Revoke" : "Grant", {
src/web/studio/crew.ts +1 −1
@@ -158,7 +158,7 @@ function crewCard(ctx: Ctx, repo: Repo, base: string, admin: boolean): Html {
158158 ...(admin
159159 ? {
160160 trail: html`<form
161 class="row-actions"
161 class="row-fields"
162162 method="post"
163163 action="${base}/settings/crew/${encodeURIComponent(
164164 c.agent.user.username,
test/settings-visible.test.ts +14
@@ -107,3 +107,17 @@ test("the stylesheet never hides settings controls", () => {
107107 }
108108 expect(mine.join("}")).toMatch(/\.setting-list \.row-meta[^}]*white-space:\s*normal/);
109109 });
110
111 // #2918: the primary control of every other settings row is visible at rest too.
112 test("settings pages keep their primary forms out of .row-actions", async () => {
113 const { readFile } = await import("node:fs/promises");
114 for (const f of ["flags", "telemetry", "people", "cloud", "access"]) {
115 const src = await readFile(join(import.meta.dir, "..", "src/web/pages", `${f}.ts`), "utf8");
116 expect(src, f).not.toContain("row-actions");
117 expect(src, f).toContain("row-fields");
118 }
119 const subs = await readFile(join(import.meta.dir, "..", "src/web/pages/subscriptions.ts"), "utf8");
120 expect(subs).not.toMatch(/headroom row-actions/);
121 const flags = await get("/settings/flags");

This checks source strings rather than rendered output. Only /settings/flags is fetched, so a regression in crew.ts or people.ts would pass. Fetch the rendered pages and assert no row-actions on the primary forms.

122 expect(flags).not.toContain("row-actions");
123 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs