Public: anyone can read this; the crew can change it.
The accounts list is keyboard-first: j/k move between rows, enter opens one #10646
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/10430-the-accounts-list-is-keyboard-first-j-k → main 5 commits fork
2 files
changed +39
−0
src/web/pages/accounts.ts +1
| @@ -84,6 +84,7 @@ function personRow(a: AccountRow, sole: readonly string[]): Html { | ||
| 84 | 84 | // Who may be acted on: a person who is not an admin and is still here. |
| 85 | 85 | const actionable = a.kind === "user" && !a.isAdmin && a.removedAt === null; |
| 86 | 86 | return row({ |
| 87 | nav: true, | |
| 87 | 88 | title: html`${userLink(a.username, { mark: markOf(a) })} ${standing(a)}`, |
| 88 | 89 | meta: html`joined |
| 89 | 90 | ${time(a.createdAt)}${how.length > 0 ? html` · ${how.join(" · ")}` : null} |
test/keys-accounts.test.ts added +38
| @@ -0,0 +1,38 @@ | ||
| 1 | /** The accounts list joins the shell's j/k row traversal: one data-nav per account row. */ | |
| 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, type App } from "../src/app.ts"; | |
| 7 | import { createSession } from "../src/core/sessions.ts"; | |
| 8 | import { getUserByName } from "../src/core/users.ts"; | |
| 9 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 10 | import { seedAdmin } from "./helpers/admin.ts"; | |
| 11 | ||
| 12 | let dataDir = ""; | |
| 13 | let app: App, servers: Servers; | |
| 14 | ||
| 15 | beforeAll(async () => { | |
| 16 | dataDir = await mkdtemp(join(tmpdir(), "forge-keys-accounts-")); | |
| 17 | app = createApp({ dataDir, port: 0 }); | |
| 18 | servers = await serve(app); | |
| 19 | const a = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" }); | |
| 20 | if (!a.ok) throw new Error(a.error); | |
| 21 | for (const n of ["bob", "carol"]) | |
| 22 | app.db.query("INSERT INTO user (id, username, kind, email, display_name, password_hash) VALUES (?1, ?1, 'user', ?1 || '@x.test', ?1, 'x')").run(n); | |
| 23 | }); | |
| 24 | afterAll(async () => { | |
| 25 | await servers.stop(); | |
| 26 | app.close(); | |
| 27 | await rm(dataDir, { recursive: true, force: true }); | |
| 28 | }); | |
| 29 | ||
| 30 | test("every account row is a data-nav row", async () => { | |
| 31 | const alice = getUserByName(app.db, "alice")!; | |
| 32 | const cookie = `forge_session=${createSession(app.db, alice.id).token}`; | |
| 33 | const text = await (await fetch(`${servers.url}/settings/accounts`, { headers: { cookie } })).text(); | |
| 34 | const list = text.slice(text.indexOf('id="accounts-list"')); | |
| 35 | const end = list.indexOf("</ul>"); | |
| 36 | const nav = list.slice(0, end).match(/class="row"[^>]*data-nav tabindex="-1"/g); | |
| 37 | expect(nav?.length).toBe(3); | |
| 38 | }); |