Merge branch 'main' into cv-main-xx1jlt

This commit is contained in:
xintaofei
2026-03-15 22:46:27 +08:00
14 changed files with 293 additions and 50 deletions

View File

@@ -489,7 +489,7 @@ function RenderNode({
return (
<ContextMenu>
<ContextMenuTrigger asChild>
<ContextMenuTrigger>
<FileTreeFile
path={node.path}
name={node.name}
@@ -614,7 +614,7 @@ function RenderNode({
return (
<ContextMenu>
<ContextMenuTrigger asChild>
<ContextMenuTrigger>
<FileTreeFolder
path={node.path}
name={node.name}
@@ -2128,7 +2128,7 @@ export function FileTreeTab() {
>
{folder?.path && (
<ContextMenu>
<ContextMenuTrigger asChild>
<ContextMenuTrigger>
<FileTreeFolder
path={FILE_TREE_ROOT_PATH}
name={rootNodeName}

View File

@@ -900,7 +900,7 @@ export function GitChangesTab() {
return (
<ContextMenu key={`tracked:${node.path}`}>
<ContextMenuTrigger asChild>
<ContextMenuTrigger>
<FileTreeFolder
path={node.path}
name={node.name}
@@ -956,7 +956,7 @@ export function GitChangesTab() {
return (
<ContextMenu key={`tracked:${file.path}`}>
<ContextMenuTrigger asChild>
<ContextMenuTrigger>
<FileTreeFile
className="w-full min-w-0 cursor-pointer"
name={node.name}
@@ -1047,7 +1047,7 @@ export function GitChangesTab() {
return (
<ContextMenu key={`untracked:${node.path}`}>
<ContextMenuTrigger asChild>
<ContextMenuTrigger>
<FileTreeFolder
path={node.path}
name={node.name}
@@ -1102,7 +1102,7 @@ export function GitChangesTab() {
return (
<ContextMenu key={`untracked:${file.path}`}>
<ContextMenuTrigger asChild>
<ContextMenuTrigger>
<FileTreeFile
className="w-full min-w-0 cursor-pointer"
name={node.name}
@@ -1239,7 +1239,7 @@ export function GitChangesTab() {
onExpandedChange={setExpandedTrackedPaths}
>
<ContextMenu>
<ContextMenuTrigger asChild>
<ContextMenuTrigger>
<FileTreeFolder
path={TRACKED_ROOT_PATH}
name={folderName}
@@ -1332,7 +1332,7 @@ export function GitChangesTab() {
onExpandedChange={setExpandedUntrackedPaths}
>
<ContextMenu>
<ContextMenuTrigger asChild>
<ContextMenuTrigger>
<FileTreeFolder
path={UNTRACKED_ROOT_PATH}
name={folderName}

View File

@@ -422,7 +422,7 @@ function CommitFilesTree({
const file = node.change
return (
<ContextMenu key={`${commitHash}:${file.path}`}>
<ContextMenuTrigger asChild>
<ContextMenuTrigger>
<FileTreeFile
className="w-full min-w-0 cursor-pointer"
name={node.name}

View File

@@ -157,6 +157,12 @@ function buildFileTree(entries: GitStatusEntry[]): TreeNode[] {
return toNodes(root)
}
/** Collect all file paths under a tree node (recursive). */
function collectFilePaths(node: TreeNode): string[] {
if (node.kind === "file") return [node.path]
return node.children.flatMap(collectFilePaths)
}
/** Depth-first traversal to find the first file node (matches visual order). */
function findFirstFile(nodes: TreeNode[]): string | undefined {
for (const node of nodes) {
@@ -523,6 +529,99 @@ export function CommitWorkspace({
[folderPath, loadStatus, t]
)
const handleRollbackDir = useCallback(
(dirPath: string, files: string[], displayName?: string) => {
const label = displayName ?? dirPath
setConfirm({
open: true,
title: t("confirm.rollbackTitle"),
description: t("confirm.rollbackDirDescription", { dir: label }),
variant: "destructive",
action: () => {
void (async () => {
if (!folderPath) return
try {
await gitRollbackFile(folderPath, dirPath)
toast.success(t("toasts.dirRolledBack"), {
description: label,
})
if (diffFileRef.current && files.includes(diffFileRef.current)) {
setDiffFile(null)
setDiffOriginal("")
setDiffModified("")
}
setSelected((prev) => {
const next = new Set(prev)
files.forEach((f) => next.delete(f))
return next
})
void loadStatus()
} catch (err) {
toast.error(t("toasts.rollbackFailed"), {
description: String(err),
})
}
})()
},
})
},
[folderPath, loadStatus, t]
)
const handleDeleteDir = useCallback(
(dirPath: string, files: string[], displayName?: string) => {
const label = displayName ?? dirPath
setConfirm({
open: true,
title: t("confirm.deleteTitle"),
description: t("confirm.deleteDirDescription", { dir: label }),
variant: "destructive",
action: () => {
void (async () => {
if (!folderPath) return
try {
await deleteFileTreeEntry(folderPath, dirPath)
toast.success(t("toasts.dirDeleted"), {
description: label,
})
if (diffFileRef.current && files.includes(diffFileRef.current)) {
setDiffFile(null)
setDiffOriginal("")
setDiffModified("")
}
setSelected((prev) => {
const next = new Set(prev)
files.forEach((f) => next.delete(f))
return next
})
void loadStatus()
} catch (err) {
toast.error(t("toasts.deleteFailed"), {
description: String(err),
})
}
})()
},
})
},
[folderPath, loadStatus, t]
)
const handleAddDirToVcs = useCallback(
async (dirPath: string, files: string[], displayName?: string) => {
if (!folderPath) return
const label = displayName ?? dirPath
try {
await gitAddFiles(folderPath, files)
toast.success(t("toasts.addedToVcs"), { description: label })
void loadStatus()
} catch (err) {
toast.error(t("toasts.addToVcsFailed"), { description: String(err) })
}
},
[folderPath, loadStatus, t]
)
const closeConfirm = useCallback(() => {
setConfirm(CONFIRM_INITIAL)
}, [])
@@ -607,14 +706,36 @@ export function CommitWorkspace({
const renderTrackedNode = useCallback(
function renderNode(node: TreeNode): React.ReactNode {
if (node.kind === "dir") {
const dirFiles = collectFilePaths(node)
const hasNonDeleted = node.children.some(
(child) =>
child.kind === "file" &&
child.entry.status !== " D" &&
child.entry.status !== "D"
)
return (
<FileTreeFolder
key={`tracked:${node.path}`}
name={node.name}
path={node.path}
>
{node.children.map(renderNode)}
</FileTreeFolder>
<ContextMenu key={`tracked:${node.path}`}>
<ContextMenuTrigger>
<FileTreeFolder name={node.name} path={node.path}>
{node.children.map(renderNode)}
</FileTreeFolder>
</ContextMenuTrigger>
<ContextMenuContent>
{hasNonDeleted && (
<ContextMenuItem
onClick={() => handleRollbackDir(node.path, dirFiles)}
>
{t("actions.rollback")}
</ContextMenuItem>
)}
<ContextMenuItem
variant="destructive"
onClick={() => handleDeleteDir(node.path, dirFiles)}
>
{tCommon("delete")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
@@ -686,7 +807,9 @@ export function CommitWorkspace({
toggleFile,
handleViewDiff,
handleRollbackFile,
handleRollbackDir,
handleDeleteFile,
handleDeleteDir,
t,
tCommon,
]
@@ -695,14 +818,31 @@ export function CommitWorkspace({
const renderUntrackedNode = useCallback(
function renderNode(node: TreeNode): React.ReactNode {
if (node.kind === "dir") {
const dirFiles = collectFilePaths(node)
return (
<FileTreeFolder
key={`untracked:${node.path}`}
name={node.name}
path={node.path}
>
{node.children.map(renderNode)}
</FileTreeFolder>
<ContextMenu key={`untracked:${node.path}`}>
<ContextMenuTrigger>
<FileTreeFolder name={node.name} path={node.path}>
{node.children.map(renderNode)}
</FileTreeFolder>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onClick={() => {
void handleAddDirToVcs(node.path, dirFiles)
}}
>
{t("actions.addToVcs")}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
variant="destructive"
onClick={() => handleDeleteDir(node.path, dirFiles)}
>
{tCommon("delete")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
@@ -768,7 +908,9 @@ export function CommitWorkspace({
toggleFile,
handleViewDiff,
handleAddToVcs,
handleAddDirToVcs,
handleDeleteFile,
handleDeleteDir,
t,
tCommon,
]
@@ -879,9 +1021,37 @@ export function CommitWorkspace({
selectedPath={diffFile ?? undefined}
onSelect={handleSelectPath}
>
<FileTreeFolder name={folderName} path={folderName}>
{trackedTree.map(renderTrackedNode)}
</FileTreeFolder>
<ContextMenu>
<ContextMenuTrigger>
<FileTreeFolder
name={folderName}
path={folderName}
>
{trackedTree.map(renderTrackedNode)}
</FileTreeFolder>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onClick={() =>
handleRollbackDir(
".",
trackedFiles,
folderName
)
}
>
{t("actions.rollback")}
</ContextMenuItem>
<ContextMenuItem
variant="destructive"
onClick={() =>
handleDeleteDir(".", trackedFiles, folderName)
}
>
{tCommon("delete")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</FileTree>
</section>
)}
@@ -933,9 +1103,42 @@ export function CommitWorkspace({
selectedPath={diffFile ?? undefined}
onSelect={handleSelectPath}
>
<FileTreeFolder name={folderName} path={folderName}>
{untrackedTree.map(renderUntrackedNode)}
</FileTreeFolder>
<ContextMenu>
<ContextMenuTrigger>
<FileTreeFolder
name={folderName}
path={folderName}
>
{untrackedTree.map(renderUntrackedNode)}
</FileTreeFolder>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
onClick={() => {
void handleAddDirToVcs(
".",
untrackedFiles,
folderName
)
}}
>
{t("actions.addToVcs")}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
variant="destructive"
onClick={() =>
handleDeleteDir(
".",
untrackedFiles,
folderName
)
}
>
{tCommon("delete")}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</FileTree>
)}
</section>

View File

@@ -895,13 +895,17 @@
"fileDeleted": "تم حذف الملف",
"deleteFailed": "فشل الحذف",
"fileRolledBack": "تم التراجع عن الملف",
"rollbackFailed": "فشل التراجع"
"rollbackFailed": "فشل التراجع",
"dirRolledBack": "تم استعادة المجلد",
"dirDeleted": "تم حذف المجلد"
},
"confirm": {
"deleteTitle": "تأكيد الحذف",
"deleteDescription": "حذف الملف \"{file}\"؟ لا يمكن التراجع عن هذا الإجراء.",
"rollbackTitle": "تأكيد التراجع",
"rollbackDescription": "التراجع عن الملف \"{file}\" إلى HEAD؟ ستفقد التغييرات غير المحفوظة."
"rollbackDescription": "التراجع عن الملف \"{file}\" إلى HEAD؟ ستفقد التغييرات غير المحفوظة.",
"rollbackDirDescription": "هل تريد استعادة المجلد \"{dir}\" إلى HEAD؟ ستفقد التغييرات غير المحفوظة.",
"deleteDirDescription": "هل تريد حذف المجلد \"{dir}\"؟ لا يمكن التراجع عن هذا الإجراء."
},
"actions": {
"select": "تحديد",

View File

@@ -895,13 +895,17 @@
"fileDeleted": "Datei gelöscht",
"deleteFailed": "Löschen fehlgeschlagen",
"fileRolledBack": "Datei zurückgesetzt",
"rollbackFailed": "Rollback fehlgeschlagen"
"rollbackFailed": "Rollback fehlgeschlagen",
"dirRolledBack": "Verzeichnis zurückgesetzt",
"dirDeleted": "Verzeichnis gelöscht"
},
"confirm": {
"deleteTitle": "Löschen bestätigen",
"deleteDescription": "Datei \"{file}\" löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"rollbackTitle": "Rollback bestätigen",
"rollbackDescription": "Datei \"{file}\" auf HEAD zurücksetzen? Ungespeicherte Änderungen gehen verloren."
"rollbackDescription": "Datei \"{file}\" auf HEAD zurücksetzen? Ungespeicherte Änderungen gehen verloren.",
"rollbackDirDescription": "Verzeichnis \"{dir}\" auf HEAD zurücksetzen? Nicht gespeicherte Änderungen gehen verloren.",
"deleteDirDescription": "Verzeichnis \"{dir}\" löschen? Diese Aktion kann nicht rückgängig gemacht werden."
},
"actions": {
"select": "Auswählen",

View File

@@ -895,13 +895,17 @@
"fileDeleted": "File deleted",
"deleteFailed": "Delete failed",
"fileRolledBack": "File rolled back",
"rollbackFailed": "Rollback failed"
"rollbackFailed": "Rollback failed",
"dirRolledBack": "Directory rolled back",
"dirDeleted": "Directory deleted"
},
"confirm": {
"deleteTitle": "Confirm deletion",
"deleteDescription": "Delete file \"{file}\"? This action cannot be undone.",
"rollbackTitle": "Confirm rollback",
"rollbackDescription": "Rollback file \"{file}\" to HEAD? Unsaved changes will be lost."
"rollbackDescription": "Rollback file \"{file}\" to HEAD? Unsaved changes will be lost.",
"rollbackDirDescription": "Rollback directory \"{dir}\" to HEAD? Unsaved changes will be lost.",
"deleteDirDescription": "Delete directory \"{dir}\"? This action cannot be undone."
},
"actions": {
"select": "Select",

View File

@@ -895,13 +895,17 @@
"fileDeleted": "Archivo eliminado",
"deleteFailed": "Error al eliminar",
"fileRolledBack": "Archivo revertido",
"rollbackFailed": "Error al revertir"
"rollbackFailed": "Error al revertir",
"dirRolledBack": "Directorio revertido",
"dirDeleted": "Directorio eliminado"
},
"confirm": {
"deleteTitle": "Confirmar eliminación",
"deleteDescription": "¿Eliminar el archivo \"{file}\"? Esta acción no se puede deshacer.",
"rollbackTitle": "Confirmar reversión",
"rollbackDescription": "¿Revertir el archivo \"{file}\" a HEAD? Se perderán los cambios sin guardar."
"rollbackDescription": "¿Revertir el archivo \"{file}\" a HEAD? Se perderán los cambios sin guardar.",
"rollbackDirDescription": "¿Revertir el directorio \"{dir}\" a HEAD? Los cambios no guardados se perderán.",
"deleteDirDescription": "¿Eliminar el directorio \"{dir}\"? Esta acción no se puede deshacer."
},
"actions": {
"select": "Seleccionar",

View File

@@ -895,13 +895,17 @@
"fileDeleted": "Fichier supprimé",
"deleteFailed": "Échec de la suppression",
"fileRolledBack": "Fichier restauré",
"rollbackFailed": "Échec du rollback"
"rollbackFailed": "Échec du rollback",
"dirRolledBack": "Répertoire restauré",
"dirDeleted": "Répertoire supprimé"
},
"confirm": {
"deleteTitle": "Confirmer la suppression",
"deleteDescription": "Supprimer le fichier \"{file}\" ? Cette action est irréversible.",
"rollbackTitle": "Confirmer le rollback",
"rollbackDescription": "Restaurer le fichier \"{file}\" vers HEAD ? Les modifications non enregistrées seront perdues."
"rollbackDescription": "Restaurer le fichier \"{file}\" vers HEAD ? Les modifications non enregistrées seront perdues.",
"rollbackDirDescription": "Restaurer le répertoire \"{dir}\" vers HEAD ? Les modifications non enregistrées seront perdues.",
"deleteDirDescription": "Supprimer le répertoire \"{dir}\" ? Cette action est irréversible."
},
"actions": {
"select": "Sélectionner",

View File

@@ -895,13 +895,17 @@
"fileDeleted": "ファイルを削除しました",
"deleteFailed": "削除に失敗しました",
"fileRolledBack": "ファイルをロールバックしました",
"rollbackFailed": "ロールバックに失敗しました"
"rollbackFailed": "ロールバックに失敗しました",
"dirRolledBack": "ディレクトリをロールバックしました",
"dirDeleted": "ディレクトリを削除しました"
},
"confirm": {
"deleteTitle": "削除の確認",
"deleteDescription": "ファイル \"{file}\" を削除しますか?この操作は元に戻せません。",
"rollbackTitle": "ロールバックの確認",
"rollbackDescription": "ファイル \"{file}\" を HEAD にロールバックしますか?未保存の変更は失われます。"
"rollbackDescription": "ファイル \"{file}\" を HEAD にロールバックしますか?未保存の変更は失われます。",
"rollbackDirDescription": "ディレクトリ「{dir}」をHEADにロールバックしますか未保存の変更は失われます。",
"deleteDirDescription": "ディレクトリ「{dir}」を削除しますか?この操作は元に戻せません。"
},
"actions": {
"select": "選択",

View File

@@ -895,13 +895,17 @@
"fileDeleted": "파일이 삭제되었습니다",
"deleteFailed": "삭제에 실패했습니다",
"fileRolledBack": "파일이 롤백되었습니다",
"rollbackFailed": "롤백에 실패했습니다"
"rollbackFailed": "롤백에 실패했습니다",
"dirRolledBack": "디렉토리가 롤백되었습니다",
"dirDeleted": "디렉토리가 삭제되었습니다"
},
"confirm": {
"deleteTitle": "삭제 확인",
"deleteDescription": "파일 \"{file}\"을(를) 삭제할까요? 이 작업은 되돌릴 수 없습니다.",
"rollbackTitle": "롤백 확인",
"rollbackDescription": "파일 \"{file}\"을(를) HEAD로 롤백할까요? 저장되지 않은 변경 사항은 사라집니다."
"rollbackDescription": "파일 \"{file}\"을(를) HEAD로 롤백할까요? 저장되지 않은 변경 사항은 사라집니다.",
"rollbackDirDescription": "디렉토리 \"{dir}\"를 HEAD로 롤백하시겠습니까? 저장되지 않은 변경 사항이 손실됩니다.",
"deleteDirDescription": "디렉토리 \"{dir}\"를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
},
"actions": {
"select": "선택",

View File

@@ -895,13 +895,17 @@
"fileDeleted": "Arquivo excluído",
"deleteFailed": "Falha ao excluir",
"fileRolledBack": "Arquivo revertido",
"rollbackFailed": "Falha no rollback"
"rollbackFailed": "Falha no rollback",
"dirRolledBack": "Diretório revertido",
"dirDeleted": "Diretório excluído"
},
"confirm": {
"deleteTitle": "Confirmar exclusão",
"deleteDescription": "Excluir o arquivo \"{file}\"? Esta ação não pode ser desfeita.",
"rollbackTitle": "Confirmar rollback",
"rollbackDescription": "Reverter o arquivo \"{file}\" para o HEAD? Alterações não salvas serão perdidas."
"rollbackDescription": "Reverter o arquivo \"{file}\" para o HEAD? Alterações não salvas serão perdidas.",
"rollbackDirDescription": "Reverter o diretório \"{dir}\" para HEAD? Alterações não salvas serão perdidas.",
"deleteDirDescription": "Excluir o diretório \"{dir}\"? Esta ação não pode ser desfeita."
},
"actions": {
"select": "Selecionar",

View File

@@ -895,13 +895,17 @@
"fileDeleted": "文件已删除",
"deleteFailed": "删除失败",
"fileRolledBack": "文件已回滚",
"rollbackFailed": "回滚失败"
"rollbackFailed": "回滚失败",
"dirRolledBack": "目录已回滚",
"dirDeleted": "目录已删除"
},
"confirm": {
"deleteTitle": "确认删除",
"deleteDescription": "确定要删除文件「{file}」吗?此操作不可恢复。",
"rollbackTitle": "确认回滚",
"rollbackDescription": "确定要回滚文件「{file}」到 HEAD 版本吗?未保存的修改将丢失。"
"rollbackDescription": "确定要回滚文件「{file}」到 HEAD 版本吗?未保存的修改将丢失。",
"rollbackDirDescription": "确定要回滚目录「{dir}」到 HEAD 版本吗?未保存的修改将丢失。",
"deleteDirDescription": "确定要删除目录「{dir}」吗?此操作不可恢复。"
},
"actions": {
"select": "选择",

View File

@@ -895,13 +895,17 @@
"fileDeleted": "檔案已刪除",
"deleteFailed": "刪除失敗",
"fileRolledBack": "檔案已回滾",
"rollbackFailed": "回滾失敗"
"rollbackFailed": "回滾失敗",
"dirRolledBack": "目錄已回滾",
"dirDeleted": "目錄已刪除"
},
"confirm": {
"deleteTitle": "確認刪除",
"deleteDescription": "確定要刪除檔案「{file}」嗎?此操作無法復原。",
"rollbackTitle": "確認回滾",
"rollbackDescription": "確定要回滾檔案「{file}」到 HEAD 版本嗎?未儲存修改將遺失。"
"rollbackDescription": "確定要回滾檔案「{file}」到 HEAD 版本嗎?未儲存修改將遺失。",
"rollbackDirDescription": "確定要回滾目錄「{dir}」到 HEAD 版本嗎?未儲存的修改將遺失。",
"deleteDirDescription": "確定要刪除目錄「{dir}」嗎?此操作不可恢復。"
},
"actions": {
"select": "選擇",