From b10591ee60a8627d36f4931c0ad6bb0ac1cbfa4c Mon Sep 17 00:00:00 2001 From: LordMathis Date: Mon, 7 Jul 2025 18:14:14 +0200 Subject: [PATCH] Add file upload functionality to FileActions and FileTree components --- app/src/components/files/FileActions.tsx | 47 +++++++- app/src/components/files/FileTree.tsx | 131 +++++++++++++++++++++-- 2 files changed, 170 insertions(+), 8 deletions(-) diff --git a/app/src/components/files/FileActions.tsx b/app/src/components/files/FileActions.tsx index a3f0075..8ef048e 100644 --- a/app/src/components/files/FileActions.tsx +++ b/app/src/components/files/FileActions.tsx @@ -1,10 +1,11 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { ActionIcon, Tooltip, Group } from '@mantine/core'; import { IconPlus, IconTrash, IconGitPullRequest, IconGitCommit, + IconUpload, } from '@tabler/icons-react'; import { useModalContext } from '../../contexts/ModalContext'; import { useWorkspace } from '../../hooks/useWorkspace'; @@ -12,11 +13,13 @@ import { useWorkspace } from '../../hooks/useWorkspace'; interface FileActionsProps { handlePullChanges: () => Promise; selectedFile: string | null; + onFileUpload?: (files: FileList) => Promise; } const FileActions: React.FC = ({ handlePullChanges, selectedFile, + onFileUpload, }) => { const { currentWorkspace } = useWorkspace(); const { @@ -25,10 +28,30 @@ const FileActions: React.FC = ({ setCommitMessageModalVisible, } = useModalContext(); + // Hidden file input for upload + const fileInputRef = useRef(null); + const handleCreateFile = (): void => setNewFileModalVisible(true); const handleDeleteFile = (): void => setDeleteFileModalVisible(true); const handleCommitAndPush = (): void => setCommitMessageModalVisible(true); + const handleUploadClick = (): void => { + fileInputRef.current?.click(); + }; + + const handleFileInputChange = ( + event: React.ChangeEvent + ): void => { + const files = event.target.files; + if (files && files.length > 0 && onFileUpload) { + onFileUpload(files).catch((error) => { + console.error('Error uploading files:', error); + }); + // Reset the input so the same file can be selected again + event.target.value = ''; + } + }; + return ( @@ -43,6 +66,18 @@ const FileActions: React.FC = ({ + + + + + + @@ -104,6 +139,16 @@ const FileActions: React.FC = ({ + + {/* Hidden file input */} + ); }; diff --git a/app/src/components/files/FileTree.tsx b/app/src/components/files/FileTree.tsx index 0c7e141..cc69954 100644 --- a/app/src/components/files/FileTree.tsx +++ b/app/src/components/files/FileTree.tsx @@ -1,7 +1,12 @@ -import React, { useRef, useState, useLayoutEffect } from 'react'; +import React, { useRef, useState, useLayoutEffect, useCallback } from 'react'; import { Tree, type NodeApi } from 'react-arborist'; -import { IconFile, IconFolder, IconFolderOpen } from '@tabler/icons-react'; -import { Tooltip } from '@mantine/core'; +import { + IconFile, + IconFolder, + IconFolderOpen, + IconUpload, +} from '@tabler/icons-react'; +import { Tooltip, Text, Box } from '@mantine/core'; import useResizeObserver from '@react-hook/resize-observer'; import type { FileNode } from '@/types/models'; @@ -14,6 +19,12 @@ interface FileTreeProps { files: FileNode[]; handleFileSelect: (filePath: string | null) => Promise; showHiddenFiles: boolean; + onFileMove?: ( + dragIds: string[], + parentId: string | null, + index: number + ) => Promise; + onFileUpload?: (files: FileList, targetPath?: string) => Promise; } const useSize = (target: React.RefObject): Size | undefined => { @@ -40,7 +51,7 @@ const FileIcon = ({ node }: { node: NodeApi }) => { ); }; -// Define a Node component that matches what React-Arborist expects +// Enhanced Node component with drag handle function Node({ node, style, @@ -52,7 +63,6 @@ function Node({ style: React.CSSProperties; dragHandle?: React.Ref; onNodeClick?: (node: NodeApi) => void; - // Accept any extra props from Arborist, but do not use an index signature } & Record) { const handleClick = () => { if (node.isInternal) { @@ -65,7 +75,7 @@ function Node({ return (
= ({ files, handleFileSelect, showHiddenFiles, + onFileMove, + onFileUpload, }) => { const target = useRef(null); const size = useSize(target); + // State for drag and drop overlay + const [isDragOver, setIsDragOver] = useState(false); + const filteredFiles = files.filter((file) => { if (file.name.startsWith('.') && !showHiddenFiles) { return false; @@ -118,11 +135,110 @@ const FileTree: React.FC = ({ } }; + // Handle file movement within the tree + const handleMove = useCallback( + async ({ + dragIds, + parentId, + index, + }: { + dragIds: string[]; + parentId: string | null; + index: number; + }) => { + if (onFileMove) { + await onFileMove(dragIds, parentId, index); + } + }, + [onFileMove] + ); + + // External file drag and drop handlers + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // Check if drag contains files (not internal tree nodes) + if (e.dataTransfer.types.includes('Files')) { + setIsDragOver(true); + } + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + // Only hide overlay when leaving the container itself + if (e.currentTarget === e.target) { + setIsDragOver(false); + } + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + // Set the drop effect to indicate this is a valid drop target + e.dataTransfer.dropEffect = 'copy'; + }, []); + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + setIsDragOver(false); + + const { files } = e.dataTransfer; + if (files && files.length > 0 && onFileUpload) { + // Handle the upload without awaiting to avoid the eslint warning + onFileUpload(files).catch((error) => { + console.error('Error uploading files:', error); + }); + } + }, + [onFileUpload] + ); + return (
+ {/* Drag overlay */} + {isDragOver && ( + + + + Drop files here to upload + + + )} + {size && ( = ({ height={size.height} indent={24} rowHeight={28} + onMove={handleMove} onActivate={(node) => { const fileNode = node.data; if (!node.isInternal) {