1import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip";
2import type { EnvironmentId, PullRequestRef, PullRequestStackMembership } from "@t3tools/contracts";
3import { useState } from "react";
4import { usePullRequestStack } from "~/state/usePullRequestStack";
5import { Menu, MenuTrigger, MenuPopup, MenuGroup, MenuGroupLabel, MenuItem } from "../ui/menu";
6import { PullRequestStackLayers } from "./PullRequestStackLayers";
7import { PullRequestStackHeader } from "./PullRequestStackHeader";
8import { PullRequestGlyph } from "./pullRequestIcons";
9
10/** Mounted only while the menu is open, so list rows do not each fetch a stack. */
11function StackBody({
12 environmentId,
13 reference,
14 onSelect,
15 stackNumber,
16}: {
17 environmentId: EnvironmentId;
18 reference: PullRequestRef;
19 onSelect: (reference: PullRequestRef) => void;
20 stackNumber: number;
21}) {
22 const query = usePullRequestStack(environmentId, reference);
23 if (query.data !== null) {
24 return (
25 <>
26 <PullRequestStackHeader
27 number={query.data.number}
28 notice={query.notice}
29 stale={!!query.error}
30 />
31 {query.error ? <MenuItem onClick={query.refresh}>Retry stack refresh</MenuItem> : null}
32 <PullRequestStackLayers stack={query.data} reference={reference} onSelect={onSelect} />
33 </>
34 );
35 }
36 return (
37 <>
38 <PullRequestStackHeader number={stackNumber} />
39 <MenuGroupLabel>
40 {query.error ??
41 (query.isPending ? "Loading stack…" : "This pull request is no longer in a stack.")}
42 </MenuGroupLabel>
43 </>
44 );
45}
46
47export function PullRequestStackPopover({
48 environmentId,
49 reference,
50 membership,
51 onSelect,
52}: {
53 environmentId: EnvironmentId;
54 reference: PullRequestRef;
55 membership: PullRequestStackMembership;
56 onSelect: (reference: PullRequestRef) => void;
57}) {
58 const [open, setOpen] = useState(false);
59 return (
60 <Menu open={open} onOpenChange={setOpen}>
61 <Tooltip>
62 <TooltipTrigger
63 render={
64 <MenuTrigger
65 nativeButton={false}
66 render={
67 <span
68 role="button"
69 tabIndex={0}
70 className="inline-flex shrink-0 cursor-pointer items-center gap-1 text-xs font-normal text-muted-foreground"
71 />
72 }
73 aria-label={`Stack ${membership.number}, layer ${membership.position} of ${membership.size}`}
74 onClick={(event) => event.stopPropagation()}
75 onKeyDown={(event) => event.stopPropagation()}
76 >
77 <PullRequestGlyph.stack aria-hidden className="size-3" />
78 {membership.position}/{membership.size}
79 </MenuTrigger>
80 }
81 />
82 <TooltipPopup>
83 View stack #{membership.number}, layer {membership.position} of {membership.size}
84 </TooltipPopup>
85 </Tooltip>
86 <MenuPopup
87 align="start"
88 onClick={(event) => event.stopPropagation()}
89 onKeyDown={(event) => event.stopPropagation()}
90 >
91 <MenuGroup>
92 {open ? (
93 <StackBody
94 environmentId={environmentId}
95 reference={reference}
96 stackNumber={membership.number}
97 onSelect={(target) => {
98 setOpen(false);
99 onSelect(target);
100 }}
101 />
102 ) : null}
103 </MenuGroup>
104 </MenuPopup>
105 </Menu>
106 );
107}