"use client"; import { Button } from "@/components/ui/button"; import { HelpTip } from "@/components/ui/help-tip"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { ArrowDown, ArrowUp, Plus, X } from "lucide-react"; import type { EnvironmentRung } from "@/types"; interface EnvironmentLadderEditorProps { // null/empty => degenerate 1-rung ladder synthesized from default_branch. rungs: EnvironmentRung[] | null; onChange: (rungs: EnvironmentRung[] | null) => void; } // An empty editor (no rungs) means "inherit default_branch" via the backend // shim, so we keep a plain array internally and emit null when it empties. function toRungs(value: EnvironmentRung[] | null): EnvironmentRung[] { return value ?? []; } export function EnvironmentLadderEditor({ rungs, onChange, }: EnvironmentLadderEditorProps) { const items = toRungs(rungs); const emit = (next: EnvironmentRung[]) => { onChange(next.length ? next : null); }; const handleAdd = () => { emit([...items, { name: "", branch: "" }]); }; const handleRemove = (index: number) => { emit(items.filter((_, i) => i !== index)); }; const handleUpdate = (index: number, field: keyof EnvironmentRung, value: string) => { const next = items.map((r, i) => (i === index ? { ...r, [field]: value } : r)); emit(next); }; const handleMove = (index: number, direction: -1 | 1) => { const target = index + direction; if (target < 0 || target >= items.length) return; const next = [...items]; [next[index], next[target]] = [next[target], next[index]]; emit(next); }; return (
{items.length} rung{items.length !== 1 ? "s" : ""}
{items.length > 0 && (
{items.map((rung, index) => { const isFirst = index === 0; const isLast = index === items.length - 1; const role = isFirst ? isLast ? "PRs + release" : "PRs land" : isLast ? "release" : ""; return (
{!isFirst && (
promotes to
)}
{/* span-wrap: disabled Button has pointer-events-none, so the tooltip needs a hoverable wrapper to fire when isFirst disables the button. */} {isFirst ? "Already first — lands PRs, nothing to promote from." : "Move earlier in the flow"} {isLast ? "Already last — the release target, nothing further to promote to." : "Move later in the flow"}
{role} handleUpdate(index, "name", e.target.value)} placeholder="Name (e.g. dev, qa, stag)" className="h-8" /> handleUpdate(index, "branch", e.target.value)} placeholder="Branch (e.g. dev, master)" className="h-8" /> Remove this rung
); })}
)}

Top to bottom is the promotion flow: PRs land on the first branch, each rung promotes to the next, and{" "} releases are cut from the last — e.g.{" "} dev → qa → staging → prod. Leave empty to use{" "} default branch for everything; when set, this overrides it for both the PR target and the release target.

); }