import type { EnvironmentId, PullRequestCheck, PullRequestChecksState, PullRequestRef, ScopedThreadRef,} from "@t3tools/contracts"; import { useOpenLink } from "~/browser/useOpenLink";import { cn } from "~/lib/utils";import { pullRequestEnvironment } from "~/state/pullRequests";import { useEnvironmentQuery } from "~/state/query"; import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import { toastManager } from "../ui/toast";import { PullRequestCheckStatusIcon, pullRequestCheckStatusLabel, pullRequestChecksStatePresentation, summarizePullRequestChecks,} from "./pullRequestPresentation"; /** * The checks behind the rollup, for a row that only carries the rollup. Mounted by the popup, so * the read starts when somebody opens it rather than once per row of a listing — the detail read * is a request per pull request, and a page of them at rest would be a hundred. */function LazyChecksBody({ environmentId, reference, threadRef,}: { environmentId: EnvironmentId; reference: PullRequestRef; threadRef: ScopedThreadRef | null;}) { const detailQuery = useEnvironmentQuery( pullRequestEnvironment.detail({ environmentId, input: reference }), ); if (detailQuery.error !== null) { return <p className="text-muted-foreground text-xs">{detailQuery.error}</p>; } if (detailQuery.data === null) { return ( <p className="text-muted-foreground text-xs"> {detailQuery.isPending ? "Loading checks…" : "No checks reported"} </p> ); } return <ChecksBody checks={detailQuery.data.checks} threadRef={threadRef} />;} function ChecksBody({ checks, threadRef,}: { checks: ReadonlyArray<PullRequestCheck>; threadRef: ScopedThreadRef | null;}) { const openLink = useOpenLink(threadRef); if (checks.length === 0) { return <p className="text-muted-foreground text-xs">No checks reported</p>; } return ( <ul className="flex flex-col gap-1"> {/* Keyed by position as well as by name: the host is the one that decides how many runs share a name, and a repeated key is a rendering fault rather than a wrong list. */} {checks.map((check, index) => ( <li key={`${index}:${check.name}`} className="flex items-center gap-2 text-xs"> <PullRequestCheckStatusIcon status={check.status} /> <Tooltip> <TooltipTrigger render={<span className="min-w-0 flex-1 truncate">{check.name}</span>} /> <TooltipPopup side="top">{check.description ?? check.name}</TooltipPopup> </Tooltip> <span className="shrink-0 text-muted-foreground"> {pullRequestCheckStatusLabel(check)} </span> {check.url === null ? null : ( <button type="button" className="shrink-0 text-primary hover:underline" onClick={() => { if (!check.url) return; void openLink(check.url).catch((error: unknown) => { console.error(error); toastManager.add({ type: "error", title: "Unable to open check details" }); }); }} > Details </button> )} </li> ))} </ul> );} /** * The checks indicator and what it opens, in both places a change request is shown: a listing * row, which knows only the rollup, and the detail header, which is already holding every check. * * `checks` decides between the two. Given them, nothing is read; without them, the popup reads * the detail itself, which is why the row must also say which environment it came from. */export function PullRequestChecksPopover({ checksState, checks, environmentId, reference, threadRef = null, className,}: { checksState: PullRequestChecksState; /** The checks already in hand, for the detail header. Absent on a listing row. */ checks?: ReadonlyArray<PullRequestCheck>; environmentId?: EnvironmentId; reference?: PullRequestRef; /** Thread the popover sits beside; a listing row has none. */ threadRef?: ScopedThreadRef | null; className?: string;}) { const presentation = pullRequestChecksStatePresentation(checksState); // Counts beat the rollup's own wording where they are known, the way GitHub's own header reads. const summary = checks === undefined ? null : summarizePullRequestChecks(checks); return ( <Popover> {/* A listing row is itself a button, so the trigger renders as a span: a nested button is not valid inside one. The click is stopped here so opening the checks does not also select the row it sits on. */} <PopoverTrigger nativeButton={false} render={ <span role="button" tabIndex={0} aria-label={`Checks: ${presentation.label}`} className={cn("inline-flex shrink-0 cursor-pointer items-center", className)} /> } onClick={(event) => event.stopPropagation()} > <presentation.Icon aria-hidden className={cn("size-3.5", presentation.toneClassName)} /> </PopoverTrigger> <PopoverPopup align="start" className="w-80 max-w-full" side="bottom"> <p className="mb-2 font-medium text-sm">{presentation.label}</p> {summary === null ? null : <p className="mb-2 text-muted-foreground text-xs">{summary}</p>} {checks !== undefined ? ( <ChecksBody checks={checks} threadRef={threadRef} /> ) : environmentId !== undefined && reference !== undefined ? ( <LazyChecksBody environmentId={environmentId} reference={reference} threadRef={threadRef} /> ) : null} </PopoverPopup> </Popover> );}
import type {
EnvironmentId,
PullRequestCheck,
PullRequestChecksState,
PullRequestRef,
ScopedThreadRef,
} from "@t3tools/contracts";
import { useOpenLink } from "~/browser/useOpenLink";
import { cn } from "~/lib/utils";
import { pullRequestEnvironment } from "~/state/pullRequests";
import { useEnvironmentQuery } from "~/state/query";
import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import { toastManager } from "../ui/toast";
import {
PullRequestCheckStatusIcon,
pullRequestCheckStatusLabel,
pullRequestChecksStatePresentation,
summarizePullRequestChecks,
} from "./pullRequestPresentation";
/**
* The checks behind the rollup, for a row that only carries the rollup. Mounted by the popup, so
* the read starts when somebody opens it rather than once per row of a listing — the detail read
* is a request per pull request, and a page of them at rest would be a hundred.
*/
function LazyChecksBody({
environmentId,
reference,
threadRef,
}: {
environmentId: EnvironmentId;
reference: PullRequestRef;
threadRef: ScopedThreadRef | null;
}) {
const detailQuery = useEnvironmentQuery(
pullRequestEnvironment.detail({ environmentId, input: reference }),
);
if (detailQuery.error !== null) {
return <p className="text-muted-foreground text-xs">{detailQuery.error}</p>;
}
if (detailQuery.data === null) {
return (
<p className="text-muted-foreground text-xs">
{detailQuery.isPending ? "Loading checks…" : "No checks reported"}
</p>
);
}
return <ChecksBody checks={detailQuery.data.checks} threadRef={threadRef} />;
}
function ChecksBody({
checks,
threadRef,
}: {
checks: ReadonlyArray<PullRequestCheck>;
threadRef: ScopedThreadRef | null;
}) {
const openLink = useOpenLink(threadRef);
if (checks.length === 0) {
return <p className="text-muted-foreground text-xs">No checks reported</p>;
}
return (
<ul className="flex flex-col gap-1">
{/* Keyed by position as well as by name: the host is the one that decides how many runs
share a name, and a repeated key is a rendering fault rather than a wrong list. */}
{checks.map((check, index) => (
<li key={`${index}:${check.name}`} className="flex items-center gap-2 text-xs">
<PullRequestCheckStatusIcon status={check.status} />
<Tooltip>
<TooltipTrigger
render={<span className="min-w-0 flex-1 truncate">{check.name}</span>}
/>
<TooltipPopup side="top">{check.description ?? check.name}</TooltipPopup>
</Tooltip>
<span className="shrink-0 text-muted-foreground">
{pullRequestCheckStatusLabel(check)}
</span>
{check.url === null ? null : (
<button
type="button"
className="shrink-0 text-primary hover:underline"
onClick={() => {
if (!check.url) return;
void openLink(check.url).catch((error: unknown) => {
console.error(error);
toastManager.add({ type: "error", title: "Unable to open check details" });
});
}}
>
Details
</button>
)}
</li>
))}
</ul>
);
}
/**
* The checks indicator and what it opens, in both places a change request is shown: a listing
* row, which knows only the rollup, and the detail header, which is already holding every check.
*
* `checks` decides between the two. Given them, nothing is read; without them, the popup reads
* the detail itself, which is why the row must also say which environment it came from.
*/
export function PullRequestChecksPopover({
checksState,
checks,
environmentId,
reference,
threadRef = null,
className,
}: {
checksState: PullRequestChecksState;
/** The checks already in hand, for the detail header. Absent on a listing row. */
checks?: ReadonlyArray<PullRequestCheck>;
environmentId?: EnvironmentId;
reference?: PullRequestRef;
/** Thread the popover sits beside; a listing row has none. */
threadRef?: ScopedThreadRef | null;
className?: string;
}) {
const presentation = pullRequestChecksStatePresentation(checksState);
// Counts beat the rollup's own wording where they are known, the way GitHub's own header reads.
const summary = checks === undefined ? null : summarizePullRequestChecks(checks);
return (
<Popover>
{/* A listing row is itself a button, so the trigger renders as a span: a nested button is
not valid inside one. The click is stopped here so opening the checks does not also
select the row it sits on. */}
<PopoverTrigger
nativeButton={false}
render={
<span
role="button"
tabIndex={0}
aria-label={`Checks: ${presentation.label}`}
className={cn("inline-flex shrink-0 cursor-pointer items-center", className)}
/>
}
onClick={(event) => event.stopPropagation()}
>
<presentation.Icon aria-hidden className={cn("size-3.5", presentation.toneClassName)} />
</PopoverTrigger>
<PopoverPopup align="start" className="w-80 max-w-full" side="bottom">
<p className="mb-2 font-medium text-sm">{presentation.label}</p>
{summary === null ? null : <p className="mb-2 text-muted-foreground text-xs">{summary}</p>}
{checks !== undefined ? (
<ChecksBody checks={checks} threadRef={threadRef} />
) : environmentId !== undefined && reference !== undefined ? (
<LazyChecksBody
environmentId={environmentId}
reference={reference}
threadRef={threadRef}
/>
) : null}
</PopoverPopup>
</Popover>
);
}