1import type { ContextMenuItem } from "@t3tools/contracts";
2import {
3 type SourceControlClientDefinition,
4 UNKNOWN_SOURCE_CONTROL_CLIENT,
5} from "@t3tools/client-runtime/source-control-clients";
6
7import { writeTextToClipboard } from "~/hooks/useCopyToClipboard";
8import { readLocalApi } from "~/localApi";
9
10import { toastManager } from "../ui/toast";
11
12export type PullRequestLinkContextMenuAction = "copy-link" | "open-external";
13
14/** Named for the host rather than "externally": the point is where you will land. */
15export const openOnHostLabel = (host: SourceControlClientDefinition): string =>
16 host === UNKNOWN_SOURCE_CONTROL_CLIENT ? "Open on host" : `Open on ${host.label}`;
17
18/** Copy first: it is the reason to right-click a number rather than click it. */
19function pullRequestLinkContextMenuItems(
20 openLabel: string,
21): readonly ContextMenuItem<PullRequestLinkContextMenuAction>[] {
22 return [
23 { id: "copy-link", label: "Copy link", icon: "copy" },
24 { id: "open-external", label: openLabel },
25 ];
26}
27
28/**
29 * The right-click on a change request's number. Everywhere else that number is written it is a
30 * link, and the gesture that copies a link is the one hand reaches for — so without this the
31 * platform's own edit menu opens over a control that has nothing to cut, paste or select.
32 *
33 * The host is named by the caller rather than guessed here: the same number belongs to GitHub,
34 * GitLab, Bitbucket or Azure DevOps depending on where it was read, and the `url` the contract
35 * carries is already whichever of them it came from.
36 */
37export async function showPullRequestLinkContextMenu({
38 url,
39 openLabel,
40 position,
41}: {
42 readonly url: string;
43 readonly openLabel: string;
44 readonly position: { readonly x: number; readonly y: number };
45}): Promise<void> {
46 const api = readLocalApi();
47 if (!api) return;
48 let action: PullRequestLinkContextMenuAction | null = null;
49 try {
50 action = await api.contextMenu.show(pullRequestLinkContextMenuItems(openLabel), position);
51 } catch {
52 // A menu that could not be shown has already cost the reader their right-click; there is
53 // nothing to say about it that a second popup would not make worse.
54 return;
55 }
56 try {
57 if (action === "copy-link") await writeTextToClipboard(url, "link");
58 else if (action === "open-external") await api.shell.openExternal(url);
59 } catch {
60 toastManager.add({
61 type: "error",
62 title: action === "copy-link" ? "Could not copy the link" : "Could not open the link",
63 });
64 }
65}