From 8c83c3c9aaaa5299bf04be5a34fbdf7359c31ac9 Mon Sep 17 00:00:00 2001 From: KevIsDev Date: Mon, 3 Mar 2025 12:16:13 +0000 Subject: [PATCH] feat: add creation of files and folders in the FileTree - Drag and drop images directly in the file tree. Image will convert to base64 format --- app/components/workbench/FileTree.tsx | 232 +++++++++++++++++++++++--- app/lib/stores/workbench.ts | 48 ++++++ 2 files changed, 257 insertions(+), 23 deletions(-) diff --git a/app/components/workbench/FileTree.tsx b/app/components/workbench/FileTree.tsx index eed791ef..ff4f531d 100644 --- a/app/components/workbench/FileTree.tsx +++ b/app/components/workbench/FileTree.tsx @@ -1,10 +1,13 @@ -import { memo, useEffect, useMemo, useState, type ReactNode } from 'react'; +import { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; import type { FileMap } from '~/lib/stores/files'; import { classNames } from '~/utils/classNames'; import { createScopedLogger, renderLogger } from '~/utils/logger'; import * as ContextMenu from '@radix-ui/react-context-menu'; import type { FileHistory } from '~/types/actions'; import { diffLines, type Change } from 'diff'; +import { workbenchStore } from '~/lib/stores/workbench'; +import { toast } from 'react-toastify'; +import { path } from '~/utils/path'; const logger = createScopedLogger('FileTree'); @@ -25,6 +28,13 @@ interface Props { className?: string; } +interface InlineInputProps { + depth: number; + placeholder: string; + onSubmit: (value: string) => void; + onCancel: () => void; +} + export const FileTree = memo( ({ files = {}, @@ -213,28 +223,204 @@ function ContextMenuItem({ onSelect, children }: { onSelect?: () => void; childr ); } -function FileContextMenu({ onCopyPath, onCopyRelativePath, children }: FolderContextMenuProps) { +function InlineInput({ depth, placeholder, onSubmit, onCancel }: InlineInputProps) { + const inputRef = useRef(null); + + useEffect(() => { + const timer = setTimeout(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, 50); + + return () => clearTimeout(timer); + }, []); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + const value = inputRef.current?.value.trim(); + + if (value) { + onSubmit(value); + } + } else if (e.key === 'Escape') { + onCancel(); + } + }; + return ( - - {children} - - - - Copy path - Copy relative path - - - - +
+
+ { + setTimeout(() => { + if (document.activeElement !== inputRef.current) { + onCancel(); + } + }, 100); + }} + /> +
); } +// Modify the FileContextMenu component +function FileContextMenu({ + onCopyPath, + onCopyRelativePath, + fullPath, + children, +}: FolderContextMenuProps & { fullPath: string }) { + const [isCreatingFile, setIsCreatingFile] = useState(false); + const [isCreatingFolder, setIsCreatingFolder] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const depth = useMemo(() => fullPath.split('/').length, [fullPath]); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }, []); + + const handleDrop = useCallback( + async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const items = Array.from(e.dataTransfer.items); + const imageFiles = items.filter((item) => item.type.startsWith('image/')); + + for (const item of imageFiles) { + const file = item.getAsFile(); + + if (file) { + try { + const filePath = path.join(fullPath, file.name); + const success = await workbenchStore.createNewFile(filePath, file); + + if (success) { + toast.success(`Image ${file.name} uploaded successfully`); + } else { + toast.error(`Failed to upload image ${file.name}`); + } + } catch (error) { + toast.error(`Error uploading ${file.name}`); + logger.error(error); + } + } + } + + setIsDragging(false); + }, + [fullPath], + ); + + const handleCreateFile = async (fileName: string) => { + const newFilePath = path.join(fullPath, fileName); + const success = await workbenchStore.createNewFile(newFilePath); + + if (success) { + toast.success('File created successfully'); + } else { + toast.error('Failed to create file'); + } + + setIsCreatingFile(false); + }; + + const handleCreateFolder = async (folderName: string) => { + const newFolderPath = path.join(fullPath, folderName); + const success = await workbenchStore.createNewFolder(newFolderPath); + + if (success) { + toast.success('Folder created successfully'); + } else { + toast.error('Failed to create folder'); + } + + setIsCreatingFolder(false); + }; + + return ( + <> + + +
+ {children} +
+
+ + + + setIsCreatingFile(true)}> +
+
+ New File +
+ + setIsCreatingFolder(true)}> +
+
+ New Folder +
+ + + + Copy path + Copy relative path + + + + + {isCreatingFile && ( + setIsCreatingFile(false)} + /> + )} + {isCreatingFolder && ( + setIsCreatingFolder(false)} + /> + )} + + ); +} + +// Update the Folder component to pass the fullPath function Folder({ folder, collapsed, selected = false, onCopyPath, onCopyRelativePath, onClick }: FolderProps) { return ( - + { if (!fileModifications?.originalContent) { return { additions: 0, deletions: 0 }; } - // Usar a mesma lógica do DiffView para processar as mudanças const normalizedOriginal = fileModifications.originalContent.replace(/\r\n/g, '\n'); const normalizedCurrent = fileModifications.versions[fileModifications.versions.length - 1]?.content.replace(/\r\n/g, '\n') || ''; @@ -317,7 +503,7 @@ function File({ const showStats = additions > 0 || deletions > 0; return ( - +