Merge branch 'main' into cv-main-xx1jlt
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user