/** * Asking someone to review, from the row that says who is already reviewing. * * The people who may be asked are read only once this menu opens: on a large repository that is * a list of everyone with access, which is worth a request when somebody wants it and worth * nothing on every pull request they merely open. */ import type { EnvironmentId, PullRequestRef, PullRequestReviewerCandidate, } from "@t3tools/contracts"; import { CheckIcon, UserPlusIcon } from "lucide-react"; import { useMemo, useState } from "react"; import { pullRequestEnvironment } from "~/state/pullRequests"; import { useEnvironmentQuery } from "~/state/query"; import { useAtomCommand } from "~/state/use-atom-command"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import { toastManager } from "../ui/toast"; import { PullRequestCandidatePicker } from "./PullRequestCandidatePicker"; import { PullRequestActorLabel } from "./pullRequestPresentation"; import { readableFailure } from "./pullRequestDetail.logic"; /** Long lists are common — an organisation repository lists everyone — so what arrived can be * narrowed here. It narrows only what arrived: the host is asked once, when the menu opens. */ function matches(candidate: PullRequestReviewerCandidate, query: string): boolean { if (query.length === 0) return true; const needle = query.toLowerCase(); return ( candidate.login.toLowerCase().includes(needle) || (candidate.name ?? "").toLowerCase().includes(needle) ); } export function PullRequestReviewerPicker({ environmentId, reference, allowed, }: { environmentId: EnvironmentId; reference: PullRequestRef; /** False where the host would refuse this account's request, which is worth saying rather than * hiding: the control disabled with a reason answers the question its absence would raise. */ allowed: boolean; }) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const [pending, setPending] = useState(null); // Mounted with the menu closed, so nothing is asked of the host until it opens. const candidatesQuery = useEnvironmentQuery( open ? pullRequestEnvironment.reviewerCandidates({ environmentId, input: reference }) : null, ); const requestReviewers = useAtomCommand(pullRequestEnvironment.requestReviewers, { reportFailure: false, }); const candidates = useMemo( () => (candidatesQuery.data?.candidates ?? []).filter((entry) => matches(entry, query)), [candidatesQuery.data, query], ); const toggle = async (candidate: PullRequestReviewerCandidate) => { if (pending !== null) return; setPending(candidate.id); const result = await requestReviewers({ environmentId, input: { ...reference, reviewers: [{ id: candidate.id, kind: candidate.kind }], requested: !candidate.isRequested, }, }); setPending(null); if (result._tag === "Failure") { toastManager.add({ type: "error", title: candidate.isRequested ? `Could not take back the review request to ${candidate.login}` : `Could not ask ${candidate.login} for a review`, description: readableFailure( squashAtomCommandFailure(result), "The host refused it. Check that you have write access on this repository, and that they still have access to it.", ), }); return; } toastManager.add({ type: "success", title: candidate.isRequested ? `Review request to ${candidate.login} taken back` : `Review requested from ${candidate.login}`, }); }; return ( } label="Request a review" allowed={allowed} disabledReason="Asking someone to review needs write access on this repository" open={open} onOpenChange={setOpen} query={query} onQueryChange={setQuery} searchLabel="Search people with access" isPending={candidatesQuery.isPending && candidatesQuery.data === null} error={candidatesQuery.data === null ? candidatesQuery.error : null} candidates={candidates} emptyLabel="Nobody else has access to this repository." noMatchLabel="Nobody with access matches that." errorLabel="The people with access could not be read." truncated={candidatesQuery.data?.truncated === true} truncatedLabel="This repository has more people with access than are listed here. Ask for the rest on the host." candidateKey={(candidate) => `${candidate.kind}:${candidate.id}`} disabled={pending !== null} onSelect={(candidate) => void toggle(candidate)} > {(candidate) => ( <> {candidate.kind === "team" ? ( team ) : null} {candidate.isRequested ? ( ) : null} )} ); }