Add drag-and-drop file organization to FileTree
Implemented intuitive drag-and-drop functionality: - Drag files and folders to reorganize the project structure - Visual feedback with opacity and border highlights during drag - Prevents invalid drops (e.g., dropping on itself) - Toast notifications for successful moves - Seamless integration with existing file tree state management
This commit is contained in:
parent
538c1ff44b
commit
024ec42c5e
2 changed files with 111 additions and 1 deletions
46
src/App.tsx
46
src/App.tsx
|
|
@ -274,6 +274,50 @@ end)`,
|
|||
captureEvent('file_delete', { id });
|
||||
};
|
||||
|
||||
const handleFileMove = (fileId: string, targetParentId: string) => {
|
||||
setFiles((prev) => {
|
||||
let movedNode: FileNode | null = null;
|
||||
|
||||
// First, find and remove the node from its current location
|
||||
const removeNode = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.filter((node) => {
|
||||
if (node.id === fileId) {
|
||||
movedNode = node;
|
||||
return false;
|
||||
}
|
||||
if (node.children) {
|
||||
node.children = removeNode(node.children);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
// Then, add the node to the target folder
|
||||
const addToTarget = (nodes: FileNode[]): FileNode[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.id === targetParentId && node.type === 'folder') {
|
||||
return {
|
||||
...node,
|
||||
children: [...(node.children || []), movedNode!],
|
||||
};
|
||||
}
|
||||
if (node.children) {
|
||||
return { ...node, children: addToTarget(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
const withoutMoved = removeNode(prev || []);
|
||||
if (movedNode) {
|
||||
return addToTarget(withoutMoved);
|
||||
}
|
||||
return prev || [];
|
||||
});
|
||||
|
||||
captureEvent('file_move', { fileId, targetParentId });
|
||||
};
|
||||
|
||||
const handleFileClose = (id: string) => {
|
||||
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
|
||||
if (activeFileId === id) {
|
||||
|
|
@ -335,6 +379,7 @@ end)`,
|
|||
onFileCreate={handleFileCreate}
|
||||
onFileRename={handleFileRename}
|
||||
onFileDelete={handleFileDelete}
|
||||
onFileMove={handleFileMove}
|
||||
selectedFileId={activeFileId}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
|
@ -369,6 +414,7 @@ end)`,
|
|||
onFileCreate={handleFileCreate}
|
||||
onFileRename={handleFileRename}
|
||||
onFileDelete={handleFileDelete}
|
||||
onFileMove={handleFileMove}
|
||||
selectedFileId={activeFileId}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ interface FileTreeProps {
|
|||
onFileCreate: (name: string, parentId?: string) => void;
|
||||
onFileRename: (id: string, newName: string) => void;
|
||||
onFileDelete: (id: string) => void;
|
||||
onFileMove?: (fileId: string, targetParentId: string) => void;
|
||||
selectedFileId?: string;
|
||||
}
|
||||
|
||||
|
|
@ -42,11 +43,14 @@ export function FileTree({
|
|||
onFileCreate,
|
||||
onFileRename,
|
||||
onFileDelete,
|
||||
onFileMove,
|
||||
selectedFileId,
|
||||
}: FileTreeProps) {
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(['root']));
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editingName, setEditingName] = useState('');
|
||||
const [draggedId, setDraggedId] = useState<string | null>(null);
|
||||
const [dropTargetId, setDropTargetId] = useState<string | null>(null);
|
||||
|
||||
const toggleFolder = (id: string) => {
|
||||
setExpandedFolders((prev) => {
|
||||
|
|
@ -81,17 +85,77 @@ export function FileTree({
|
|||
}
|
||||
};
|
||||
|
||||
const handleDragStart = (e: React.DragEvent, node: FileNode) => {
|
||||
e.stopPropagation();
|
||||
setDraggedId(node.id);
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', node.id);
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent, node: FileNode) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Only allow dropping on folders
|
||||
if (node.type === 'folder' && draggedId !== node.id) {
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
setDropTargetId(node.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDropTargetId(null);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent, targetNode: FileNode) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!draggedId || !onFileMove || targetNode.type !== 'folder') {
|
||||
setDraggedId(null);
|
||||
setDropTargetId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent dropping on itself or its children
|
||||
if (draggedId === targetNode.id) {
|
||||
setDraggedId(null);
|
||||
setDropTargetId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
onFileMove(draggedId, targetNode.id);
|
||||
toast.success('File moved');
|
||||
setDraggedId(null);
|
||||
setDropTargetId(null);
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
setDraggedId(null);
|
||||
setDropTargetId(null);
|
||||
};
|
||||
|
||||
const renderNode = (node: FileNode, depth: number = 0) => {
|
||||
const isExpanded = expandedFolders.has(node.id);
|
||||
const isSelected = selectedFileId === node.id;
|
||||
const isEditing = editingId === node.id;
|
||||
const isDragging = draggedId === node.id;
|
||||
const isDropTarget = dropTargetId === node.id;
|
||||
|
||||
return (
|
||||
<div key={node.id}>
|
||||
<div
|
||||
draggable={!isEditing}
|
||||
onDragStart={(e) => handleDragStart(e, node)}
|
||||
onDragOver={(e) => handleDragOver(e, node)}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={(e) => handleDrop(e, node)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className={`flex items-center gap-1 px-2 py-1 hover:bg-muted/60 cursor-pointer group rounded-sm transition-colors ${
|
||||
isSelected ? 'bg-accent/30 text-accent font-semibold' : 'text-foreground'
|
||||
}`}
|
||||
} ${isDragging ? 'opacity-50' : ''} ${isDropTarget && node.type === 'folder' ? 'bg-blue-500/20 border-2 border-blue-500 border-dashed' : ''}`}
|
||||
style={{ paddingLeft: `${depth * 10 + 8}px` }}
|
||||
onClick={() => {
|
||||
if (node.type === 'folder') {
|
||||
|
|
|
|||
Loading…
Reference in a new issue