127 lines · 4.8 KB · TypeScript
RawDownload
/** * Putting a label on, and taking one off, from the row that says which it already wears. * * The repository's labels are read only once this menu opens, for the reason the reviewer menu * reads its people then: they are worth a request when somebody wants them and worth nothing on * every pull request they merely open. */import type { EnvironmentId, PullRequestLabelCandidate, PullRequestRef } from "@t3tools/contracts";import { CheckIcon, TagIcon } 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 { readableFailure } from "./pullRequestDetail.logic";import { pullRequestLabelColor } from "./pullRequestList.logic"; /** Narrows only what arrived: the host is asked once, when the menu opens. */function matches(candidate: PullRequestLabelCandidate, query: string): boolean {  if (query.length === 0) return true;  const needle = query.toLowerCase();  return (    candidate.name.toLowerCase().includes(needle) ||    (candidate.description ?? "").toLowerCase().includes(needle)  );} export function PullRequestLabelPicker({  environmentId,  reference,  allowed,}: {  environmentId: EnvironmentId;  reference: PullRequestRef;  /** False where the host would refuse this account's change. Disabled with the reason rather   * than hidden, like the reviewer control beside it. */  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.labelCandidates({ environmentId, input: reference }) : null,  );  const setLabels = useAtomCommand(pullRequestEnvironment.setLabels, { reportFailure: false });   const candidates = useMemo(    () => (candidatesQuery.data?.candidates ?? []).filter((entry) => matches(entry, query)),    [candidatesQuery.data, query],  );   const toggle = async (candidate: PullRequestLabelCandidate) => {    if (pending !== null) return;    setPending(candidate.name);    const result = await setLabels({      environmentId,      input: { ...reference, labels: [candidate.name], applied: !candidate.isApplied },    });    setPending(null);    if (result._tag === "Failure") {      toastManager.add({        type: "error",        title: candidate.isApplied          ? `Could not take ${candidate.name} off`          : `Could not put ${candidate.name} on`,        description: readableFailure(          squashAtomCommandFailure(result),          "The host refused it. Check that you have triage access on this repository.",        ),      });      return;    }  };   return (    <PullRequestCandidatePicker      icon={<TagIcon className="size-3.5" />}      label="Change labels"      allowed={allowed}      disabledReason="Changing labels needs triage access on this repository"      open={open}      onOpenChange={setOpen}      query={query}      onQueryChange={setQuery}      searchLabel="Search labels"      isPending={candidatesQuery.isPending && candidatesQuery.data === null}      error={candidatesQuery.data === null ? candidatesQuery.error : null}      candidates={candidates}      emptyLabel="This repository has no labels."      noMatchLabel="No label matches that."      errorLabel="The labels could not be read."      truncated={candidatesQuery.data?.truncated === true}      truncatedLabel="This repository has more labels than are listed here. Apply the rest on the host."      candidateKey={(candidate) => candidate.name}      disabled={pending !== null}      onSelect={(candidate) => void toggle(candidate)}    >      {(candidate) => {        const dot = pullRequestLabelColor(candidate.color);        return (          <>            <span              aria-hidden              className="size-2 shrink-0 rounded-full bg-muted-foreground"              {...(dot ? { style: { backgroundColor: dot } } : {})}            />            <span className="min-w-0 flex-1 truncate">              {candidate.name}              {candidate.description ? (                <span className="text-muted-foreground"> · {candidate.description}</span>              ) : null}            </span>            {candidate.isApplied ? (              <CheckIcon aria-label="Applied" className="size-3.5 shrink-0" />            ) : null}          </>        );      }}    </PullRequestCandidatePicker>  );}