import { useCallback, useState } from "react" import { Loader2, Search } from "lucide-react" import { Button } from "~/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog" import { Input } from "~/components/ui/input" import { Label } from "~/components/ui/label" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "~/components/ui/table" import { api } from "~/lib/api" import type { HistoryEntry, MappingInfo } from "~/lib/api" import { resolveLocalPath } from "~/lib/tfs" import { cn, formatDateTime } from "~/lib/utils" /** * 合并向导弹窗:源(当前对象)→ 目标路径(必须已映射), * 加载待合并 changeset 候选后支持「全部合并」或选择单个合并; * 结果为挂起更改,冲突由编排层进入既有冲突弹窗。 */ export function MergeDialog({ sourceServerPath, mappings, onMerge, onClose, }: { sourceServerPath: string mappings: MappingInfo[] /** 执行合并(changeset 为 undefined 表示全部),由编排层调用接口并刷新。 */ onMerge: (targetServerPath: string, changeset: number | undefined) => Promise onClose: () => void }) { const [target, setTarget] = useState("") const [candidates, setCandidates] = useState(null) const [loading, setLoading] = useState(false) const [selected, setSelected] = useState(null) const [busy, setBusy] = useState(false) const [error, setError] = useState(null) const trimmed = target.trim() const targetMapped = trimmed.length > 2 && resolveLocalPath(mappings, trimmed) != null const validation = trimmed.length === 0 ? null : !trimmed.startsWith("$/") || trimmed.length <= 2 ? "目标路径必须以 $/ 开头" : trimmed === sourceServerPath ? "目标路径不能与源路径相同" : !targetMapped ? "目标路径未映射,请先在工作区中映射" : null const targetReady = trimmed.length > 2 && validation == null /** * 加载源 → 目标的待合并候选列表。 */ const loadCandidates = useCallback(async () => { setLoading(true) setError(null) setSelected(null) const result = await api.mergeCandidates({ sourceServerPath, targetServerPath: trimmed, }) setLoading(false) if (!result.ok) { setCandidates(null) setError(result.errorMessage ?? "候选查询失败(请确认源与目标存在分支关系)") return } setCandidates(result.data?.candidates ?? []) }, [sourceServerPath, trimmed]) /** * 执行合并:changeset 为 undefined 表示合并全部候选。 */ const handleMerge = useCallback( (changeset: number | undefined) => { void (async () => { setBusy(true) const ok = await onMerge(trimmed, changeset) setBusy(false) if (ok) { onClose() } })() }, [onMerge, trimmed, onClose], ) return ( !open && !busy && onClose()}> 合并 将源分支的变更集合并到目标分支,结果为挂起更改,冲突需逐个处理

{sourceServerPath}

{ setTarget(event.target.value) setCandidates(null) }} placeholder="$/Project/Branch" disabled={busy} className="font-mono text-xs" />
{validation != null &&

{validation}

}
{loading ? (
正在查询待合并变更集…
) : candidates == null ? (
输入目标路径并点击「查询候选」,查看可合并的变更集
) : candidates.length === 0 ? (
没有待合并的变更集,两个分支已同步
) : ( Changeset 类型 作者 时间 注释 {candidates.map((entry) => ( setSelected((prev) => (prev === entry.changeset ? null : entry.changeset)) } > {entry.changeset} {entry.changeType} {entry.author} {formatDateTime(entry.date)} {entry.comment || "—"} ))}
)}
{error &&

{error}

}

合并只产生挂起更改,不会直接写入服务器;出现冲突会进入冲突处理弹窗,签入前可撤销。

) }