Public: anyone can read this; the crew can change it.
A studio list with nothing in it blames a filter nobody set #521
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/273-a-studio-list-with-nothing-in-it-blames → main 1 commit fork
2 files
changed +111
−3
src/web/studio/lists.ts +14 −3
| @@ -94,6 +94,17 @@ function inWordFilter(word: Word, filter: WordFilter | undefined): boolean { | ||
| 94 | 94 | : bucket === filter; |
| 95 | 95 | } |
| 96 | 96 | |
| 97 | /** The empty card's one line. A filter that is set is named, so the way out | |
| 98 | * is visible; none set is not the filter's fault, so it says what to do. */ | |
| 99 | function emptyNote( | |
| 100 | filter: WordFilter | undefined, | |
| 101 | subject: string, | |
| 102 | starter: string, | |
| 103 | ): string { | |
| 104 | if (filter === undefined) return starter; | |
| 105 | return `${subject} is ${filter} yet.`; | |
| 106 | } | |
| 107 | ||
| 97 | 108 | /** A filter bar of five words, each a link carrying `?word=` — never a |
| 98 | 109 | * form, so the filter lives in the address and can be shared. No filter |
| 99 | 110 | * reads as "Open": what is open and moving. */ |
| @@ -169,7 +180,7 @@ export async function studioTells( | ||
| 169 | 180 | ? empty({ |
| 170 | 181 | icon: "issue", |
| 171 | 182 | title: "Nothing here", |
| 172 | note: "Nothing told matches this filter yet.", | |
| 183 | note: emptyNote(filter, "Nothing told", "Nothing open. Tell the crew what you want and it shows up here."), | |
| 173 | 184 | }) |
| 174 | 185 | : listCard( |
| 175 | 186 | rows.map(({ issue, info }) => |
| @@ -259,7 +270,7 @@ export async function studioChanges( | ||
| 259 | 270 | ? empty({ |
| 260 | 271 | icon: "pull", |
| 261 | 272 | title: "Nothing here", |
| 262 | note: "Nothing matches this filter yet.", | |
| 273 | note: emptyNote(filter, "No change", "Nothing open. A change is the crew's proposed edit to your app; the crew opens them as it works."), | |
| 263 | 274 | }) |
| 264 | 275 | : listCard( |
| 265 | 276 | rows.map(({ pull, info }) => |
| @@ -374,7 +385,7 @@ export async function studioRuns( | ||
| 374 | 385 | ? empty({ |
| 375 | 386 | icon: "bot", |
| 376 | 387 | title: "Nothing here", |
| 377 | note: "Nothing matches this filter yet.", | |
| 388 | note: emptyNote(filter, "No run", "Nothing open. A run is one go the crew takes at a job; each one shows up here."), | |
| 378 | 389 | }) |
| 379 | 390 | : listCard( |
| 380 | 391 | rows.map(({ run, info }) => { |
test/studio.test.ts +97
| @@ -8042,3 +8042,100 @@ describe("the studio names a person on the app's page, and the crew only for the | ||
| 8042 | 8042 | expect(rowWith(appPage, `/changes/${personPull.number}`)).toContain("ivy"); |
| 8043 | 8043 | }); |
| 8044 | 8044 | }); |
| 8045 | ||
| 8046 | describe("the studio's empty lists", () => { | |
| 8047 | let dataDir: string; | |
| 8048 | let app: App; | |
| 8049 | let servers: Servers; | |
| 8050 | let base: string; | |
| 8051 | let cookie: string; | |
| 8052 | ||
| 8053 | const get = (path: string) => | |
| 8054 | fetch(`${base}${path}`, { | |
| 8055 | headers: { cookie: `${cookie}; forge_mode=studio` }, | |
| 8056 | redirect: "manual", | |
| 8057 | }).then((r) => r.text()); | |
| 8058 | ||
| 8059 | const setup = async (name: string) => { | |
| 8060 | const made = await createRepo(app, alice, { | |
| 8061 | name, | |
| 8062 | visibility: "private", | |
| 8063 | description: "", | |
| 8064 | }); | |
| 8065 | if (!made.ok) throw new Error(String(made.error)); | |
| 8066 | const seeded = await commitToBranch(app, alice, made.value, { | |
| 8067 | message: "Start", | |
| 8068 | files: [ | |
| 8069 | { | |
| 8070 | path: "package.json", | |
| 8071 | content: '{"name":"x","scripts":{"start":"bun run server.ts"}}\n', | |
| 8072 | }, | |
| 8073 | { path: "server.ts", content: "export {};\n" }, | |
| 8074 | ], | |
| 8075 | }); | |
| 8076 | if (!seeded.ok) throw new Error(JSON.stringify(seeded.error)); | |
| 8077 | const enabled = await enableApp(app, alice, made.value, { slug: name }); | |
| 8078 | if (!enabled.ok) throw new Error(String(enabled.error)); | |
| 8079 | return made.value; | |
| 8080 | }; | |
| 8081 | let alice: User; | |
| 8082 | ||
| 8083 | beforeAll(async () => { | |
| 8084 | dataDir = await mkdtemp(join(tmpdir(), "forge-studio-empty-")); | |
| 8085 | app = createApp({ dataDir, port: 0 }); | |
| 8086 | servers = await serve(app); | |
| 8087 | base = servers.url; | |
| 8088 | const a = await createUser(app.db, { | |
| 8089 | username: "alice", | |
| 8090 | email: "[email protected]", | |
| 8091 | password: "a-good-passphrase", | |
| 8092 | adminIfFirst: true, | |
| 8093 | }); | |
| 8094 | if (!a.ok) throw new Error(a.error); | |
| 8095 | alice = a.value; | |
| 8096 | cookie = ( | |
| 8097 | ( | |
| 8098 | await fetch(`${base}/signin`, { | |
| 8099 | method: "POST", | |
| 8100 | body: new URLSearchParams({ | |
| 8101 | username: "alice", | |
| 8102 | password: "a-good-passphrase", | |
| 8103 | }), | |
| 8104 | redirect: "manual", | |
| 8105 | }) | |
| 8106 | ).headers.get("set-cookie") ?? "" | |
| 8107 | ).split(";")[0]!; | |
| 8108 | }); | |
| 8109 | ||
| 8110 | afterAll(async () => { | |
| 8111 | servers.stop(); | |
| 8112 | await rm(dataDir, { recursive: true, force: true }); | |
| 8113 | }); | |
| 8114 | ||
| 8115 | test("a repository with nothing in it says what to do, and blames no filter", async () => { | |
| 8116 | await setup("blank"); | |
| 8117 | for (const [path, words] of [ | |
| 8118 | ["issues", "Tell the crew what you want"], | |
| 8119 | ["pulls", "A change is the crew"], | |
| 8120 | ["runs", "A run is one go"], | |
| 8121 | ] as const) { | |
| 8122 | const page = await get(`/alice/blank/${path}`); | |
| 8123 | expect(page).toContain("Nothing here"); | |
| 8124 | expect(page).toContain(words); | |
| 8125 | expect(page).not.toContain("this filter"); | |
| 8126 | } | |
| 8127 | }); | |
| 8128 | ||
| 8129 | test("a filter that hides every row is named", async () => { | |
| 8130 | const repo = await setup("busy"); | |
| 8131 | const told = openIssue(app, alice, repo, { title: "Add a tab", body: "" }); | |
| 8132 | if (!told.ok) throw new Error(String(told.error)); | |
| 8133 | const issues = await get("/alice/busy/issues?word=shipped"); | |
| 8134 | expect(issues).toContain("Nothing told is shipped yet."); | |
| 8135 | expect(issues).not.toContain("Tell the crew what you want"); | |
| 8136 | const pulls = await get("/alice/busy/pulls?word=look"); | |
| 8137 | expect(pulls).toContain("No change is look yet."); | |
| 8138 | const runs = await get("/alice/busy/runs?word=stopped"); | |
| 8139 | expect(runs).toContain("No run is stopped yet."); | |
| 8140 | }); | |
| 8141 | }); |
new above
offline