1import {
2 isAtomCommandInterrupted,
3 squashAtomCommandFailure,
4} from "@t3tools/client-runtime/state/runtime";
5import { AuthPreviewOperateScope } from "@t3tools/contracts";
6import { useNavigate, useParams } from "@tanstack/react-router";
7import { ListOrderedIcon } from "lucide-react";
8import { useRef, useState } from "react";
9
10import { useRightPanelStore } from "../../rightPanelStore";
11import { previewEnvironment } from "../../state/preview";
12import { useEnvironmentScope } from "../../state/session";
13import { useEnvironment, usePrimaryEnvironmentId } from "../../state/environments";
14import { useAtomCommand } from "../../state/use-atom-command";
15import { resolveThreadRouteRef } from "../../threadRoutes";
16import { readThreadPreviewState } from "../../previewStateStore";
17import { openPreviewSession } from "../preview/openPreviewSession";
18import { SidebarMenuButton, SidebarMenuItem, useSidebar } from "../ui/sidebar";
19import { toastManager } from "../ui/toast";
20import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
21
22export function SidebarMergebotButton() {
23 const navigate = useNavigate();
24 const threadRef = useParams({ strict: false, select: resolveThreadRouteRef });
25 const primaryEnvironmentId = usePrimaryEnvironmentId();
26 const environment = useEnvironment(threadRef?.environmentId ?? primaryEnvironmentId);
27 const configuration = environment?.serverConfig?.mergebot;
28 const origin = configuration ? `${configuration.origin}/` : undefined;
29 const canPreview = useEnvironmentScope(threadRef?.environmentId ?? null, AuthPreviewOperateScope);
30 const openPreview = useAtomCommand(previewEnvironment.open, { reportFailure: false });
31 const { isMobile, setOpenMobile } = useSidebar();
32 const busy = useRef(false);
33 const [pending, setPending] = useState(false);
34 const open = async () => {
35 if (!origin || busy.current) return;
36 busy.current = true;
37 setPending(true);
38 try {
39 if (threadRef && canPreview) {
40 const matching = Object.values(readThreadPreviewState(threadRef).sessions).find(
41 (session) => session.navStatus._tag !== "Idle" && session.navStatus.url === origin,
42 );
43 if (matching) useRightPanelStore.getState().openBrowser(threadRef, matching.tabId);
44 else {
45 const result = await openPreviewSession({ threadRef, url: origin, openPreview });
46 if (isAtomCommandInterrupted(result)) return;
47 if (result._tag === "Failure") throw squashAtomCommandFailure(result);
48 useRightPanelStore.getState().openBrowser(threadRef, result.value.tabId);
49 }
50 } else {
51 const environmentId = threadRef?.environmentId ?? primaryEnvironmentId;
52 if (!environmentId) return;
53 await navigate({
54 to: "/pull-requests",
55 search: { involvement: "all", state: "open", mergebot: environmentId },
56 });
57 }
58 if (isMobile) setOpenMobile(false);
59 } catch (error) {
60 toastManager.add({
61 type: "error",
62 title: "Could not open Mergebot",
63 description:
64 error instanceof Error ? error.message : "Try again when the environment is connected.",
65 });
66 } finally {
67 busy.current = false;
68 setPending(false);
69 }
70 };
71
72 if (!origin) return null;
73
74 return (
75 <SidebarMenuItem className="shrink-0">
76 <Tooltip>
77 <TooltipTrigger
78 render={
79 <SidebarMenuButton
80 aria-label="Mergebot queue"
81 aria-busy={pending}
82 disabled={pending}
83 onClick={() => void open()}
84 size="icon"
85 >
86 <ListOrderedIcon aria-hidden="true" />
87 </SidebarMenuButton>
88 }
89 />
90 <TooltipPopup side="top">Mergebot queue</TooltipPopup>
91 </Tooltip>
92 </SidebarMenuItem>
93 );
94}