Fix drag and drop on collapsed folders
This commit is contained in:
parent
a1f069c0ae
commit
df8772de37
4 changed files with 49 additions and 52 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
import { Fragment } from 'react';
|
import { Fragment, KeyboardEvent } from 'react';
|
||||||
import { Dialog, DialogPanel, DialogTitle, Input, Transition, TransitionChild } from '@headlessui/react';
|
import { Dialog, DialogPanel, DialogTitle, Input, Transition, TransitionChild } from '@headlessui/react';
|
||||||
|
|
||||||
interface CreateFolderDialogProps {
|
interface CreateFolderDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
folderName: string;
|
folderName: string;
|
||||||
onFolderNameChange: (name: string) => void;
|
onFolderNameChange: (name: string) => void;
|
||||||
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useState, DragEvent } from 'react';
|
||||||
import { Button } from '@headlessui/react';
|
import { Button } from '@headlessui/react';
|
||||||
import { DocumentListItem } from './DocumentListItem';
|
import { DocumentListItem } from './DocumentListItem';
|
||||||
import { Folder, DocumentListDocument } from '@/types/documents';
|
import { Folder, DocumentListDocument } from '@/types/documents';
|
||||||
|
|
@ -11,12 +11,9 @@ interface DocumentFolderProps {
|
||||||
sortedDocuments: DocumentListDocument[];
|
sortedDocuments: DocumentListDocument[];
|
||||||
onDocumentDelete: (doc: DocumentListDocument) => void;
|
onDocumentDelete: (doc: DocumentListDocument) => void;
|
||||||
draggedDoc: DocumentListDocument | null;
|
draggedDoc: DocumentListDocument | null;
|
||||||
dropTargetDoc: DocumentListDocument | null;
|
|
||||||
onDragStart: (doc: DocumentListDocument) => void;
|
onDragStart: (doc: DocumentListDocument) => void;
|
||||||
onDragEnd: () => void;
|
onDragEnd: () => void;
|
||||||
onDragOver: (e: React.DragEvent, doc: DocumentListDocument) => void;
|
onDrop: (e: DragEvent, folderId: string) => void;
|
||||||
onDragLeave: () => void;
|
|
||||||
onDrop: (e: React.DragEvent, doc: DocumentListDocument) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChevronIcon = ({ className = "w-5 h-5" }) => (
|
const ChevronIcon = ({ className = "w-5 h-5" }) => (
|
||||||
|
|
@ -37,38 +34,33 @@ export function DocumentFolder({
|
||||||
sortedDocuments,
|
sortedDocuments,
|
||||||
onDocumentDelete,
|
onDocumentDelete,
|
||||||
draggedDoc,
|
draggedDoc,
|
||||||
dropTargetDoc,
|
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
onDragOver,
|
|
||||||
onDragLeave,
|
|
||||||
onDrop,
|
onDrop,
|
||||||
}: DocumentFolderProps) {
|
}: DocumentFolderProps) {
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
const isDropTarget = isHovering && draggedDoc && !draggedDoc.folderId;
|
const isDropTarget = isHovering && draggedDoc && !draggedDoc.folderId && draggedDoc.id !== folder.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onDragOver={(e) => {
|
onDragOver={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsHovering(true);
|
|
||||||
if (draggedDoc && !draggedDoc.folderId) {
|
if (draggedDoc && !draggedDoc.folderId) {
|
||||||
onDragOver(e, { ...draggedDoc, folderId: folder.id });
|
setIsHovering(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDragLeave={(e) => {
|
onDragLeave={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsHovering(false);
|
setIsHovering(false);
|
||||||
onDragLeave();
|
|
||||||
}}
|
}}
|
||||||
onDrop={(e) => {
|
onDrop={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsHovering(false);
|
setIsHovering(false);
|
||||||
if (!draggedDoc || draggedDoc.folderId) return;
|
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 ${
|
className={`rounded-lg p-2 transition-all bg-offbase shadow hover:shadow-md ${
|
||||||
isDropTarget ? 'ring-2 ring-accent bg-primary/10' : ''
|
isDropTarget ? 'ring-2 ring-accent bg-primary/10' : ''
|
||||||
|
|
@ -105,13 +97,10 @@ export function DocumentFolder({
|
||||||
key={`${doc.type}-${doc.id}`}
|
key={`${doc.type}-${doc.id}`}
|
||||||
doc={doc}
|
doc={doc}
|
||||||
onDelete={onDocumentDelete}
|
onDelete={onDocumentDelete}
|
||||||
dragEnabled={true}
|
dragEnabled={false} // Documents in folders can't be dragged to other documents
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
onDragOver={onDragOver}
|
isDropTarget={false}
|
||||||
onDragLeave={onDragLeave}
|
|
||||||
onDrop={onDrop}
|
|
||||||
isDropTarget={dropTargetDoc?.id === doc.id}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useState, useEffect } from 'react';
|
import { useCallback, useState, useEffect, DragEvent, KeyboardEvent } from 'react';
|
||||||
import { useDocuments } from '@/contexts/DocumentContext';
|
import { useDocuments } from '@/contexts/DocumentContext';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
|
@ -172,7 +172,7 @@ export function DocumentList() {
|
||||||
setDropTargetDoc(null);
|
setDropTargetDoc(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent, doc: DocumentListDocument) => {
|
const handleDragOver = useCallback((e: DragEvent, doc: DocumentListDocument) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (draggedDoc && draggedDoc.id !== doc.id && !draggedDoc.folderId) {
|
if (draggedDoc && draggedDoc.id !== doc.id && !draggedDoc.folderId) {
|
||||||
// Only highlight target if neither document is in a folder
|
// Only highlight target if neither document is in a folder
|
||||||
|
|
@ -186,26 +186,14 @@ export function DocumentList() {
|
||||||
setDropTargetDoc(null);
|
setDropTargetDoc(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDrop = useCallback((e: React.DragEvent, targetDoc: DocumentListDocument) => {
|
const handleDrop = useCallback((e: DragEvent, targetDoc: DocumentListDocument) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
console.log('Dropped', draggedDoc?.name, 'on', targetDoc.name);
|
||||||
|
|
||||||
if (!draggedDoc || draggedDoc.id === targetDoc.id || draggedDoc.folderId) return;
|
if (!draggedDoc || draggedDoc.id === targetDoc.id || draggedDoc.folderId) return;
|
||||||
|
|
||||||
// If target has a folderId, we're dropping into an existing folder
|
// If target doc is unfoldered, create a new folder
|
||||||
if (targetDoc.folderId) {
|
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
|
|
||||||
setPendingFolderDocs({
|
setPendingFolderDocs({
|
||||||
source: draggedDoc,
|
source: draggedDoc,
|
||||||
target: targetDoc
|
target: targetDoc
|
||||||
|
|
@ -213,6 +201,25 @@ export function DocumentList() {
|
||||||
setNewFolderName('');
|
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);
|
setDraggedDoc(null);
|
||||||
setDropTargetDoc(null);
|
setDropTargetDoc(null);
|
||||||
}, [draggedDoc, folders]);
|
}, [draggedDoc, folders]);
|
||||||
|
|
@ -255,7 +262,7 @@ export function DocumentList() {
|
||||||
setShowHint(false);
|
setShowHint(false);
|
||||||
}, [pendingFolderDocs, newFolderName]);
|
}, [pendingFolderDocs, newFolderName]);
|
||||||
|
|
||||||
const handleFolderNameKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleFolderNameKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
createFolder();
|
createFolder();
|
||||||
|
|
@ -316,16 +323,12 @@ export function DocumentList() {
|
||||||
sortedDocuments={sortDocuments(folder.documents)}
|
sortedDocuments={sortDocuments(folder.documents)}
|
||||||
onDocumentDelete={setDocumentToDelete}
|
onDocumentDelete={setDocumentToDelete}
|
||||||
draggedDoc={draggedDoc}
|
draggedDoc={draggedDoc}
|
||||||
dropTargetDoc={dropTargetDoc}
|
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
onDragOver={handleDragOver}
|
onDrop={handleFolderDrop}
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
||||||
{sortDocuments(unfolderedDocuments).map(doc => (
|
{sortDocuments(unfolderedDocuments).map(doc => (
|
||||||
<DocumentListItem
|
<DocumentListItem
|
||||||
key={`${doc.type}-${doc.id}`}
|
key={`${doc.type}-${doc.id}`}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { DragEvent } from 'react';
|
||||||
import { Button } from '@headlessui/react';
|
import { Button } from '@headlessui/react';
|
||||||
import { PDFIcon, EPUBIcon } from '@/components/icons/Icons';
|
import { PDFIcon, EPUBIcon } from '@/components/icons/Icons';
|
||||||
import { DocumentListDocument } from '@/types/documents';
|
import { DocumentListDocument } from '@/types/documents';
|
||||||
|
|
@ -9,9 +10,9 @@ interface DocumentListItemProps {
|
||||||
dragEnabled?: boolean;
|
dragEnabled?: boolean;
|
||||||
onDragStart?: (doc: DocumentListDocument) => void;
|
onDragStart?: (doc: DocumentListDocument) => void;
|
||||||
onDragEnd?: () => void;
|
onDragEnd?: () => void;
|
||||||
onDragOver?: (e: React.DragEvent, doc: DocumentListDocument) => void;
|
onDragOver?: (e: DragEvent, doc: DocumentListDocument) => void;
|
||||||
onDragLeave?: () => void;
|
onDragLeave?: () => void;
|
||||||
onDrop?: (e: React.DragEvent, doc: DocumentListDocument) => void;
|
onDrop?: (e: DragEvent, doc: DocumentListDocument) => void;
|
||||||
isDropTarget?: boolean;
|
isDropTarget?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,17 +27,21 @@ export function DocumentListItem({
|
||||||
onDrop,
|
onDrop,
|
||||||
isDropTarget = false,
|
isDropTarget = false,
|
||||||
}: DocumentListItemProps) {
|
}: DocumentListItemProps) {
|
||||||
|
// Only allow drag and drop interactions for documents not in folders
|
||||||
|
const isDraggable = dragEnabled && !doc.folderId;
|
||||||
|
const allowDropTarget = !doc.folderId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
draggable={dragEnabled && !doc.folderId}
|
draggable={isDraggable}
|
||||||
onDragStart={() => onDragStart?.(doc)}
|
onDragStart={() => onDragStart?.(doc)}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
onDragOver={(e) => onDragOver?.(e, doc)}
|
onDragOver={(e) => allowDropTarget && onDragOver?.(e, doc)}
|
||||||
onDragLeave={onDragLeave}
|
onDragLeave={() => allowDropTarget && onDragLeave?.()}
|
||||||
onDrop={(e) => onDrop?.(e, doc)}
|
onDrop={(e) => allowDropTarget && onDrop?.(e, doc)}
|
||||||
className={`
|
className={`
|
||||||
w-full
|
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
|
bg-background rounded-lg p-2 shadow hover:shadow-md transition-shadow
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue