/** * 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<string | null>(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 ( <PullRequestCandidatePicker icon={<UserPlusIcon className="size-3.5" />} 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) => ( <> <PullRequestActorLabel actor={candidate} className="min-w-0 flex-1 truncate" /> {candidate.kind === "team" ? ( <span className="shrink-0 text-muted-foreground">team</span> ) : null} {candidate.isRequested ? ( <CheckIcon aria-label="Already asked" className="size-3.5 shrink-0" /> ) : null} </> )} </PullRequestCandidatePicker> );}
/**
* 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<string | null>(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 (
<PullRequestCandidatePicker
icon={<UserPlusIcon className="size-3.5" />}
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) => (
<>
<PullRequestActorLabel actor={candidate} className="min-w-0 flex-1 truncate" />
{candidate.kind === "team" ? (
<span className="shrink-0 text-muted-foreground">team</span>
) : null}
{candidate.isRequested ? (
<CheckIcon aria-label="Already asked" className="size-3.5 shrink-0" />
) : null}
</>
)}
</PullRequestCandidatePicker>
);
}