Files
mactfs/mactfsui/app/components/explorer/branch-dialog.tsx
T

139 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "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 type { MappingInfo } from "~/lib/api"
import { getParentPath, resolveLocalPath } from "~/lib/tfs"
/**
* 分支弹窗:把源路径分叉到目标路径(pendBranch 产生挂起更改,签入后服务器生效)。
* 目标路径父目录必须已映射(分支内容需要落盘到本地工作区);可选基于指定 changeset。
*/
export function BranchDialog({
sourceServerPath,
mappings,
onConfirm,
onClose,
}: {
sourceServerPath: string
mappings: MappingInfo[]
/** 确认分支,由编排层调用接口并刷新,返回是否成功。 */
onConfirm: (targetServerPath: string, changeset: number | undefined) => Promise<boolean>
onClose: () => void
}) {
const [target, setTarget] = useState(`${sourceServerPath}-branch`)
const [changesetInput, setChangesetInput] = useState("")
const [busy, setBusy] = useState(false)
const trimmed = target.trim()
const parentPath = trimmed.length > 2 ? getParentPath(trimmed) : null
const parentMapped = parentPath != null && resolveLocalPath(mappings, parentPath) != null
const targetLocalPath = trimmed.length > 2 ? resolveLocalPath(mappings, trimmed) : null
const changesetTrimmed = changesetInput.trim()
const changesetValid = changesetTrimmed.length === 0 || /^\d+$/.test(changesetTrimmed)
const validation =
!trimmed.startsWith("$/") || trimmed.length <= 2
? "目标路径必须以 $/ 开头"
: trimmed === sourceServerPath
? "目标路径不能与源路径相同"
: trimmed.startsWith(`${sourceServerPath}/`)
? "目标路径不能位于源路径内部"
: !parentMapped
? "目标路径所在父目录未映射,请先在工作区中映射"
: !changesetValid
? "changeset 必须是数字"
: null
/**
* 执行分支:成功后由上层刷新并关闭弹窗。
*/
const handleConfirm = () => {
void (async () => {
setBusy(true)
const ok = await onConfirm(
trimmed,
changesetTrimmed.length === 0 ? undefined : Number(changesetTrimmed),
)
setBusy(false)
if (ok) {
onClose()
}
})()
}
return (
<Dialog open onOpenChange={(open) => !open && !busy && onClose()}>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>分支</DialogTitle>
<DialogDescription>
从源路径创建分支,结果为 branch 挂起更改,签入后服务器生效
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-1.5">
<Label>源路径</Label>
<p className="rounded-md border bg-muted/40 px-2.5 py-1.5 font-mono text-xs">
{sourceServerPath}
</p>
</div>
<div className="space-y-1.5">
<Label htmlFor="branchTarget">目标路径</Label>
<Input
id="branchTarget"
value={target}
onChange={(event) => setTarget(event.target.value)}
disabled={busy}
className="font-mono text-xs"
/>
{targetLocalPath != null && validation == null && (
<p className="break-all font-mono text-xs text-muted-foreground">
本地落盘:{targetLocalPath}
</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="branchChangeset">基于 changeset(可选,默认 latest)</Label>
<Input
id="branchChangeset"
value={changesetInput}
onChange={(event) => setChangesetInput(event.target.value)}
placeholder="留空表示最新版本"
disabled={busy}
inputMode="numeric"
className="w-56 font-mono text-xs"
/>
</div>
{validation != null && <p className="text-xs text-destructive">{validation}</p>}
<p className="text-xs text-muted-foreground">
分支只产生挂起更改,可在挂起更改面板审查、撤销,签入后目标路径才会出现在服务器上。
</p>
</div>
<DialogFooter>
<Button variant="ghost" onClick={onClose} disabled={busy}>
取消
</Button>
<Button disabled={validation != null || busy} onClick={handleConfirm}>
{busy ? "正在创建分支…" : "创建分支"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}