Public: anyone can read this; the crew can change it.
Home's stalled-step button and the pull page's Check again rest without a spinning loader #3068
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3066-home-s-stalled-step-button-and-the-pull → main 1 commit fork 1 thread · 1 unresolved
3 files
changed +28
−2
src/web/pages/pull.ts +1 −1
| @@ -358,7 +358,7 @@ function pullChecksLine( | ||
| 358 | 358 | tier, |
| 359 | 359 | pull.state === "open" && canWrite(ctx.app.db, repo, ctx.auth.user) |
| 360 | 360 | ? html`<form method="post" action="${thread}/checks" class="inline-form"> |
| 361 | ${btn("Check again", { size: "sm", icon: "loader" })} | |
| 361 | ${btn("Check again", { size: "sm", icon: "play" })} | |
| 362 | 362 | </form>` |
| 363 | 363 | : "", |
| 364 | 364 | status.conflicts.length, |
src/web/studio/flow_card.ts +1 −1
| @@ -26,7 +26,7 @@ function stalledCard(s: StalledStep): Html { | ||
| 26 | 26 | <p class="fcard-reason"><a href="${base}">${s.title}</a>${s.issue === null ? null : html` <span class="muted small">for #${String(s.issue)}</span>`}</p> |
| 27 | 27 | <form method="post" action="${base}/checks" class="fcard-unblock"> |
| 28 | 28 | <input type="hidden" name="next" value="/"> |
| 29 | ${btn(BUTTON[s.action], { size: "sm", icon: "loader" })} | |
| 29 | ${btn(BUTTON[s.action], { size: "sm", icon: "play" })} | |
| 30 | 30 | <span class="muted small">${s.unblock}</span> |
| 31 | 31 | </form> |
| 32 | 32 | </article>`; |
test/button-rest.test.ts added +26
| @@ -0,0 +1,26 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { Glob } from "bun"; | |
| 3 | ||
| 4 | // A button at rest must not spin: `.ico-loader` always animates, so a | |
| 5 | // btn(...) whose icon is "loader" looks busy before anyone clicks it. | |
| 6 | test("no btn(...) call in src/web rests on the loader icon", async () => { | |
| 7 | const offenders: string[] = []; | |
| 8 | for (const path of new Glob("src/web/**/*.ts").scanSync(".")) { | |
| 9 | const text = await Bun.file(path).text(); | |
| 10 | for (const m of text.matchAll(/\bbtn\(/g)) { | |
| 11 | // Read the call's argument list by matching parentheses. | |
| 12 | let depth = 0; | |
| 13 | let end = m.index! + m[0].length - 1; | |
| 14 | for (; end < text.length; end++) { | |
| 15 | if (text[end] === "(") depth++; | |
| 16 | else if (text[end] === ")" && --depth === 0) break; | |
| 17 | } | |
| 18 | const call = text.slice(m.index!, end + 1); | |
| 19 | if (/\bicon:\s*["']loader["']/.test(call)) { | |
| 20 | const line = text.slice(0, m.index).split("\n").length; | |
| 21 | offenders.push(`${path}:${line}`); | |
| 22 | } | |
| 23 | } | |
| 24 | } | |
| 25 | expect(offenders).toEqual([]); | |
| 26 | }); |
new above
offline
The paren matcher ignores parentheses inside string literals, such as a label like "Retry (x)". It cannot give a false pass on the loader check today. If a label ever unbalances it, scan to the closing paren with a small tokenizer.