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 { 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 type { ChangeRequestTerminology } from "~/sourceControlPresentation"; import { PullRequestStackLayers } from "./PullRequestStackLayers"; import { PullRequestStackHeader } from "./PullRequestStackHeader"; import { PullRequestStackLayerContent } from "./PullRequestStackLayerContent"; import { PullRequestGlyph } from "./pullRequestIcons"; import { readableFailure } from "./pullRequestDetail.logic"; export function PullRequestStackMenu({ hostLabel, terminology, stack, reference, environmentId, stale, canMerge, canRebase, mergeMethod, onSelect, onActed, notice, onRetry, }: { hostLabel: string; terminology: ChangeRequestTerminology; notice?: string | null; onRetry?: (() => void) | undefined; stack: PullRequestStack; reference: PullRequestRef; environmentId: EnvironmentId; stale: boolean; 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 || stale || selectedLayer?.state !== "open" || mergeLayers.some((layer) => !layer.headSha) || mergeHasClosed || mergeLayers.length === 0 || mergeLayers.some((layer) => layer.isDraft); const rebaseDisabled = pending || stale || 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); setConfirmation(null); const toastId = toastManager.add({ type: "loading", title: action === "merge" ? "Merging stack..." : "Rebasing stack...", }); const result = await runAction({ environmentId, input: { ...reference, number: target.number, stackNumber: stack.number, expectedStackHeads: actionHeads, action, ...(action === "merge" ? { mergeMethod } : { updateMethod: "rebase" }), }, }); setPending(false); onActed(); if (result._tag === "Failure") { toastManager.update(toastId, { type: "error", title: action === "merge" ? "Could not merge the stack" : "Could not rebase the stack", description: readableFailure( squashAtomCommandFailure(result), action === "merge" ? "The host refused the merge. Check that you have write access, that the checks it requires have passed, and that the branch is not conflicting." : "The host refused the rebase. A rebase stops at the first commit that does not apply cleanly.", ), }); } else { toastManager.update(toastId, { type: "success", title: action === "merge" ? "Stack merge request completed" : "Stack rebased", description: action === "merge" ? `${hostLabel} 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" ? ( } /> {stale ? notice : `Merge stack through #${reference.number} into ${stack.base} (${mergeLayers.length} ${mergeLayers.length === 1 ? terminology.singular : terminology.plural})`} ) : null} { if (!value) setConfirmation(null); }} > {confirmation === "merge" ? `Merge ${mergeLayers.length} ${mergeLayers.length === 1 ? terminology.singular : terminology.plural}?` : `Rebase ${unmerged.length} ${unmerged.length === 1 ? terminology.singular : terminology.plural}?`} {confirmation === "merge" ? `Merge #${reference.number} and its unmerged layers below into ${stack.base} using ${mergeMethod}. ${hostLabel} 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) => (
  • ))}
); }