Merge pull request #69 from lordmathis/fix/drag-and-drop

Fix FileTree drag and drop
This commit is contained in:
2025-11-04 18:25:09 +01:00
committed by GitHub

View File

@@ -6,7 +6,7 @@ import {
IconFolderOpen, IconFolderOpen,
IconUpload, IconUpload,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { Tooltip, Text, Box } from '@mantine/core'; import { Text, Box } from '@mantine/core';
import useResizeObserver from '@react-hook/resize-observer'; import useResizeObserver from '@react-hook/resize-observer';
import { useFileOperations } from '../../hooks/useFileOperations'; import { useFileOperations } from '../../hooks/useFileOperations';
import type { FileNode } from '@/types/models'; import type { FileNode } from '@/types/models';
@@ -53,13 +53,12 @@ function Node({
style, style,
dragHandle, dragHandle,
onNodeClick, onNodeClick,
...rest
}: { }: {
node: NodeApi<FileNode>; node: NodeApi<FileNode>;
style: React.CSSProperties; style: React.CSSProperties;
dragHandle?: React.Ref<HTMLDivElement>; dragHandle?: React.Ref<HTMLDivElement>;
onNodeClick?: (node: NodeApi<FileNode>) => void; onNodeClick?: (node: NodeApi<FileNode>) => void;
} & Record<string, unknown>) { }) {
const handleClick = () => { const handleClick = () => {
if (node.isInternal) { if (node.isInternal) {
node.toggle(); node.toggle();
@@ -69,37 +68,40 @@ function Node({
}; };
return ( return (
<Tooltip label={node.data.name} openDelay={500}> <div
<div ref={dragHandle} // This enables dragging for the node
ref={dragHandle} // This enables dragging for the node style={{
...style,
paddingLeft: `${node.level * 20}px`,
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
whiteSpace: 'nowrap',
overflow: 'hidden',
// Add visual feedback when being dragged
opacity: node.state?.isDragging ? 0.5 : 1,
// Highlight when this node will receive the drop
backgroundColor: node.state?.willReceiveDrop
? 'rgba(0, 123, 255, 0.2)'
: 'transparent',
borderRadius: '4px',
}}
onClick={handleClick}
title={node.data.name}
>
<FileIcon node={node} />
<span
style={{ style={{
...style, marginLeft: '8px',
paddingLeft: `${node.level * 20}px`, fontSize: '14px',
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
whiteSpace: 'nowrap',
overflow: 'hidden', overflow: 'hidden',
// Add visual feedback when being dragged textOverflow: 'ellipsis',
opacity: node.state?.isDragging ? 0.5 : 1, flexGrow: 1,
}} }}
onClick={handleClick}
{...rest}
> >
<FileIcon node={node} /> {node.data.name}
<span </span>
style={{ </div>
marginLeft: '8px',
fontSize: '14px',
overflow: 'hidden',
textOverflow: 'ellipsis',
flexGrow: 1,
}}
>
{node.data.name}
</span>
</div>
</Tooltip>
); );
} }
@@ -205,41 +207,46 @@ export const FileTree: React.FC<FileTreeProps> = ({
// External file drag and drop handlers // External file drag and drop handlers
const handleDragEnter = useCallback((e: React.DragEvent) => { const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
// Check if drag contains files (not internal tree nodes) // Check if drag contains files (not internal tree nodes)
if (e.dataTransfer.types.includes('Files')) { if (e.dataTransfer.types.includes('Files')) {
e.preventDefault();
e.stopPropagation();
setIsDragOver(true); setIsDragOver(true);
} }
}, []); }, []);
const handleDragLeave = useCallback((e: React.DragEvent) => { const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault(); // Only handle if it's an external file drag
e.stopPropagation(); if (e.dataTransfer.types.includes('Files')) {
e.preventDefault();
e.stopPropagation();
// Only hide overlay when leaving the container itself // Only hide overlay when leaving the container itself
if (e.currentTarget === e.target) { if (e.currentTarget === e.target) {
setIsDragOver(false); setIsDragOver(false);
}
} }
}, []); }, []);
const handleDragOver = useCallback((e: React.DragEvent) => { const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault(); // Only handle external file drags
e.stopPropagation(); if (e.dataTransfer.types.includes('Files')) {
// Set the drop effect to indicate this is a valid drop target e.preventDefault();
e.dataTransfer.dropEffect = 'copy'; e.stopPropagation();
// Set the drop effect to indicate this is a valid drop target
e.dataTransfer.dropEffect = 'copy';
}
}, []); }, []);
const handleDrop = useCallback( const handleDrop = useCallback(
(e: React.DragEvent) => { (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
const { files } = e.dataTransfer; const { files } = e.dataTransfer;
// Only handle if it's an external file drop
if (files && files.length > 0) { if (files && files.length > 0) {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
const uploadFiles = async () => { const uploadFiles = async () => {
try { try {
const success = await handleUpload(files); const success = await handleUpload(files);
@@ -305,7 +312,10 @@ export const FileTree: React.FC<FileTreeProps> = ({
height={size.height} height={size.height}
indent={24} indent={24}
rowHeight={28} rowHeight={28}
idAccessor="id"
onMove={handleTreeMove} onMove={handleTreeMove}
disableDrag={() => false}
disableDrop={() => false}
onActivate={(node) => { onActivate={(node) => {
const fileNode = node.data; const fileNode = node.data;
if (!node.isInternal) { if (!node.isInternal) {