1import type { EnvironmentId, ScopedThreadRef, ThreadPullRequestLink } from "@t3tools/contracts";
2import {
3 isAtomCommandInterrupted,
4 squashAtomCommandFailure,
5} from "@t3tools/client-runtime/state/runtime";
6import {
7 planThreadPullRequestMutation,
8 threadPullRequestLinkMode,
9} from "@t3tools/client-runtime/thread-pull-request-compatibility";
10import {
11 threadPullRequestKeysEqual,
12 visibleThreadPullRequests,
13} from "@t3tools/client-runtime/thread-pull-requests";
14import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients";
15import { useAtomValue } from "@effect/atom-react";
16import { Atom } from "effect/reactivity";
17import { appAtomRegistry } from "~/rpc/atomRegistry";
18import { environmentServerConfigsAtom } from "~/state/server";
19import { threadEnvironment } from "~/state/threads";
20import { useAtomCommand } from "~/state/use-atom-command";
21
22// A primitive mode prevents unchanged status pushes from re-rendering every link.
23const linkModeAtom = Atom.family((environmentId: EnvironmentId | null) =>
24 Atom.make((get) =>
25 threadPullRequestLinkMode(
26 environmentId === null
27 ? undefined
28 : get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities,
29 ),
30 ).pipe(Atom.withLabel("pull-request-link-mode")),
31);
32
33export function usePullRequestLinking(environmentId: EnvironmentId | null | undefined) {
34 const mode = useAtomValue(linkModeAtom(environmentId ?? null));
35 const link = useAtomCommand(threadEnvironment.linkPullRequest, { reportFailure: false });
36 const unlink = useAtomCommand(threadEnvironment.unlinkPullRequest, { reportFailure: false });
37 const canLink = (url: string) =>
38 mode !== "unsupported" && sourceControlClients.parseChangeRequestUrl(url) !== null;
39 const isLinked = (
40 thread: { readonly pullRequests?: readonly ThreadPullRequestLink[] } | null,
41 url: string,
42 ) => {
43 if (thread === null || mode === "unsupported") return false;
44 const parsed = sourceControlClients.parseChangeRequestUrl(url)?.link;
45 return (
46 parsed !== undefined &&
47 visibleThreadPullRequests(thread.pullRequests ?? []).some((entry) =>
48 threadPullRequestKeysEqual(entry, parsed),
49 )
50 );
51 };
52 const changeLink = async (threadRef: ScopedThreadRef, url: string, linked: boolean) => {
53 const parsed = sourceControlClients.parseChangeRequestUrl(url)?.link;
54 if (
55 parsed === undefined ||
56 threadRef.environmentId !== environmentId ||
57 (linked && !canLink(url))
58 )
59 throw new Error("The pull request is not available in this environment.");
60 const mutation = planThreadPullRequestMutation({
61 capabilities: appAtomRegistry.get(environmentServerConfigsAtom).get(threadRef.environmentId)
62 ?.environment.capabilities,
63 threadId: threadRef.threadId,
64 reference: { ...parsed, url },
65 linked,
66 });
67 if (mutation === null)
68 throw new Error("This environment does not support linking this pull request.");
69 const result = await (mutation.type === "thread.pull-request.link"
70 ? link({ environmentId: threadRef.environmentId, input: mutation.input })
71 : unlink({ environmentId: threadRef.environmentId, input: mutation.input }));
72 if (result._tag === "Failure") {
73 if (isAtomCommandInterrupted(result)) throw new Error("Link update interrupted.");
74 throw squashAtomCommandFailure(result);
75 }
76 };
77 return { mode, canLink, isLinked, changeLink };
78}