import { useMemo } from "react";import { sourceControlRepositorySelector } from "@t3tools/shared/sourceControl";import type { EnvironmentId, ScopedThreadRef, ThreadLinkedPullRequest, ThreadPullRequestLink,} from "@t3tools/contracts";import { isAtomCommandInterrupted, squashAtomCommandFailure,} from "@t3tools/client-runtime/state/runtime";import { planThreadPullRequestMutation, threadPullRequestLinkMode,} from "@t3tools/client-runtime/thread-pull-request-compatibility";import { threadPullRequestKeysEqual, visibleThreadPullRequests,} from "@t3tools/shared/threadPullRequests";import { findProjectForChangeRequest, findProjectOnChangeRequestHost, matchesLinkedPullRequestUrl, parseChangeRequestUrl,} from "~/lib/openPullRequestLink";import { useProjects, useServerConfigs } from "~/state/entities";import { threadEnvironment } from "~/state/threads";import { useAtomCommand } from "~/state/use-atom-command"; /** Routes link actions through the command advertised by this environment. */export function usePullRequestLinking(environmentId: EnvironmentId | null | undefined) { const configs = useServerConfigs(); const projects = useProjects(); const capabilities = environmentId == null ? undefined : configs.get(environmentId)?.environment.capabilities; const mode = threadPullRequestLinkMode(capabilities); const link = useAtomCommand(threadEnvironment.linkPullRequest, { reportFailure: false }); const unlink = useAtomCommand(threadEnvironment.unlinkPullRequest, { reportFailure: false }); const updateMetadata = useAtomCommand(threadEnvironment.updateMetadata, { reportFailure: false }); return useMemo(() => { const environmentProjects = projects.filter( (project) => project.environmentId === environmentId, ); const canLink = (url: string) => { const parsed = parseChangeRequestUrl(url); if (parsed === null || mode === "unsupported") return false; return ( (mode === "multiple" ? findProjectOnChangeRequestHost : findProjectForChangeRequest)( environmentProjects, parsed, ) !== undefined ); }; const isLinked = ( thread: { readonly pullRequests?: readonly ThreadPullRequestLink[]; readonly linkedPullRequest?: ThreadLinkedPullRequest | null | undefined; } | null, url: string, ) => { if (thread === null || mode === "unsupported") return false; if (mode !== "multiple") return ( thread.linkedPullRequest != null && matchesLinkedPullRequestUrl(thread.linkedPullRequest, url) ); const parsed = parseChangeRequestUrl(url); return ( parsed !== null && visibleThreadPullRequests(thread.pullRequests ?? []).some((entry) => threadPullRequestKeysEqual(entry, parsed), ) ); }; const changeLink = async (threadRef: ScopedThreadRef, url: string, linked: boolean) => { const parsed = parseChangeRequestUrl(url); if (parsed === null || threadRef.environmentId !== environmentId || (linked && !canLink(url))) throw new Error("The pull request is not available in this environment."); const legacyProject = findProjectForChangeRequest(environmentProjects, parsed); const mutation = planThreadPullRequestMutation({ capabilities, threadId: threadRef.threadId, reference: { ...parsed, url }, legacyProjectId: legacyProject?.id ?? null, legacyRepository: sourceControlRepositorySelector(legacyProject?.repositoryIdentity) ?? undefined, linked, }); if (mutation === null) throw new Error("This environment does not support linking this pull request."); const result = await (mutation.type === "thread.meta.update" ? updateMetadata({ environmentId: threadRef.environmentId, input: mutation.input }) : mutation.type === "thread.pull-request.link" ? link({ environmentId: threadRef.environmentId, input: mutation.input }) : unlink({ environmentId: threadRef.environmentId, input: mutation.input })); if (result._tag === "Failure") { if (isAtomCommandInterrupted(result)) throw new Error("Link update interrupted."); throw squashAtomCommandFailure(result); } }; return { mode, canLink, isLinked, changeLink }; }, [capabilities, environmentId, link, mode, projects, unlink, updateMetadata]);}
import { useMemo } from "react";
import { sourceControlRepositorySelector } from "@t3tools/shared/sourceControl";
import type {
EnvironmentId,
ScopedThreadRef,
ThreadLinkedPullRequest,
ThreadPullRequestLink,
} from "@t3tools/contracts";
import {
isAtomCommandInterrupted,
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import {
planThreadPullRequestMutation,
threadPullRequestLinkMode,
} from "@t3tools/client-runtime/thread-pull-request-compatibility";
import {
threadPullRequestKeysEqual,
visibleThreadPullRequests,
} from "@t3tools/shared/threadPullRequests";
import {
findProjectForChangeRequest,
findProjectOnChangeRequestHost,
matchesLinkedPullRequestUrl,
parseChangeRequestUrl,
} from "~/lib/openPullRequestLink";
import { useProjects, useServerConfigs } from "~/state/entities";
import { threadEnvironment } from "~/state/threads";
import { useAtomCommand } from "~/state/use-atom-command";
/** Routes link actions through the command advertised by this environment. */
export function usePullRequestLinking(environmentId: EnvironmentId | null | undefined) {
const configs = useServerConfigs();
const projects = useProjects();
const capabilities =
environmentId == null ? undefined : configs.get(environmentId)?.environment.capabilities;
const mode = threadPullRequestLinkMode(capabilities);
const link = useAtomCommand(threadEnvironment.linkPullRequest, { reportFailure: false });
const unlink = useAtomCommand(threadEnvironment.unlinkPullRequest, { reportFailure: false });
const updateMetadata = useAtomCommand(threadEnvironment.updateMetadata, { reportFailure: false });
return useMemo(() => {
const environmentProjects = projects.filter(
(project) => project.environmentId === environmentId,
);
const canLink = (url: string) => {
const parsed = parseChangeRequestUrl(url);
if (parsed === null || mode === "unsupported") return false;
return (
(mode === "multiple" ? findProjectOnChangeRequestHost : findProjectForChangeRequest)(
environmentProjects,
parsed,
) !== undefined
);
};
const isLinked = (
thread: {
readonly pullRequests?: readonly ThreadPullRequestLink[];
readonly linkedPullRequest?: ThreadLinkedPullRequest | null | undefined;
} | null,
url: string,
) => {
if (thread === null || mode === "unsupported") return false;
if (mode !== "multiple")
return (
thread.linkedPullRequest != null &&
matchesLinkedPullRequestUrl(thread.linkedPullRequest, url)
);
const parsed = parseChangeRequestUrl(url);
return (
parsed !== null &&
visibleThreadPullRequests(thread.pullRequests ?? []).some((entry) =>
threadPullRequestKeysEqual(entry, parsed),
)
);
};
const changeLink = async (threadRef: ScopedThreadRef, url: string, linked: boolean) => {
const parsed = parseChangeRequestUrl(url);
if (parsed === null || threadRef.environmentId !== environmentId || (linked && !canLink(url)))
throw new Error("The pull request is not available in this environment.");
const legacyProject = findProjectForChangeRequest(environmentProjects, parsed);
const mutation = planThreadPullRequestMutation({
capabilities,
threadId: threadRef.threadId,
reference: { ...parsed, url },
legacyProjectId: legacyProject?.id ?? null,
legacyRepository:
sourceControlRepositorySelector(legacyProject?.repositoryIdentity) ?? undefined,
linked,
});
if (mutation === null)
throw new Error("This environment does not support linking this pull request.");
const result = await (mutation.type === "thread.meta.update"
? updateMetadata({ environmentId: threadRef.environmentId, input: mutation.input })
: mutation.type === "thread.pull-request.link"
? link({ environmentId: threadRef.environmentId, input: mutation.input })
: unlink({ environmentId: threadRef.environmentId, input: mutation.input }));
if (result._tag === "Failure") {
if (isAtomCommandInterrupted(result)) throw new Error("Link update interrupted.");
throw squashAtomCommandFailure(result);
}
};
return { mode, canLink, isLinked, changeLink };
}, [capabilities, environmentId, link, mode, projects, unlink, updateMetadata]);
}