mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
Add file upload functionality to FileActions and FileTree components
This commit is contained in:
@@ -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<boolean>;
|
||||
selectedFile: string | null;
|
||||
onFileUpload?: (files: FileList) => Promise<void>;
|
||||
}
|
||||
|
||||
const FileActions: React.FC<FileActionsProps> = ({
|
||||
handlePullChanges,
|
||||
selectedFile,
|
||||
onFileUpload,
|
||||
}) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const {
|
||||
@@ -25,10 +28,30 @@ const FileActions: React.FC<FileActionsProps> = ({
|
||||
setCommitMessageModalVisible,
|
||||
} = useModalContext();
|
||||
|
||||
// Hidden file input for upload
|
||||
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>
|
||||
): 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 (
|
||||
<Group gap="xs">
|
||||
<Tooltip label="Create new file">
|
||||
@@ -43,6 +66,18 @@ const FileActions: React.FC<FileActionsProps> = ({
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Upload files">
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="md"
|
||||
onClick={handleUploadClick}
|
||||
aria-label="Upload files"
|
||||
data-testid="upload-files-button"
|
||||
>
|
||||
<IconUpload size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
label={selectedFile ? 'Delete current file' : 'No file selected'}
|
||||
>
|
||||
@@ -104,6 +139,16 @@ const FileActions: React.FC<FileActionsProps> = ({
|
||||
<IconGitCommit size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
{/* Hidden file input */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileInputChange}
|
||||
multiple
|
||||
aria-label="File upload input"
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<void>;
|
||||
showHiddenFiles: boolean;
|
||||
onFileMove?: (
|
||||
dragIds: string[],
|
||||
parentId: string | null,
|
||||
index: number
|
||||
) => Promise<void>;
|
||||
onFileUpload?: (files: FileList, targetPath?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const useSize = (target: React.RefObject<HTMLElement>): Size | undefined => {
|
||||
@@ -40,7 +51,7 @@ const FileIcon = ({ node }: { node: NodeApi<FileNode> }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// 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<HTMLDivElement>;
|
||||
onNodeClick?: (node: NodeApi<FileNode>) => void;
|
||||
// Accept any extra props from Arborist, but do not use an index signature
|
||||
} & Record<string, unknown>) {
|
||||
const handleClick = () => {
|
||||
if (node.isInternal) {
|
||||
@@ -65,7 +75,7 @@ function Node({
|
||||
return (
|
||||
<Tooltip label={node.data.name} openDelay={500}>
|
||||
<div
|
||||
ref={dragHandle}
|
||||
ref={dragHandle} // This enables dragging for the node
|
||||
style={{
|
||||
...style,
|
||||
paddingLeft: `${node.level * 20}px`,
|
||||
@@ -74,6 +84,8 @@ function Node({
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
// Add visual feedback when being dragged
|
||||
opacity: node.state?.isDragging ? 0.5 : 1,
|
||||
}}
|
||||
onClick={handleClick}
|
||||
{...rest}
|
||||
@@ -99,10 +111,15 @@ const FileTree: React.FC<FileTreeProps> = ({
|
||||
files,
|
||||
handleFileSelect,
|
||||
showHiddenFiles,
|
||||
onFileMove,
|
||||
onFileUpload,
|
||||
}) => {
|
||||
const target = useRef<HTMLDivElement>(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<FileTreeProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<div
|
||||
ref={target}
|
||||
style={{ height: 'calc(100vh - 140px)', marginTop: '20px' }}
|
||||
style={{
|
||||
height: 'calc(100vh - 140px)',
|
||||
marginTop: '20px',
|
||||
position: 'relative',
|
||||
}}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{/* Drag overlay */}
|
||||
{isDragOver && (
|
||||
<Box
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
||||
border: '2px dashed var(--mantine-color-blue-6)',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
zIndex: 1000,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<IconUpload size={48} color="var(--mantine-color-blue-6)" />
|
||||
<Text size="lg" fw={500} c="blue" mt="md">
|
||||
Drop files here to upload
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{size && (
|
||||
<Tree
|
||||
data={filteredFiles}
|
||||
@@ -131,6 +247,7 @@ const FileTree: React.FC<FileTreeProps> = ({
|
||||
height={size.height}
|
||||
indent={24}
|
||||
rowHeight={28}
|
||||
onMove={handleMove}
|
||||
onActivate={(node) => {
|
||||
const fileNode = node.data;
|
||||
if (!node.isInternal) {
|
||||
|
||||
Reference in New Issue
Block a user