| 1 | /** |
| 2 | * What `#N` names, asked of the work service (`resolveRef`, the rules in |
| 3 | * lib/refs.ts), as a page to send the browser to or as the card a |
| 4 | * reference shows on hover. |
| 5 | */ |
| 6 | import type { RefTarget, RepoPath, Viewer } from "@g1t/contracts"; |
| 7 | |
| 8 | import { itemPath } from "./refs"; |
| 9 | import { work } from "./services.server"; |
| 10 | |
| 11 | /** Where `#number` is read: a repository, or a workspace alone. */ |
| 12 | export type RefScope = { repo: RepoPath } | { workspace: string }; |
| 13 | |
| 14 | /** |
| 15 | * What a reference's card shows (components/chat/ref-chip.tsx): a work item |
| 16 | * or a pull request, as the viewer may see it, and where it is. |
| 17 | */ |
| 18 | export type RefCard = { |
| 19 | kind: "item" | "pull"; |
| 20 | /** A pull request's repository, `owner/name`; null for a work item. */ |
| 21 | repo: string | null; |
| 22 | /** The workspace a work item is numbered in, or a pull request's repository's. */ |
| 23 | workspace: string; |
| 24 | number: number; |
| 25 | title: string; |
| 26 | state: string; |
| 27 | /** Its page. */ |
| 28 | to: string; |
| 29 | }; |
| 30 | |
| 31 | /** What `#number` names for the viewer, read in `scope`; none when the service cannot say. */ |
| 32 | export async function resolveRef(viewer: Viewer, scope: RefScope, number: number): Promise<RefTarget> { |
| 33 | if (!Number.isInteger(number) || number < 1) return { kind: "none" }; |
| 34 | return work.resolveRef(viewer, scope, number).catch((): RefTarget => ({ kind: "none" })); |
| 35 | } |
| 36 | |
| 37 | /** The page a resolved reference is: a work item's or a pull request's; null for none. */ |
| 38 | export function refTargetPath(target: RefTarget): string | null { |
| 39 | if (target.kind === "work_item") return itemPath(target.workspace, target.number); |
| 40 | if (target.kind === "pull") return `/${target.repo}/pull/${target.number}`; |
| 41 | return null; |
| 42 | } |
| 43 | |
| 44 | /** Where `#number`, read in `scope`, leads; null when it names nothing the viewer can see. */ |
| 45 | export async function refPath(viewer: Viewer, scope: RefScope, number: number): Promise<string | null> { |
| 46 | return refTargetPath(await resolveRef(viewer, scope, number)); |
| 47 | } |
| 48 | |
| 49 | /** The card for `#number` read in `scope`; null when it names nothing the viewer can see. */ |
| 50 | export async function refCard(viewer: Viewer, scope: RefScope, number: number): Promise<RefCard | null> { |
| 51 | const target = await resolveRef(viewer, scope, number); |
| 52 | if (target.kind === "work_item") { |
| 53 | const found = await work.getWorkItem(target.workspace, target.number, viewer).catch(() => null); |
| 54 | if (!found?.ok) return null; |
| 55 | const { item } = found.value; |
| 56 | return { |
| 57 | kind: "item", |
| 58 | repo: null, |
| 59 | workspace: item.workspace, |
| 60 | number: item.number, |
| 61 | title: item.title, |
| 62 | state: item.state === "closed" && item.reason === "not_planned" ? "not planned" : item.state, |
| 63 | to: itemPath(item.workspace, item.number), |
| 64 | }; |
| 65 | } |
| 66 | if (target.kind === "pull") { |
| 67 | const [namespace = "", name = ""] = target.repo.split("/"); |
| 68 | const found = await work.getPull({ namespace, name }, target.number, viewer).catch(() => null); |
| 69 | if (!found?.ok) return null; |
| 70 | return { |
| 71 | kind: "pull", |
| 72 | repo: target.repo, |
| 73 | workspace: namespace.toLowerCase(), |
| 74 | number: target.number, |
| 75 | title: found.value.pull.title, |
| 76 | state: found.value.pull.status, |
| 77 | to: `/${target.repo}/pull/${target.number}`, |
| 78 | }; |
| 79 | } |
| 80 | return null; |
| 81 | } |