import { Tooltip, TooltipTrigger, TooltipPopup } from "../ui/tooltip"; import type { EnvironmentId, PullRequestRef, PullRequestStack, PullRequestMergeMethod, } from "@t3tools/contracts"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import { GitMergeIcon, LayersIcon, RefreshCwIcon, TriangleAlertIcon } from "lucide-react"; import { useState } from "react"; import { useAtomCommand } from "~/state/use-atom-command"; import { pullRequestEnvironment } from "~/state/pullRequests"; import { Button } from "../ui/button"; import { Menu, MenuPopup, MenuTrigger, MenuItem, MenuGroup, MenuSeparator } from "../ui/menu"; import { Dialog, DialogPopup, DialogTitle, DialogDescription, DialogHeader, DialogPanel, DialogFooter, } from "../ui/dialog"; import { toastManager } from "../ui/toast"; import { PullRequestStackLayers } from "./PullRequestStackLayers"; import { PullRequestStackHeader } from "./PullRequestStackHeader"; import { PullRequestStackLayerContent } from "./PullRequestStackLayerContent"; export function PullRequestStackMenu({ stack, reference, environmentId, canMerge, canRebase, mergeMethod, onSelect, onActed, notice, onRetry, }: { notice?: string | null; onRetry?: (() => void) | undefined; stack: PullRequestStack; reference: PullRequestRef; environmentId: EnvironmentId; canMerge: boolean; canRebase: boolean; mergeMethod: PullRequestMergeMethod; onSelect?: ((reference: PullRequestRef) => void) | undefined; onActed: () => void; }) { const [open, setOpen] = useState(false); const [confirmation, setConfirmation] = useState<"merge" | "update-branch" | null>(null); const [pending, setPending] = useState(false); const runAction = useAtomCommand(pullRequestEnvironment.runAction, { reportFailure: false }); const top = stack.layers.at(-1); const unmerged = stack.layers.filter((layer) => layer.state !== "merged"); const hasClosed = unmerged.some((layer) => layer.state !== "open"); const position = stack.layers.findIndex((layer) => layer.number === reference.number) + 1; const mergeLayers = stack.layers.slice(0, position).filter((layer) => layer.state !== "merged"); const selectedLayer = stack.layers[position - 1]; const mergeHasClosed = mergeLayers.some((layer) => layer.state !== "open"); const expectedStackHeads = unmerged.flatMap((layer) => layer.headSha ? [{ number: layer.number, headSha: layer.headSha }] : [], ); const hasUnknownHead = expectedStackHeads.length !== unmerged.length; const mergeDisabled = pending || selectedLayer?.state !== "open" || mergeLayers.some((layer) => !layer.headSha) || mergeHasClosed || mergeLayers.length === 0 || mergeLayers.some((layer) => layer.isDraft); const rebaseDisabled = pending || hasUnknownHead || hasClosed || unmerged.length === 0; const run = async () => { if ( pending || !confirmation || (confirmation === "merge" ? !canMerge || mergeDisabled : !canRebase || rebaseDisabled) ) return; const action = confirmation; const target = action === "merge" ? selectedLayer : top; if (!target?.headSha) return; const actionHeads = (action === "merge" ? mergeLayers : unmerged).flatMap((layer) => layer.headSha ? [{ number: layer.number, headSha: layer.headSha }] : [], ); setPending(true); const result = await runAction({ environmentId, input: { ...reference, number: target.number, stackNumber: stack.number, expectedStackHeads: actionHeads, action, ...(action === "merge" ? { mergeMethod } : { updateMethod: "rebase" }), }, }); setPending(false); setConfirmation(null); onActed(); if (result._tag === "Failure") { toastManager.add({ type: "error", title: "Stack operation did not complete", description: String(squashAtomCommandFailure(result)), }); } else { toastManager.add({ type: "success", title: action === "merge" ? "Stack merge request completed" : "Stack rebased", description: action === "merge" ? "GitHub merged the stack or added it to its merge queue." : undefined, }); } }; const confirmationLayers = confirmation === "merge" ? mergeLayers : unmerged; return ( <>
{canMerge && selectedLayer?.state === "open" ? (