/** * 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 { Button } from "../ui/button"; import { Input } from "../ui/input"; import { Menu, MenuPopup, MenuTrigger } from "../ui/menu"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; import { toastManager } from "../ui/toast"; import { PullRequestPeopleGhost } from "./PullRequestGhosts"; 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, onRequested, }: { 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; /** The detail carries who is requested, so it is re-read once the host has taken the change. */ onRequested: () => void; }) { 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}`, }); onRequested(); candidatesQuery.refresh(); }; if (!allowed) { return ( } /> Asking someone to review needs write access on this repository ); } return ( } />
setQuery(event.currentTarget.value)} placeholder="Search people with access" aria-label="Search people with access" size="compact" />
{candidatesQuery.isPending ? ( ) : candidatesQuery.error !== null ? (

The people with access could not be read. {candidatesQuery.error}

) : candidates.length === 0 ? (

{query.length > 0 ? "Nobody with access matches that." : "Nobody else has access to this repository."}

) : ( candidates.map((candidate) => ( )) )} {candidatesQuery.data?.truncated ? ( // Typing filters what arrived; it does not ask the host again, so this says what the // list is rather than offering a search that would find nothing further.

This repository has more people with access than are listed here. Ask for the rest on the host.

) : null}
); }