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

The map's issues are keyboard rows: j/k move between them, enter opens the issue #10359

Merged opened this

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

Merged

scout/forge:scout/10342-the-map-s-issues-are-keyboard-rows-j-k-m → main 3 commits fork

  • Merged. scout/10342-the-map-s-issues-are-keyboard-rows-j-k-m was merged into main as d621249.

Review

Sign in to review this change.
2 files changed +74 −0
src/web/pages/map.ts +1
@@ -975,6 +975,7 @@ function rankedList(
975975 href: issueHref(base, r.issue.number),
976976 title: r.issue.title,
977977 titleAttr: r.issue.title,
978 nav: true,
978979 meta: html`#${r.issue.number} · ${say(r)}${first?.by.has(r.issue.id)
979980 ? ` · first, by ${first.by.get(r.issue.id)}`
980981 : ""}`,
test/keys-map.test.ts added +73
@@ -0,0 +1,73 @@
1 /** The map's ready/blocked issue rows join the shell's j/k row traversal: one `data-nav` row each, none when empty (#10342). */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtempSync, rmSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { openIssue } from "../src/actions/issues.ts";
7 import { createRepo } from "../src/actions/repos.ts";
8 import { createApp, type App } from "../src/app.ts";
9 import { addDependency } from "../src/core/dependencies.ts";
10 import { createUser } from "../src/core/users.ts";
11 import { serve, type Servers } from "../src/http/server.ts";
12 import { quietLogs } from "./helpers/globals.ts";
13
14 quietLogs();
15
16 let root = "";
17 let app: App;
18 let servers: Servers;
19 let cookie = "";
20
21 beforeAll(async () => {
22 root = mkdtempSync(join(tmpdir(), "forge-map-keys-"));
23 app = createApp({ dataDir: root, port: 0 });
24 const u = await createUser(app.db, {
25 origin: "seed",
26 trusted: true,
27 username: "alice",
28 email: "[email protected]",
29 password: "a-good-passphrase",
30 adminIfFirst: true,
31 });
32 if (!u.ok) throw new Error(u.error);
33 const alice = u.value;
34 for (const name of ["busy", "bare"]) {
35 const made = await createRepo(app, alice, { name, visibility: "private" });
36 if (!made.ok) throw new Error(made.error);
37 if (name === "bare") continue;
38 const a = await openIssue(app, alice, made.value, { title: "First thing" });
39 const b = await openIssue(app, alice, made.value, { title: "Second thing" });
40 if (!a.ok || !b.ok) throw new Error("issue");
41 const dep = addDependency(app.db, b.value, a.value, alice.id);
42 if (!dep.ok) throw new Error(dep.error);
43 }
44 servers = await serve(app);
45 const res = await fetch(`${servers.url}/signin`, {
46 method: "POST",
47 headers: { origin: servers.url },
48 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
49 redirect: "manual",
50 });
51 cookie = (res.headers.get("set-cookie") ?? "").split(";")[0]!;
52 });
53 afterAll(() => {
54 servers.stop();
55 app.close();
56 rmSync(root, { recursive: true, force: true });
57 });
58
59 const get = async (path: string) =>
60 (await fetch(`${servers.url}${path}`, { headers: { cookie } })).text();
61
62 test("each ranked issue row is a data-nav link to the issue's own page", async () => {
63 const text = await get("/alice/busy/map");
64 const navs = text.match(/<a [^>]*data-nav[^>]*>/g) ?? [];
65 expect(navs.length).toBeGreaterThanOrEqual(2);
66 expect(navs.some((a) => a.includes('href="/alice/busy/issues/1"'))).toBe(true);
67 expect(navs.every((a) => a.includes('tabindex="-1"'))).toBe(true);
68 });
69
70 test("a map with no issues has no data-nav", async () => {
71 const text = await get("/alice/bare/map");
72 expect(text).not.toContain("data-nav");
73 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs