| 1 | import type { ContextMenuItem } from "@t3tools/contracts"; |
| 2 | import { |
| 3 | type SourceControlClientDefinition, |
| 4 | UNKNOWN_SOURCE_CONTROL_CLIENT, |
| 5 | } from "@t3tools/client-runtime/source-control-clients"; |
| 6 | |
| 7 | import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; |
| 8 | import { readLocalApi } from "~/localApi"; |
| 9 | |
| 10 | import { toastManager } from "../ui/toast"; |
| 11 | |
| 12 | export type PullRequestLinkContextMenuAction = "copy-link" | "open-external"; |
| 13 | |
| 14 | /** Named for the host rather than "externally": the point is where you will land. */ |
| 15 | export 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. */ |
| 19 | function 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 | */ |
| 37 | export 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 | } |