Public: anyone can read this; the crew can change it.
/boards lists its boards as keyboard rows: j/k move, enter opens a board #10351
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/10341-boards-lists-its-boards-as-keyboard-rows → main 2 commits fork
2 files
changed +57
−0
src/web/pages/boards-view.ts +1
| @@ -105,6 +105,7 @@ export async function boardsList(ctx: Ctx): Promise<Response> { | ||
| 105 | 105 | ${listed.value.map(({ slug, board }) => |
| 106 | 106 | row({ |
| 107 | 107 | href: `/boards/${slug}`, |
| 108 | nav: true, | |
| 108 | 109 | title: board.title, |
| 109 | 110 | meta: html`${String(board.panels.length)} panel${board.panels.length === 1 ? "" : "s"} · ${board.range} · refresh ${board.refresh}`, |
| 110 | 111 | }), |
test/keys-boards-list.test.ts added +56
| @@ -0,0 +1,56 @@ | ||
| 1 | /** /boards: each board is a keyboard row (`data-nav`), so j/k move and Enter opens it. */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp } from "../src/app.ts"; | |
| 7 | import { createToken } from "../src/core/tokens.ts"; | |
| 8 | import { createUser } from "../src/core/users.ts"; | |
| 9 | import { serve } from "../src/http/server.ts"; | |
| 10 | ||
| 11 | let dir: string; | |
| 12 | let app: ReturnType<typeof createApp>; | |
| 13 | let servers: Awaited<ReturnType<typeof serve>>; | |
| 14 | let token: string; | |
| 15 | const call = (path: string, fields?: Record<string, string>) => | |
| 16 | fetch(servers.url + path, { | |
| 17 | redirect: "manual", | |
| 18 | ...(fields ? { method: "POST", body: new URLSearchParams(fields) } : {}), | |
| 19 | headers: { authorization: `Bearer ${token}` }, | |
| 20 | }); | |
| 21 | ||
| 22 | beforeAll(async () => { | |
| 23 | dir = await mkdtemp(join(tmpdir(), "forge-keys-boards-")); | |
| 24 | app = createApp({ dataDir: dir, port: 0 }); | |
| 25 | servers = await serve(app); | |
| 26 | const r = await createUser(app.db, { | |
| 27 | origin: "seed", trusted: true, username: "alice", email: "[email protected]", | |
| 28 | password: "a-good-passphrase", adminIfFirst: true, | |
| 29 | }); | |
| 30 | if (!r.ok) throw new Error(r.error); | |
| 31 | token = createToken(app.db, r.value.id, "t", "admin").token; | |
| 32 | }); | |
| 33 | afterAll(async () => { | |
| 34 | servers.stop(); | |
| 35 | app.close(); | |
| 36 | await rm(dir, { recursive: true, force: true }); | |
| 37 | }); | |
| 38 | ||
| 39 | const listed = async () => { | |
| 40 | const html = await (await call("/boards")).text(); | |
| 41 | const at = html.indexOf('id="boards-all"'); | |
| 42 | expect(at).toBeGreaterThan(-1); | |
| 43 | return html.slice(at, html.indexOf("</section>", at)); | |
| 44 | }; | |
| 45 | ||
| 46 | test("no boards: the list has no data-nav", async () => { | |
| 47 | expect(await listed()).not.toContain("data-nav"); | |
| 48 | }); | |
| 49 | ||
| 50 | test("each board is one data-nav row linking to its own page", async () => { | |
| 51 | for (const title of ["Alpha", "Beta"]) expect((await call("/boards/new", { title })).status).toBe(303); | |
| 52 | const all = await listed(); | |
| 53 | expect(all.match(/data-nav tabindex="-1"/g)?.length).toBe(2); | |
| 54 | const links = [...all.matchAll(/<a href="(\/boards\/[^"]+)"[^>]*data-nav/g)].map((m) => m[1]); | |
| 55 | expect(links).toEqual(["/boards/alpha", "/boards/beta"]); | |
| 56 | }); |