1import { ThreadDetailsControl } from "./ThreadDetailsControl";
2import type { EnvironmentId, ScopedThreadRef, ThreadPullRequestLink } from "@t3tools/contracts";
3import {
4 resolveThreadPullRequestChains,
5 threadPullRequestKeyOf,
6 visibleThreadPullRequests,
7} from "@t3tools/shared/threadPullRequests";
8import { Minus, Plus } from "lucide";
9import { useState, type ComponentProps, type MouseEvent as ReactMouseEvent } from "react";
10
11import { findProjectOnChangeRequestHost, parseChangeRequestUrl } from "~/lib/openPullRequestLink";
12
13import { useProjects } from "~/state/entities";
14import { threadEnvironment } from "~/state/threads";
15import { useAtomCommand } from "~/state/use-atom-command";
16
17import { pullRequestListLines } from "../pullRequest/pullRequestListLines";
18import { MorphIcon } from "~/components/MorphIcon";
19import { linkedPullRequestSnapshotStatus, prStatusIndicator } from "../ThreadStatusIndicators";
20
21import { ThreadDetailsPrRow } from "./ThreadDetailsPrRow";
22
23function ThreadDetailsPrLinkRow({
24 environmentId,
25 link,
26 onOpen,
27 onActed,
28 onStopWatching,
29}: {
30 environmentId: EnvironmentId;
31 link: ThreadPullRequestLink;
32 onOpen: (event: ReactMouseEvent<HTMLElement>) => void;
33 onActed?: (() => void) | undefined;
34 onStopWatching?: (() => void) | undefined;
35}) {
36 const projects = useProjects();
37 const parsed = parseChangeRequestUrl(link.url);
38 const project =
39 parsed === null
40 ? null
41 : (findProjectOnChangeRequestHost(
42 projects.filter((candidate) => candidate.environmentId === environmentId),
43 parsed,
44 ) ?? null);
45 const linked = linkedPullRequestSnapshotStatus(link);
46 const pr = linked?.pr ?? null;
47 return (
48 <ThreadDetailsPrRow
49 environmentId={environmentId}
50 pr={pr}
51 number={link.number}
52 reference={link}
53 status={prStatusIndicator(pr, linked?.sourceControlProvider)}
54 project={project}
55 label={`#${link.number}${link.snapshot === null ? "" : `: ${link.snapshot.title}`}`}
56 openAriaLabel={link.url}
57 onOpen={onOpen}
58 onStopWatching={onStopWatching}
59 {...(onActed ? { onActed } : {})}
60 />
61 );
62}
63
64export function ThreadDetailsPrRows({
65 threadRef,
66 links,
67 currentLink,
68 onOpenLink,
69 ...row
70}: ComponentProps<typeof ThreadDetailsPrRow> & {
71 threadRef: ScopedThreadRef;
72 links: ReadonlyArray<ThreadPullRequestLink>;
73 currentLink: ThreadPullRequestLink | null;
74 onOpenLink: (event: ReactMouseEvent<HTMLElement>, url: string) => void;
75}) {
76 const [expanded, setExpanded] = useState(false);
77 const watch = useAtomCommand(threadEnvironment.watchPullRequest, { reportFailure: true });
78 // Only watched links get the eye; the row hides it once the server records the stop.
79 const stopWatching = (link: ThreadPullRequestLink | null) =>
80 link?.watch === undefined
81 ? undefined
82 : () =>
83 void watch({
84 environmentId: threadRef.environmentId,
85 input: {
86 threadId: threadRef.threadId,
87 host: link.host,
88 repository: link.repository,
89 number: link.number,
90 watching: false,
91 },
92 });
93 const currentRow = <ThreadDetailsPrRow {...row} onStopWatching={stopWatching(currentLink)} />;
94 const rest =
95 currentLink === null
96 ? []
97 : pullRequestListLines(resolveThreadPullRequestChains(visibleThreadPullRequests(links)))
98 .map((line) => line.link)
99 .filter((link) => threadPullRequestKeyOf(link) !== threadPullRequestKeyOf(currentLink));
100 if (rest.length === 0) return currentRow;
101
102 return (
103 <>
104 {currentRow}
105 {expanded
106 ? rest.map((link) => (
107 <ThreadDetailsPrLinkRow
108 key={threadPullRequestKeyOf(link)}
109 environmentId={row.environmentId}
110 link={link}
111 onOpen={(event) => onOpenLink(event, link.url)}
112 onActed={row.onActed}
113 onStopWatching={stopWatching(link)}
114 />
115 ))
116 : null}
117 <ThreadDetailsControl
118 variant="ghost"
119 size="sm"
120 onClick={() => setExpanded(!expanded)}
121 part="row"
122 tone="muted"
123 className="w-full active:scale-100"
124 >
125 <MorphIcon aria-hidden className="size-4 shrink-0" icon={expanded ? Minus : Plus} />
126 {expanded ? "Show less" : `Show ${rest.length} more`}
127 </ThreadDetailsControl>
128 </>
129 );
130}