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 ( <> } > {position}/{stack.layers.length} {onRetry ? : null} } /> View stack #{stack.number}, layer {position} of {stack.layers.length} {notice ? ` · ${notice}` : null} {onRetry ? Retry stack refresh : null} { setOpen(false); onSelect(target); } : undefined } /> {canMerge || canRebase ? ( <> {canMerge ? ( setConfirmation("merge")}> Merge stack ({mergeLayers.length}) ) : null} {canRebase ? ( setConfirmation("update-branch")} > Rebase stack ) : null} {mergeHasClosed || mergeLayers.some((layer) => layer.isDraft) ? (

Every layer being merged must be open and ready for review.

) : null} ) : null}
{canMerge && selectedLayer?.state === "open" ? ( } /> Merge stack through #{reference.number} into {stack.base} ({mergeLayers.length}{" "} {mergeLayers.length === 1 ? "pull request" : "pull requests"}) ) : null} { if (!value && !pending) setConfirmation(null); }} > {confirmation === "merge" ? `Merge ${mergeLayers.length} pull requests?` : `Rebase ${unmerged.length} pull requests?`} {confirmation === "merge" ? `Merge #${reference.number} and its unmerged layers below into ${stack.base} using ${mergeMethod}. GitHub checks their rules before merging or queueing them and rebases the remaining stack after merging.` : `Rebase the remote branches from bottom to top onto ${stack.base}. This rewrites branch history and may restart checks. If a layer fails, earlier updates remain.`}
    {confirmationLayers.map((layer) => (
  • ))}
); }