import { useCallback, useEffect, useState } from "react" import { FolderSearch, Loader2, Pencil, Plus, Trash2 } from "lucide-react" import { ConfirmDialog } from "~/components/app/confirm-dialog" import { MappingDialog } from "~/components/explorer/mapping-dialog" import { Badge } from "~/components/ui/badge" 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 { CheckMappingTargetResult, MappingInfo, WorkspaceInfo } from "~/lib/api" import { isElectron, pathsExist, selectDirectory } from "~/lib/electron" import type { WorkspaceSession } from "~/lib/tfs" /** * 工作区集中管理弹窗:展示 Workspace 信息与全部 Mapping, * 支持新增、删除与修改本地路径(删旧 + 加新),对齐 VS 的 Edit Workspace。 */ export function WorkspaceManageDialog({ session, onClose, onMappingsChanged, }: { session: WorkspaceSession onClose: () => void /** Mapping 集合变化(新增 / 删除 / 修改)后回调上层刷新。 */ onMappingsChanged: (mappings: MappingInfo[]) => void }) { const [info, setInfo] = useState(null) // 各 Mapping 本地目录存在性(null 表示无法检测,如非 Electron 环境)。 const [existsMap, setExistsMap] = useState | null>(null) const [error, setError] = useState(null) // 新增映射:输入服务端路径后打开既有 MappingDialog。 const [addServerPath, setAddServerPath] = useState("") const [addOpen, setAddOpen] = useState(false) const [editMapping, setEditMapping] = useState(null) const [confirmDelete, setConfirmDelete] = useState(null) // Workspace 详细信息(Owner / Computer)通过 ensure 接口幂等获取。 useEffect(() => { let active = true void (async () => { const result = await api.ensureWorkspace({}) if (!active) { return } if (result.ok && result.data) { setInfo(result.data.workspace) } })() return () => { active = false } }, []) // Mapping 集合变化时重新检测本地目录存在性。 useEffect(() => { let active = true void (async () => { const result = await pathsExist(session.mappings.map((mapping) => mapping.localPath)) if (active) { setExistsMap(result) } })() return () => { active = false } }, [session.mappings]) /** * 删除 Mapping(仅解除映射,不删除本地文件),成功后回调上层刷新。 */ const handleDelete = useCallback( async (mapping: MappingInfo) => { const result = await api.deleteMapping({ serverPath: mapping.serverPath }) if (!result.ok) { setError(result.errorMessage ?? "删除 Mapping 失败") return } setError(null) onMappingsChanged(result.data?.mappings ?? []) }, [onMappingsChanged], ) const trimmedAddPath = addServerPath.trim() const addPathValid = trimmedAddPath.startsWith("$/") && trimmedAddPath.length > 2 return ( !open && onClose()}> 工作区管理 查看 Workspace 信息,集中管理全部本地映射
setAddServerPath(event.target.value)} placeholder="$/Project/Folder" className="h-8 font-mono text-xs" />
{session.mappings.length === 0 ? (
当前 Workspace 还没有任何映射
) : ( 服务端路径 本地路径 本地目录 {session.mappings.map((mapping) => { const exists = existsMap?.[mapping.localPath] return ( {mapping.serverPath} {mapping.localPath} {existsMap == null ? ( 无法检测 ) : exists ? ( 存在 ) : ( 不存在 )}
) })}
)}
{error &&

{error}

}

删除映射只解除服务端目录与本地目录的关联,不会删除本地已下载的文件。

{addOpen && ( setAddOpen(false)} onCreated={(mappings) => { onMappingsChanged(mappings) setAddOpen(false) setAddServerPath("") }} /> )} {editMapping && ( setEditMapping(null)} onChanged={(mappings) => { onMappingsChanged(mappings) setEditMapping(null) }} /> )} {confirmDelete && (

将解除 {confirmDelete.serverPath}{" "} 与本地目录的映射。

本地已下载的文件不会被删除;该目录下的挂起更改将无法继续签入。

} confirmLabel="删除映射" danger onConfirm={async () => { await handleDelete(confirmDelete) setConfirmDelete(null) }} onClose={() => setConfirmDelete(null)} /> )}
) } /** * 工作区信息单行展示。 */ function InfoRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) { return (
{label} {value}
) } /** * 修改 Mapping 本地路径弹窗:选择新的本地父目录并预校验, * 确认后按「删旧 + 加新」方式调整映射(不迁移本地内容)。 */ function EditMappingDialog({ mapping, onClose, onChanged, }: { mapping: MappingInfo onClose: () => void onChanged: (mappings: MappingInfo[]) => void }) { const [parentPath, setParentPath] = useState("") const [check, setCheck] = useState(null) const [checking, setChecking] = useState(false) const [busy, setBusy] = useState(false) const [error, setError] = useState(null) /** * 通过系统目录选择器选择新的本地父目录。 */ const handleBrowse = useCallback(async () => { const selected = await selectDirectory() if (selected) { setParentPath(selected) } }, []) // 与创建映射一致:父目录变化时由后端预校验最终目标路径。 useEffect(() => { const trimmed = parentPath.trim() if (trimmed.length === 0) { setCheck(null) return } let active = true setChecking(true) const timer = setTimeout(() => { void (async () => { const result = await api.checkMappingTarget({ serverPath: mapping.serverPath, localParentPath: trimmed, }) if (!active) { return } setChecking(false) if (!result.ok || !result.data) { setCheck(null) setError(result.errorMessage ?? "目标路径校验失败") return } setError(null) setCheck(result.data) })() }, 300) return () => { active = false clearTimeout(timer) } }, [parentPath, mapping.serverPath]) const exists = check?.exists === true const canConfirm = check != null && !exists && !busy && !checking /** * 删旧 + 加新调整映射本地路径,成功后回调最新 mappings。 */ const handleConfirm = useCallback(async () => { if (!check) { return } setBusy(true) setError(null) const removed = await api.deleteMapping({ serverPath: mapping.serverPath }) if (!removed.ok) { setBusy(false) setError(removed.errorMessage ?? "删除原映射失败") return } const added = await api.addMapping({ serverPath: mapping.serverPath, localPath: check.targetPath, getLatest: false, }) setBusy(false) if (!added.ok) { setError(`${added.errorMessage ?? "创建新映射失败"}(原映射已删除,请重新映射)`) return } const mappings = await api.listMappings() onChanged(mappings.data?.mappings ?? []) }, [check, mapping.serverPath, onChanged]) return ( !open && !busy && onClose()}> 修改本地路径 {mapping.serverPath}

{mapping.localPath}

setParentPath(event.target.value)} disabled={busy} className="font-mono text-xs" /> {isElectron() && ( )}
{checking && (

正在校验目标路径…

)} {check && !checking && (

{check.targetPath}

{exists && (

目标目录已存在,禁止映射

)}
)}

修改方式为「删除原映射 + 创建新映射」:原本地目录内容保留但不再与服务端关联, 新目录需重新执行获取最新。

{error &&

{error}

}
) }