mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 23:44:22 +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 { ActionIcon, Tooltip, Group } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
IconGitPullRequest,
|
IconGitPullRequest,
|
||||||
IconGitCommit,
|
IconGitCommit,
|
||||||
|
IconUpload,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useModalContext } from '../../contexts/ModalContext';
|
import { useModalContext } from '../../contexts/ModalContext';
|
||||||
import { useWorkspace } from '../../hooks/useWorkspace';
|
import { useWorkspace } from '../../hooks/useWorkspace';
|
||||||
@@ -12,11 +13,13 @@ import { useWorkspace } from '../../hooks/useWorkspace';
|
|||||||
interface FileActionsProps {
|
interface FileActionsProps {
|
||||||
handlePullChanges: () => Promise<boolean>;
|
handlePullChanges: () => Promise<boolean>;
|
||||||
selectedFile: string | null;
|
selectedFile: string | null;
|
||||||
|
onFileUpload?: (files: FileList) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileActions: React.FC<FileActionsProps> = ({
|
const FileActions: React.FC<FileActionsProps> = ({
|
||||||
handlePullChanges,
|
handlePullChanges,
|
||||||
selectedFile,
|
selectedFile,
|
||||||
|
onFileUpload,
|
||||||
}) => {
|
}) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const {
|
const {
|
||||||
@@ -25,10 +28,30 @@ const FileActions: React.FC<FileActionsProps> = ({
|
|||||||
setCommitMessageModalVisible,
|
setCommitMessageModalVisible,
|
||||||
} = useModalContext();
|
} = useModalContext();
|
||||||
|
|
||||||
|
// Hidden file input for upload
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleCreateFile = (): void => setNewFileModalVisible(true);
|
const handleCreateFile = (): void => setNewFileModalVisible(true);
|
||||||
const handleDeleteFile = (): void => setDeleteFileModalVisible(true);
|
const handleDeleteFile = (): void => setDeleteFileModalVisible(true);
|
||||||
const handleCommitAndPush = (): void => setCommitMessageModalVisible(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 (
|
return (
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Tooltip label="Create new file">
|
<Tooltip label="Create new file">
|
||||||
@@ -43,6 +66,18 @@ const FileActions: React.FC<FileActionsProps> = ({
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</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
|
<Tooltip
|
||||||
label={selectedFile ? 'Delete current file' : 'No file selected'}
|
label={selectedFile ? 'Delete current file' : 'No file selected'}
|
||||||
>
|
>
|
||||||
@@ -104,6 +139,16 @@ const FileActions: React.FC<FileActionsProps> = ({
|
|||||||
<IconGitCommit size={16} />
|
<IconGitCommit size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Hidden file input */}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileInputChange}
|
||||||
|
multiple
|
||||||
|
aria-label="File upload input"
|
||||||
|
/>
|
||||||
</Group>
|
</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 { Tree, type NodeApi } from 'react-arborist';
|
||||||
import { IconFile, IconFolder, IconFolderOpen } from '@tabler/icons-react';
|
import {
|
||||||
import { Tooltip } from '@mantine/core';
|
IconFile,
|
||||||
|
IconFolder,
|
||||||
|
IconFolderOpen,
|
||||||
|
IconUpload,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { Tooltip, Text, Box } from '@mantine/core';
|
||||||
import useResizeObserver from '@react-hook/resize-observer';
|
import useResizeObserver from '@react-hook/resize-observer';
|
||||||
import type { FileNode } from '@/types/models';
|
import type { FileNode } from '@/types/models';
|
||||||
|
|
||||||
@@ -14,6 +19,12 @@ interface FileTreeProps {
|
|||||||
files: FileNode[];
|
files: FileNode[];
|
||||||
handleFileSelect: (filePath: string | null) => Promise<void>;
|
handleFileSelect: (filePath: string | null) => Promise<void>;
|
||||||
showHiddenFiles: boolean;
|
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 => {
|
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({
|
function Node({
|
||||||
node,
|
node,
|
||||||
style,
|
style,
|
||||||
@@ -52,7 +63,6 @@ function Node({
|
|||||||
style: React.CSSProperties;
|
style: React.CSSProperties;
|
||||||
dragHandle?: React.Ref<HTMLDivElement>;
|
dragHandle?: React.Ref<HTMLDivElement>;
|
||||||
onNodeClick?: (node: NodeApi<FileNode>) => void;
|
onNodeClick?: (node: NodeApi<FileNode>) => void;
|
||||||
// Accept any extra props from Arborist, but do not use an index signature
|
|
||||||
} & Record<string, unknown>) {
|
} & Record<string, unknown>) {
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (node.isInternal) {
|
if (node.isInternal) {
|
||||||
@@ -65,7 +75,7 @@ function Node({
|
|||||||
return (
|
return (
|
||||||
<Tooltip label={node.data.name} openDelay={500}>
|
<Tooltip label={node.data.name} openDelay={500}>
|
||||||
<div
|
<div
|
||||||
ref={dragHandle}
|
ref={dragHandle} // This enables dragging for the node
|
||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
||||||
paddingLeft: `${node.level * 20}px`,
|
paddingLeft: `${node.level * 20}px`,
|
||||||
@@ -74,6 +84,8 @@ function Node({
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
// Add visual feedback when being dragged
|
||||||
|
opacity: node.state?.isDragging ? 0.5 : 1,
|
||||||
}}
|
}}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
{...rest}
|
{...rest}
|
||||||
@@ -99,10 +111,15 @@ const FileTree: React.FC<FileTreeProps> = ({
|
|||||||
files,
|
files,
|
||||||
handleFileSelect,
|
handleFileSelect,
|
||||||
showHiddenFiles,
|
showHiddenFiles,
|
||||||
|
onFileMove,
|
||||||
|
onFileUpload,
|
||||||
}) => {
|
}) => {
|
||||||
const target = useRef<HTMLDivElement>(null);
|
const target = useRef<HTMLDivElement>(null);
|
||||||
const size = useSize(target);
|
const size = useSize(target);
|
||||||
|
|
||||||
|
// State for drag and drop overlay
|
||||||
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
|
|
||||||
const filteredFiles = files.filter((file) => {
|
const filteredFiles = files.filter((file) => {
|
||||||
if (file.name.startsWith('.') && !showHiddenFiles) {
|
if (file.name.startsWith('.') && !showHiddenFiles) {
|
||||||
return false;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={target}
|
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 && (
|
{size && (
|
||||||
<Tree
|
<Tree
|
||||||
data={filteredFiles}
|
data={filteredFiles}
|
||||||
@@ -131,6 +247,7 @@ const FileTree: React.FC<FileTreeProps> = ({
|
|||||||
height={size.height}
|
height={size.height}
|
||||||
indent={24}
|
indent={24}
|
||||||
rowHeight={28}
|
rowHeight={28}
|
||||||
|
onMove={handleMove}
|
||||||
onActivate={(node) => {
|
onActivate={(node) => {
|
||||||
const fileNode = node.data;
|
const fileNode = node.data;
|
||||||
if (!node.isInternal) {
|
if (!node.isInternal) {
|
||||||
|
|||||||
Reference in New Issue
Block a user