Add 'Clone' feature to file tree (#5232)

* Add 'Clone' file / folder feature to file tree

* E2E Test: clone file in file tree

* Don't stat if there's no target
This commit is contained in:
49fl
2025-02-04 18:13:59 -05:00
committed by GitHub
parent bc6f0fceca
commit 8f90c352fe
8 changed files with 129 additions and 8 deletions

View File

@ -124,22 +124,43 @@ export const FileMachineProvider = ({
let createdName = input.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (input.makeDir) {
if (
(input.targetPathToClone &&
(await window.electron.statIsDirectory(
input.targetPathToClone
))) ||
input.makeDir
) {
let { name, path } = getNextDirName({
entryName: createdName,
baseDir: input.selectedDirectory.path,
entryName: input.targetPathToClone
? window.electron.path.basename(input.targetPathToClone)
: createdName,
baseDir: input.targetPathToClone
? window.electron.path.dirname(input.targetPathToClone)
: input.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.mkdir(createdPath)
} else {
const { name, path } = getNextFileName({
entryName: createdName,
baseDir: input.selectedDirectory.path,
entryName: input.targetPathToClone
? window.electron.path.basename(input.targetPathToClone)
: createdName,
baseDir: input.targetPathToClone
? window.electron.path.dirname(input.targetPathToClone)
: input.selectedDirectory.path,
})
createdName = name
createdPath = path
await window.electron.writeFile(createdPath, input.content ?? '')
if (input.targetPathToClone) {
await window.electron.copyFile(
input.targetPathToClone,
createdPath
)
} else {
await window.electron.writeFile(createdPath, input.content ?? '')
}
}
return {

View File

@ -153,6 +153,7 @@ const FileTreeItem = ({
onClickDirectory,
onCreateFile,
onCreateFolder,
onCloneFileOrFolder,
newTreeEntry,
level = 0,
treeSelection,
@ -171,6 +172,7 @@ const FileTreeItem = ({
) => void
onCreateFile: (name: string) => void
onCreateFolder: (name: string) => void
onCloneFileOrFolder: (path: string) => void
newTreeEntry: TreeEntry
level?: number
treeSelection: FileEntry | undefined
@ -403,6 +405,7 @@ const FileTreeItem = ({
currentFile={currentFile}
onCreateFile={onCreateFile}
onCreateFolder={onCreateFolder}
onCloneFileOrFolder={onCloneFileOrFolder}
newTreeEntry={newTreeEntry}
lastDirectoryClicked={lastDirectoryClicked}
onClickDirectory={onClickDirectory}
@ -441,6 +444,7 @@ const FileTreeItem = ({
itemRef={itemRef}
onRename={addCurrentItemToRenaming}
onDelete={() => setIsConfirmingDelete(true)}
onClone={() => onCloneFileOrFolder(fileOrDir.path)}
/>
</div>
)
@ -450,12 +454,14 @@ interface FileTreeContextMenuProps {
itemRef: React.RefObject<HTMLElement>
onRename: () => void
onDelete: () => void
onClone: () => void
}
function FileTreeContextMenu({
itemRef,
onRename,
onDelete,
onClone,
}: FileTreeContextMenuProps) {
const platform = usePlatform()
const metaKey = platform === 'macos' ? '⌘' : 'Ctrl'
@ -478,6 +484,13 @@ function FileTreeContextMenu({
>
Delete
</ContextMenuItem>,
<ContextMenuItem
data-testid="context-menu-clone"
onClick={onClone}
hotkey=""
>
Clone
</ContextMenuItem>,
]}
/>
)
@ -584,9 +597,22 @@ export const useFileTreeOperations = () => {
})
}
function cloneFileOrDir(args: { path: string }) {
send({
type: 'Create file',
data: {
name: '',
makeDir: false,
shouldSetToRename: false,
targetPathToClone: args.path,
},
})
}
return {
createFile,
createFolder,
cloneFileOrDir,
newTreeEntry,
}
}
@ -595,7 +621,8 @@ export const FileTree = ({
className = '',
onNavigateToFile: closePanel,
}: FileTreeProps) => {
const { createFile, createFolder, newTreeEntry } = useFileTreeOperations()
const { createFile, createFolder, cloneFileOrDir, newTreeEntry } =
useFileTreeOperations()
return (
<div className={className}>
@ -611,6 +638,7 @@ export const FileTree = ({
newTreeEntry={newTreeEntry}
onCreateFile={(name: string) => createFile({ dryRun: false, name })}
onCreateFolder={(name: string) => createFolder({ dryRun: false, name })}
onCloneFileOrFolder={(path: string) => cloneFileOrDir({ path })}
/>
</div>
)
@ -620,10 +648,12 @@ export const FileTreeInner = ({
onNavigateToFile,
onCreateFile,
onCreateFolder,
onCloneFileOrFolder,
newTreeEntry,
}: {
onCreateFile: (name: string) => void
onCreateFolder: (name: string) => void
onCloneFileOrFolder: (path: string) => void
newTreeEntry: TreeEntry
onNavigateToFile?: () => void
}) => {
@ -732,6 +762,7 @@ export const FileTreeInner = ({
fileOrDir={fileOrDir}
onCreateFile={onCreateFile}
onCreateFolder={onCreateFolder}
onCloneFileOrFolder={onCloneFileOrFolder}
newTreeEntry={newTreeEntry}
onClickDirectory={onClickDirectory}
onNavigateToFile={onNavigateToFile_}

View File

@ -122,7 +122,8 @@ export const sidebarPanes: SidebarPane[] = [
icon: 'folder',
sidebarName: 'Project Files',
Content: (props: { id: SidebarType; onClose: () => void }) => {
const { createFile, createFolder, newTreeEntry } = useFileTreeOperations()
const { createFile, createFolder, cloneFileOrDir, newTreeEntry } =
useFileTreeOperations()
return (
<>
@ -143,6 +144,7 @@ export const sidebarPanes: SidebarPane[] = [
onCreateFolder={(name: string) =>
createFolder({ dryRun: false, name })
}
onCloneFileOrFolder={(path: string) => cloneFileOrDir({ path })}
newTreeEntry={newTreeEntry}
/>
</>

View File

@ -21,6 +21,7 @@ type FileMachineEvents =
content?: string
silent?: boolean
shouldSetToRename?: boolean
targetPathToClone?: string
}
}
| { type: 'Delete file'; data: FileEntry }
@ -124,6 +125,7 @@ export const fileMachine = setup({
name: string
makeDir: boolean
selectedDirectory: FileEntry
targetPathToClone?: string
content: string
shouldSetToRename: boolean
}
@ -235,6 +237,7 @@ export const fileMachine = setup({
name: event.data.name,
makeDir: event.data.makeDir,
selectedDirectory: context.selectedDirectory,
targetPathToClone: event.data.targetPathToClone,
content: event.data.content ?? '',
shouldSetToRename: event.data.shouldSetToRename ?? false,
}

View File

@ -144,6 +144,7 @@ contextBridge.exposeInMainWorld('electron', {
// exported.
watchFileOn,
watchFileOff,
copyFile: fs.copyFile,
readFile,
writeFile,
exists,