From e0d63d94b47f1c84b191f353d1d89d631000d8ed Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Fri, 28 Nov 2025 20:12:56 +0100 Subject: [PATCH] feat: Enable moving and dragging selected folders, including mixed document and folder selections, with updated drag preview visuals and minor UI styling. --- .../documents/SelectionFloatingActions.tsx | 9 +-- .../documents/useDocumentDragHandlers.ts | 66 +++++++++++++++---- frontend/src/styles/detail/detail-panels.css | 5 ++ frontend/src/styles/documents/listing.css | 4 +- 4 files changed, 64 insertions(+), 20 deletions(-) diff --git a/frontend/src/documents/SelectionFloatingActions.tsx b/frontend/src/documents/SelectionFloatingActions.tsx index c1279dc..cb8d68d 100644 --- a/frontend/src/documents/SelectionFloatingActions.tsx +++ b/frontend/src/documents/SelectionFloatingActions.tsx @@ -360,12 +360,13 @@ const SelectionFloatingActions: React.FC = ({ const handleMoveSelectionToFolder = useCallback( async (folderId: DocumentId | null) => { - if (!documentIdList.length || !onMoveDocumentsToFolder) { + const itemsToMove = [...documentIdList, ...folderIdList]; + if (!itemsToMove.length || !onMoveDocumentsToFolder) { return; } - await onMoveDocumentsToFolder(documentIdList, folderId); + await onMoveDocumentsToFolder(itemsToMove, folderId); }, - [documentIdList, onMoveDocumentsToFolder], + [documentIdList, folderIdList, onMoveDocumentsToFolder], ); const summaryNode = totalCount > 0 ? ( @@ -395,7 +396,7 @@ const SelectionFloatingActions: React.FC = ({ placeholder="Search folders…" emptyMessage="No folders" onSelectFolder={handleMoveSelectionToFolder} - disabled={!documentCount} + disabled={!documentCount && !folderCount} onOpenMenu={handleMoveMenuOpen} rootTitle={rootTitle} /> diff --git a/frontend/src/hooks/documents/useDocumentDragHandlers.ts b/frontend/src/hooks/documents/useDocumentDragHandlers.ts index 422bbe9..7e17986 100644 --- a/frontend/src/hooks/documents/useDocumentDragHandlers.ts +++ b/frontend/src/hooks/documents/useDocumentDragHandlers.ts @@ -58,7 +58,7 @@ const useDocumentDragHandlers = ({ useEffect(() => destroyDragPreview, [destroyDragPreview]); const createDragPreview = useCallback( - ({ documents = [], folders = [] }: { documents?: Document[]; folders?: FolderIdentifier[] } = {}) => { + ({ documents = [], folders = [], prioritizeFolders = false }: { documents?: Document[]; folders?: FolderIdentifier[]; prioritizeFolders?: boolean } = {}) => { destroyDragPreview(); const docEntries = (documents || []).filter(Boolean); @@ -72,15 +72,44 @@ const useDocumentDragHandlers = ({ const size = 64; const canvasSize = Math.round(size * 1.6); - const visibleItems = []; - docEntries.slice(0, maxVisible).forEach((doc) => { - visibleItems.push({ type: 'document', payload: doc }); - }); + const visibleItems: Array<{ type: 'document' | 'folder'; payload: any }> = []; - if (visibleItems.length < maxVisible) { - folderEntries - .slice(0, maxVisible - visibleItems.length) - .forEach((folderId) => visibleItems.push({ type: 'folder', payload: folderId })); + let takeDocs = 0; + let takeFolders = 0; + + if (docEntries.length > 0 && folderEntries.length > 0) { + if (prioritizeFolders) { + // Folders on top (added last) + takeDocs = Math.min(docEntries.length, maxVisible - 1); + takeFolders = Math.min(folderEntries.length, maxVisible - takeDocs); + } else { + // Docs on top (added last) + takeFolders = Math.min(folderEntries.length, maxVisible - 1); + takeDocs = Math.min(docEntries.length, maxVisible - takeFolders); + } + } else { + takeDocs = Math.min(docEntries.length, maxVisible); + takeFolders = Math.min(folderEntries.length, maxVisible - takeDocs); + } + + if (prioritizeFolders) { + // Docs at bottom + docEntries.slice(0, takeDocs).forEach((doc) => { + visibleItems.push({ type: 'document', payload: doc }); + }); + // Folders at top + folderEntries.slice(0, takeFolders).forEach((folderId) => { + visibleItems.push({ type: 'folder', payload: folderId }); + }); + } else { + // Folders at bottom + folderEntries.slice(0, takeFolders).forEach((folderId) => { + visibleItems.push({ type: 'folder', payload: folderId }); + }); + // Docs at top + docEntries.slice(0, takeDocs).forEach((doc) => { + visibleItems.push({ type: 'document', payload: doc }); + }); } const wrapper = document.createElement('div'); @@ -152,16 +181,18 @@ const useDocumentDragHandlers = ({ layer.style.height = `${size}px`; layer.classList.add('document-drag-preview__item--folder'); - let content: HTMLElement | null = null; + let content: HTMLElement | SVGElement | null = null; if (iconEl instanceof HTMLElement) { const cloneSource = iconEl.classList.contains('folder-card__icon') ? iconEl.querySelector('svg') || iconEl : iconEl; const clone = cloneSource.cloneNode(true); - if (clone instanceof HTMLElement) { - content = clone; + if (clone instanceof HTMLElement || clone instanceof SVGElement) { + content = clone as HTMLElement | SVGElement; content.classList.add('document-drag-preview__folder-thumb'); - const svg = content.querySelector('svg'); + const svg = content.nodeName.toLowerCase() === 'svg' + ? content + : content.querySelector('svg'); if (svg) { svg.setAttribute('width', '48'); svg.setAttribute('height', '48'); @@ -213,7 +244,11 @@ const useDocumentDragHandlers = ({ const selection: Identifier[] = isAlreadySelected ? [...selectedDocumentIds] : [documentId]; - const folderSelection: FolderIdentifier[] = []; + + // If the document is part of the selection, we also want to include any selected folders + const folderSelection: FolderIdentifier[] = isAlreadySelected + ? normalizedFolderIds + : []; if (!isAlreadySelected) { applySelection([documentKey], { @@ -228,6 +263,7 @@ const useDocumentDragHandlers = ({ const previewNode = createDragPreview({ documents: previewDocs, folders: folderSelection, + prioritizeFolders: false, }); setDraggedDocumentIds(selection); @@ -266,6 +302,7 @@ const useDocumentDragHandlers = ({ createDragPreview, setDraggedFolderId, setDraggedDocumentIds, + normalizedFolderIds, ], ); @@ -331,6 +368,7 @@ const useDocumentDragHandlers = ({ .map((id) => documentLookup.get(id) || documentLookup.get(String(id)) || null) .filter(Boolean), folders: uniqueFolders, + prioritizeFolders: true, }); event.currentTarget.classList.add('dragging'); diff --git a/frontend/src/styles/detail/detail-panels.css b/frontend/src/styles/detail/detail-panels.css index 64de28d..3776f0d 100644 --- a/frontend/src/styles/detail/detail-panels.css +++ b/frontend/src/styles/detail/detail-panels.css @@ -852,4 +852,9 @@ .menu .selection-assignment__item--empty:focus-visible { background: transparent; cursor: default; +} + +.selection-assignment__item--empty .selection-assignment__item-content { + opacity: 0.5; + cursor: default; } \ No newline at end of file diff --git a/frontend/src/styles/documents/listing.css b/frontend/src/styles/documents/listing.css index bdb2e88..285586e 100644 --- a/frontend/src/styles/documents/listing.css +++ b/frontend/src/styles/documents/listing.css @@ -254,8 +254,8 @@ text-align: left; } -.documents-panel--view-grid { - padding: 0.35rem 0.5rem 1rem; +.documents-panel .documents-grid { + padding: 1rem 0.5rem; } .documents-panel th.thumb-column,