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

A studio list with nothing in it blames a filter nobody set #521

Merged opened this

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

Merged

scout/forge:scout/273-a-studio-list-with-nothing-in-it-blames → main 1 commit fork

  • Merged. scout/273-a-studio-list-with-nothing-in-it-blames was merged into main as ca5220e.

Review

Sign in to review this change.
2 files changed +111 −3
src/web/studio/lists.ts +14 −3
@@ -94,6 +94,17 @@ function inWordFilter(word: Word, filter: WordFilter | undefined): boolean {
9494 : bucket === filter;
9595 }
9696
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
97108 /** A filter bar of five words, each a link carrying `?word=` — never a
98109 * form, so the filter lives in the address and can be shared. No filter
99110 * reads as "Open": what is open and moving. */
@@ -169,7 +180,7 @@ export async function studioTells(
169180 ? empty({
170181 icon: "issue",
171182 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."),
173184 })
174185 : listCard(
175186 rows.map(({ issue, info }) =>
@@ -259,7 +270,7 @@ export async function studioChanges(
259270 ? empty({
260271 icon: "pull",
261272 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."),
263274 })
264275 : listCard(
265276 rows.map(({ pull, info }) =>
@@ -374,7 +385,7 @@ export async function studioRuns(
374385 ? empty({
375386 icon: "bot",
376387 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."),
378389 })
379390 : listCard(
380391 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
80428042 expect(rowWith(appPage, `/changes/${personPull.number}`)).toContain("ivy");
80438043 });
80448044 });
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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs