From df8772de37daacfc2382f1b0d58794b8038f1d05 Mon Sep 17 00:00:00 2001 From: Richard Roberson Date: Thu, 13 Feb 2025 15:49:15 -0700 Subject: [PATCH] Fix drag and drop on collapsed folders --- src/components/doclist/CreateFolderDialog.tsx | 4 +- src/components/doclist/DocumentFolder.tsx | 25 +++------ src/components/doclist/DocumentList.tsx | 53 ++++++++++--------- src/components/doclist/DocumentListItem.tsx | 19 ++++--- 4 files changed, 49 insertions(+), 52 deletions(-) diff --git a/src/components/doclist/CreateFolderDialog.tsx b/src/components/doclist/CreateFolderDialog.tsx index fbef2bd..60d6881 100644 --- a/src/components/doclist/CreateFolderDialog.tsx +++ b/src/components/doclist/CreateFolderDialog.tsx @@ -1,11 +1,11 @@ -import { Fragment } from 'react'; +import { Fragment, KeyboardEvent } from 'react'; import { Dialog, DialogPanel, DialogTitle, Input, Transition, TransitionChild } from '@headlessui/react'; interface CreateFolderDialogProps { isOpen: boolean; folderName: string; onFolderNameChange: (name: string) => void; - onKeyDown: (e: React.KeyboardEvent) => void; + onKeyDown: (e: KeyboardEvent) => void; onClose: () => void; } diff --git a/src/components/doclist/DocumentFolder.tsx b/src/components/doclist/DocumentFolder.tsx index 117b74e..e90a55f 100644 --- a/src/components/doclist/DocumentFolder.tsx +++ b/src/components/doclist/DocumentFolder.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, DragEvent } from 'react'; import { Button } from '@headlessui/react'; import { DocumentListItem } from './DocumentListItem'; import { Folder, DocumentListDocument } from '@/types/documents'; @@ -11,12 +11,9 @@ interface DocumentFolderProps { sortedDocuments: DocumentListDocument[]; onDocumentDelete: (doc: DocumentListDocument) => void; draggedDoc: DocumentListDocument | null; - dropTargetDoc: DocumentListDocument | null; onDragStart: (doc: DocumentListDocument) => void; onDragEnd: () => void; - onDragOver: (e: React.DragEvent, doc: DocumentListDocument) => void; - onDragLeave: () => void; - onDrop: (e: React.DragEvent, doc: DocumentListDocument) => void; + onDrop: (e: DragEvent, folderId: string) => void; } const ChevronIcon = ({ className = "w-5 h-5" }) => ( @@ -37,38 +34,33 @@ export function DocumentFolder({ sortedDocuments, onDocumentDelete, draggedDoc, - dropTargetDoc, onDragStart, onDragEnd, - onDragOver, - onDragLeave, onDrop, }: DocumentFolderProps) { const [isHovering, setIsHovering] = useState(false); - const isDropTarget = isHovering && draggedDoc && !draggedDoc.folderId; + const isDropTarget = isHovering && draggedDoc && !draggedDoc.folderId && draggedDoc.id !== folder.id; return (
{ e.preventDefault(); e.stopPropagation(); - setIsHovering(true); if (draggedDoc && !draggedDoc.folderId) { - onDragOver(e, { ...draggedDoc, folderId: folder.id }); + setIsHovering(true); } }} onDragLeave={(e) => { e.preventDefault(); e.stopPropagation(); setIsHovering(false); - onDragLeave(); }} onDrop={(e) => { e.preventDefault(); e.stopPropagation(); setIsHovering(false); if (!draggedDoc || draggedDoc.folderId) return; - onDrop(e, { ...draggedDoc, folderId: folder.id }); + onDrop(e, folder.id); }} className={`rounded-lg p-2 transition-all bg-offbase shadow hover:shadow-md ${ isDropTarget ? 'ring-2 ring-accent bg-primary/10' : '' @@ -105,13 +97,10 @@ export function DocumentFolder({ key={`${doc.type}-${doc.id}`} doc={doc} onDelete={onDocumentDelete} - dragEnabled={true} + dragEnabled={false} // Documents in folders can't be dragged to other documents onDragStart={onDragStart} onDragEnd={onDragEnd} - onDragOver={onDragOver} - onDragLeave={onDragLeave} - onDrop={onDrop} - isDropTarget={dropTargetDoc?.id === doc.id} + isDropTarget={false} /> ))}
diff --git a/src/components/doclist/DocumentList.tsx b/src/components/doclist/DocumentList.tsx index 752adfb..214f822 100644 --- a/src/components/doclist/DocumentList.tsx +++ b/src/components/doclist/DocumentList.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useState, useEffect } from 'react'; +import { useCallback, useState, useEffect, DragEvent, KeyboardEvent } from 'react'; import { useDocuments } from '@/contexts/DocumentContext'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; @@ -172,7 +172,7 @@ export function DocumentList() { setDropTargetDoc(null); }, []); - const handleDragOver = useCallback((e: React.DragEvent, doc: DocumentListDocument) => { + const handleDragOver = useCallback((e: DragEvent, doc: DocumentListDocument) => { e.preventDefault(); if (draggedDoc && draggedDoc.id !== doc.id && !draggedDoc.folderId) { // Only highlight target if neither document is in a folder @@ -186,26 +186,14 @@ export function DocumentList() { setDropTargetDoc(null); }, []); - const handleDrop = useCallback((e: React.DragEvent, targetDoc: DocumentListDocument) => { + const handleDrop = useCallback((e: DragEvent, targetDoc: DocumentListDocument) => { e.preventDefault(); + console.log('Dropped', draggedDoc?.name, 'on', targetDoc.name); + if (!draggedDoc || draggedDoc.id === targetDoc.id || draggedDoc.folderId) return; - // If target has a folderId, we're dropping into an existing folder - if (targetDoc.folderId) { - const targetFolder = folders.find(f => f.id === targetDoc.folderId); - if (targetFolder && !targetFolder.documents.some(d => d.id === draggedDoc.id)) { - setFolders(prev => prev.map(f => { - if (f.id === targetDoc.folderId) { - return { - ...f, - documents: [...f.documents, { ...draggedDoc, folderId: f.id }] - }; - } - return f; - })); - } - } else { - // Create new folder when dropping on an unfoldered document + // If target doc is unfoldered, create a new folder + if (!targetDoc.folderId) { setPendingFolderDocs({ source: draggedDoc, target: targetDoc @@ -213,6 +201,25 @@ export function DocumentList() { setNewFolderName(''); } + setDraggedDoc(null); + setDropTargetDoc(null); + }, [draggedDoc]); + + const handleFolderDrop = useCallback((e: DragEvent, folderId: string) => { + e.preventDefault(); + if (!draggedDoc || draggedDoc.folderId) return; + + // Add document to existing folder + setFolders(folders.map(f => { + if (f.id === folderId && !f.documents.some(d => d.id === draggedDoc.id)) { + return { + ...f, + documents: [...f.documents, { ...draggedDoc, folderId }] + }; + } + return f; + })); + setDraggedDoc(null); setDropTargetDoc(null); }, [draggedDoc, folders]); @@ -255,7 +262,7 @@ export function DocumentList() { setShowHint(false); }, [pendingFolderDocs, newFolderName]); - const handleFolderNameKeyDown = useCallback((e: React.KeyboardEvent) => { + const handleFolderNameKeyDown = useCallback((e: KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); createFolder(); @@ -316,16 +323,12 @@ export function DocumentList() { sortedDocuments={sortDocuments(folder.documents)} onDocumentDelete={setDocumentToDelete} draggedDoc={draggedDoc} - dropTargetDoc={dropTargetDoc} onDragStart={handleDragStart} onDragEnd={handleDragEnd} - onDragOver={handleDragOver} - onDragLeave={handleDragLeave} - onDrop={handleDrop} + onDrop={handleFolderDrop} /> ))} - {sortDocuments(unfolderedDocuments).map(doc => ( void; onDragEnd?: () => void; - onDragOver?: (e: React.DragEvent, doc: DocumentListDocument) => void; + onDragOver?: (e: DragEvent, doc: DocumentListDocument) => void; onDragLeave?: () => void; - onDrop?: (e: React.DragEvent, doc: DocumentListDocument) => void; + onDrop?: (e: DragEvent, doc: DocumentListDocument) => void; isDropTarget?: boolean; } @@ -26,17 +27,21 @@ export function DocumentListItem({ onDrop, isDropTarget = false, }: DocumentListItemProps) { + // Only allow drag and drop interactions for documents not in folders + const isDraggable = dragEnabled && !doc.folderId; + const allowDropTarget = !doc.folderId; + return (
onDragStart?.(doc)} onDragEnd={onDragEnd} - onDragOver={(e) => onDragOver?.(e, doc)} - onDragLeave={onDragLeave} - onDrop={(e) => onDrop?.(e, doc)} + onDragOver={(e) => allowDropTarget && onDragOver?.(e, doc)} + onDragLeave={() => allowDropTarget && onDragLeave?.()} + onDrop={(e) => allowDropTarget && onDrop?.(e, doc)} className={` w-full - ${!doc.folderId && isDropTarget ? 'ring-2 ring-accent bg-primary/10' : ''} + ${allowDropTarget && isDropTarget ? 'ring-2 ring-accent bg-primary/10' : ''} bg-background rounded-lg p-2 shadow hover:shadow-md transition-shadow `} >