Web: the Board reads pull requests in every repository its open items link
It read the first 20 repositories that had live sessions, so an item with an open pull request elsewhere stood a column early, and every item did when sessions could not be read. It reads each repository an open item is linked to, and says the board is partial when one cannot be read.
- G1T-Session:
- ses_01m56vafhge7ttrgs5288dpxvb
3 files+45−120/3 viewed
| 3 | 3 | ||
| 4 | 4 | import type { Claim, ItemRepo, WorkItem, WorkspaceSession } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | − | import { boardOf, pullsByItem, shownStage, stageOf } from "./board.ts"; | |
| 6 | + | import { boardOf, pullRepos, pullsByItem, shownStage, stageOf } from "./board.ts"; | |
| 7 | 7 | ||
| 8 | 8 | const NOW = Date.parse("2026-10-17T12:00:00Z"); | |
| 9 | 9 | const DAY = 86_400_000; | |
| ⋯ | |||
| 115 | 115 | assert.equal(shownStage("nonsense", board), "progress"); | |
| 116 | 116 | assert.equal(shownStage(null, boardOf([], {}, NOW)), "backlog"); | |
| 117 | 117 | }); | |
| 118 | + | ||
| 119 | + | test("the board reads open pull requests in every repository an open work item is linked to, each once", () => { | |
| 120 | + | const items = [ | |
| 121 | + | { state: "open", repos: [{ path: "acme/web" }, { path: "acme/api" }] }, | |
| 122 | + | { state: "open", repos: [{ path: "Acme/Web" }, { path: "acme/repo-21" }] }, | |
| 123 | + | // A closed item's repositories are not read: Done does not need its pull requests. | |
| 124 | + | { state: "closed", repos: [{ path: "acme/old" }] }, | |
| 125 | + | // A repository deleted before work items kept no path. | |
| 126 | + | { state: "open", repos: [{ path: "" }] }, | |
| 127 | + | ]; | |
| 128 | + | assert.deepEqual(pullRepos(items), ["acme/web", "acme/api", "acme/repo-21"]); | |
| 129 | + | // However many there are: none is left out past a limit. | |
| 130 | + | const many = Array.from({ length: 30 }, (_, index) => ({ state: "open", repos: [{ path: `acme/r${index}` }] })); | |
| 131 | + | assert.equal(pullRepos(many).length, 30); | |
| 132 | + | }); | |
| 87 | 87 | return board; | |
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | + | /** | |
| 91 | + | * The repositories whose open pull requests the board reads: every one an | |
| 92 | + | * open work item is linked to, each once, so an item's pull request moves | |
| 93 | + | * it wherever it was opened. A repository deleted before work items, with | |
| 94 | + | * no path kept, has none to read. | |
| 95 | + | */ | |
| 96 | + | export function pullRepos(items: readonly { state: string; repos: readonly { path: string }[] }[]): string[] { | |
| 97 | + | const seen = new Map<string, string>(); | |
| 98 | + | for (const item of items) { | |
| 99 | + | if (item.state !== "open") continue; | |
| 100 | + | for (const repo of item.repos) if (repo.path && !seen.has(repo.path.toLowerCase())) seen.set(repo.path.toLowerCase(), repo.path); | |
| 101 | + | } | |
| 102 | + | return [...seen.values()]; | |
| 103 | + | } | |
| 104 | + | ||
| 90 | 105 | /** Each work item's pull requests' statuses, from pull requests that name their item. */ | |
| 91 | 106 | export function pullsByItem(pulls: { item: number | null; status: PullStatus }[]): ItemPulls { | |
| 92 | 107 | const by: ItemPulls = {}; |
| 11 | 11 | import { Hint } from "../../../components/ui/hint"; | |
| 12 | 12 | import { RepoChips } from "../../../components/work-items"; | |
| 13 | 13 | import { ItemIcon } from "../../../components/work-icons"; | |
| 14 | − | import { type Board, type BoardCard, STAGES, STAGE_INFO, type Stage, boardOf, pullsByItem, shownStage } from "../../../lib/board"; | |
| 14 | + | import { type Board, type BoardCard, STAGES, STAGE_INFO, type Stage, boardOf, pullRepos, pullsByItem, shownStage } from "../../../lib/board"; | |
| 15 | 15 | import { cn } from "../../../lib/cn"; | |
| 16 | 16 | import { page } from "../../../lib/meta"; | |
| 17 | 17 | import { isFailed, part } from "../../../lib/partial"; | |
| ⋯ | |||
| 23 | 23 | import { useWorkspaceLive } from "../../../lib/use-repo-live"; | |
| 24 | 24 | import { itemPath, newItemPath, repoFilter, shortPath, workPath } from "../../../lib/work-items"; | |
| 25 | 25 | ||
| 26 | − | /** How many repositories' open pull requests the board reads. */ | |
| 27 | − | const PULL_REPOS = 20; | |
| 28 | − | ||
| 29 | 26 | export function meta({ params, location, ...args }: Route.MetaArgs) { | |
| 30 | 27 | const repo = new URLSearchParams(location.search).get("repo"); | |
| 31 | 28 | return page({ location, ...args }, { title: `Board${repo ? ` · ${repo}` : ""} · Work · ${params.owner} · g1t` }); | |
| ⋯ | |||
| 46 | 43 | part(work.listWorkspaceSessions(workspace, viewer, { shown: "live", sessionsOnly: true, repo: repoName ?? undefined, limit: 200 }), "Live sessions"), | |
| 47 | 44 | ]); | |
| 48 | 45 | const sessions = isFailed(live) || !live.ok ? [] : live.value.sessions; | |
| 49 | − | // The open pull requests that name their work items, in the repositories looked in. | |
| 50 | − | const repos = isFailed(live) || !live.ok ? [] : live.value.repos.slice(0, PULL_REPOS); | |
| 46 | + | // The open pull requests that name their work items, in every repository an open item is linked to. | |
| 47 | + | const openItems = unwrap(open); | |
| 48 | + | const repos = pullRepos(openItems); | |
| 51 | 49 | const pulls = await Promise.all( | |
| 52 | − | repos.map((one) => { | |
| 53 | − | const [namespace = "", name = ""] = one.path.split("/"); | |
| 50 | + | repos.map((path) => { | |
| 51 | + | const [namespace = "", name = ""] = path.split("/"); | |
| 54 | 52 | return work.listPulls({ namespace, name }, viewer, "open").catch(() => null); | |
| 55 | 53 | }), | |
| 56 | 54 | ); | |
| 57 | 55 | const openPulls: Pull[] = pulls.flatMap((found) => (found?.ok ? found.value : [])); | |
| 56 | + | // A repository whose pull requests could not be read: its items may stand a column early, and the board says so. | |
| 57 | + | const unread = repos.filter((_, index) => !pulls[index]?.ok); | |
| 58 | 58 | const now = Date.now(); | |
| 59 | 59 | const board = boardOf( | |
| 60 | − | [...unwrap(open), ...(isFailed(closed) || !closed.ok ? [] : closed.value)], | |
| 60 | + | [...openItems, ...(isFailed(closed) || !closed.ok ? [] : closed.value)], | |
| 61 | 61 | { claims: isFailed(claims) || !claims.ok ? [] : claims.value, sessions, pulls: pullsByItem(openPulls) }, | |
| 62 | 62 | now, | |
| 63 | 63 | ); | |
| ⋯ | |||
| 68 | 68 | repo: repoName, | |
| 69 | 69 | board, | |
| 70 | 70 | stage: shownStage(query.get("stage"), board), | |
| 71 | − | unavailable: [closed, claims, live].filter(isFailed).map((failed) => failed.what), | |
| 71 | + | unavailable: [ | |
| 72 | + | ...[closed, claims, live].filter(isFailed).map((failed) => failed.what), | |
| 73 | + | ...(unread.length === 0 ? [] : [unread.length === 1 ? `Pull requests in ${unread[0]}` : `Pull requests in ${unread.length} repositories`]), | |
| 74 | + | ], | |
| 72 | 75 | live: sessions.length, | |
| 73 | 76 | }; | |
| 74 | 77 | } | |
| ⋯ | |||
| 193 | 196 | </ButtonLink> | |
| 194 | 197 | )} | |
| 195 | 198 | </div> | |
| 196 | − | {unavailable.length > 0 && <p className="mt-3 text-xs text-warn">{unavailable.join(", ")} could not be loaded just now; the board may be missing some of it.</p>} | |
| 199 | + | {unavailable.length > 0 && <p className="mt-3 text-xs text-warn">{unavailable.join(", ")} could not be loaded just now, so this board is partial: some work items may be missing, or stand a column early.</p>} | |
| 197 | 200 | ||
| 198 | 201 | {total === 0 ? ( | |
| 199 | 202 | <div className="mt-5"> | |
Comments
Sign in to comment.