在文件树上支持右键添加文件和添加目录操作

This commit is contained in:
xintaofei
2026-03-13 23:33:31 +08:00
parent 874591a473
commit f4f70c438a
14 changed files with 363 additions and 12 deletions

View File

@@ -21,6 +21,7 @@ import { useTabContext } from "@/contexts/tab-context"
import { useTerminalContext } from "@/contexts/terminal-context"
import { useWorkspaceContext } from "@/contexts/workspace-context"
import {
createFileTreeEntry,
deleteFileTreeEntry,
gitAddFiles,
getGitBranch,
@@ -419,6 +420,7 @@ interface RenderNodeProps {
onOpenDirInTerminal: (dirPath: string, fileName: string) => Promise<void>
onRequestAddToVcs: (target: FileActionTarget) => void
onRequestRename: (target: FileActionTarget) => void
onRequestCreate: (parentPath: string, kind: "file" | "dir") => void
onRequestDelete: (target: FileActionTarget) => void
onRefresh: () => void
}
@@ -441,6 +443,7 @@ function RenderNode({
onRequestRollback,
onOpenDirInTerminal,
onRequestAddToVcs,
onRequestCreate,
onRequestRename,
onRequestDelete,
onRefresh,
@@ -507,6 +510,21 @@ function RenderNode({
>
{t("attachToCurrentSession")}
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>{t("new")}</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem
onSelect={() => onRequestCreate(node.path, "file")}
>
{t("newFile")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => onRequestCreate(node.path, "dir")}
>
{t("newDirectory")}
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSub>
<ContextMenuSubTrigger disabled={isGitMenuDisabled}>
{t("git")}
@@ -629,6 +647,7 @@ function RenderNode({
onRequestCompareWithBranch={onRequestCompareWithBranch}
onRequestRollback={onRequestRollback}
onOpenDirInTerminal={onOpenDirInTerminal}
onRequestCreate={onRequestCreate}
onRequestAddToVcs={onRequestAddToVcs}
onRequestRename={onRequestRename}
onRequestDelete={onRequestDelete}
@@ -639,6 +658,19 @@ function RenderNode({
</FileTreeFolder>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuSub>
<ContextMenuSubTrigger>{t("new")}</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem
onSelect={() => onRequestCreate(node.path, "file")}
>
{t("newFile")}
</ContextMenuItem>
<ContextMenuItem onSelect={() => onRequestCreate(node.path, "dir")}>
{t("newDirectory")}
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSub>
<ContextMenuSubTrigger disabled={isGitMenuDisabled}>
{t("git")}
@@ -736,6 +768,10 @@ export function FileTreeTab() {
)
const [renameValue, setRenameValue] = useState("")
const [renaming, setRenaming] = useState(false)
const [createParentPath, setCreateParentPath] = useState<string | null>(null)
const [createKind, setCreateKind] = useState<"file" | "dir">("file")
const [createName, setCreateName] = useState("")
const [creating, setCreating] = useState(false)
const [deleteTarget, setDeleteTarget] = useState<FileActionTarget | null>(
null
)
@@ -1139,6 +1175,15 @@ export function FileTreeTab() {
})
}, [folder, t])
const handleRequestCreate = useCallback(
(parentPath: string, kind: "file" | "dir") => {
setCreateParentPath(parentPath)
setCreateKind(kind)
setCreateName("")
},
[]
)
const handleRequestRename = useCallback((target: FileActionTarget) => {
setRenameTarget(target)
setRenameValue(target.name)
@@ -1516,6 +1561,33 @@ export function FileTreeTab() {
]
)
const handleCreateConfirm = useCallback(async () => {
if (!folder?.path || createParentPath === null) return
const trimmedName = createName.trim()
if (!trimmedName) {
setCreateParentPath(null)
return
}
setCreating(true)
try {
await createFileTreeEntry(
folder.path,
createParentPath,
trimmedName,
createKind
)
setCreateParentPath(null)
setCreateName("")
await fetchTree()
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
toast.error(t("toasts.createFailed"), { description: message })
} finally {
setCreating(false)
}
}, [createKind, createName, createParentPath, fetchTree, folder?.path, t])
const handleRenameConfirm = useCallback(async () => {
if (!folder?.path || !renameTarget) return
const nextName = renameValue.trim()
@@ -2074,6 +2146,7 @@ export function FileTreeTab() {
}
onRequestRollback={handleRequestRollback}
onOpenDirInTerminal={handleOpenDirInTerminal}
onRequestCreate={handleRequestCreate}
onRequestAddToVcs={handleAddToVcs}
onRequestRename={handleRequestRename}
onRequestDelete={handleRequestDelete}
@@ -2083,6 +2156,21 @@ export function FileTreeTab() {
</FileTreeFolder>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuSub>
<ContextMenuSubTrigger>{t("new")}</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem
onSelect={() => handleRequestCreate("", "file")}
>
{t("newFile")}
</ContextMenuItem>
<ContextMenuItem
onSelect={() => handleRequestCreate("", "dir")}
>
{t("newDirectory")}
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSub>
<ContextMenuSubTrigger disabled={!gitEnabled}>
{t("git")}
@@ -2165,6 +2253,17 @@ export function FileTreeTab() {
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuSub>
<ContextMenuSubTrigger>{t("new")}</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem onSelect={() => handleRequestCreate("", "file")}>
{t("newFile")}
</ContextMenuItem>
<ContextMenuItem onSelect={() => handleRequestCreate("", "dir")}>
{t("newDirectory")}
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem
onSelect={() => {
void fetchTree()
@@ -2175,6 +2274,68 @@ export function FileTreeTab() {
</ContextMenuContent>
</ContextMenu>
<Dialog
open={createParentPath !== null}
onOpenChange={(open) => {
if (open) return
setCreateParentPath(null)
setCreateName("")
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>
{createKind === "dir"
? t("createDialog.newDirectory")
: t("createDialog.newFile")}
</DialogTitle>
<DialogDescription>
{t("createDialog.description", {
kind:
createKind === "dir"
? t("newDirectory").toLowerCase()
: t("newFile").toLowerCase(),
})}
</DialogDescription>
</DialogHeader>
<form
onSubmit={(event) => {
event.preventDefault()
void handleCreateConfirm()
}}
className="space-y-4"
>
<Input
value={createName}
onChange={(event) => setCreateName(event.target.value)}
autoFocus
disabled={creating}
placeholder={
createKind === "dir"
? t("createDialog.placeholderDirectory")
: t("createDialog.placeholderFile")
}
/>
<DialogFooter>
<Button
type="button"
variant="outline"
disabled={creating}
onClick={() => {
setCreateParentPath(null)
setCreateName("")
}}
>
{tCommon("cancel")}
</Button>
<Button type="submit" disabled={creating || !createName.trim()}>
{tCommon("create")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog
open={Boolean(renameTarget)}
onOpenChange={(open) => {
@@ -2473,7 +2634,12 @@ export function FileTreeTab() {
<Collapsible key={remoteName}>
<CollapsibleTrigger className="flex w-full items-center gap-2.5 rounded-xl px-2 py-1.5 pl-5 text-sm hover:bg-accent hover:text-accent-foreground select-none outline-hidden">
<ChevronRight className="h-3 w-3 shrink-0 transition-transform [[data-state=open]>&]:rotate-90" />
{remoteName} ({groupedCompareRemoteBranches[remoteName].length})
{remoteName} (
{
groupedCompareRemoteBranches[remoteName]
.length
}
)
</CollapsibleTrigger>
<CollapsibleContent className="space-y-1 pt-1 pl-3">
{groupedCompareRemoteBranches[remoteName].map(
@@ -2489,7 +2655,9 @@ export function FileTreeTab() {
}}
disabled={comparing}
>
{branch.substring(remoteName.length + 1)}
{branch.substring(
remoteName.length + 1
)}
</Button>
)
)}