diff --git a/frontend/src/components/contacts/ContactFolderTree.tsx b/frontend/src/components/contacts/ContactFolderTree.tsx index 4f769e6..cce55ff 100644 --- a/frontend/src/components/contacts/ContactFolderTree.tsx +++ b/frontend/src/components/contacts/ContactFolderTree.tsx @@ -6,6 +6,7 @@ import { useCreateContactFolder, useUpdateContactFolder, useDeleteContactFolder, + useMoveContactToFolder, } from '@/api/hooks'; import { buildFolderTree, type ContactFolderTreeNode } from '@/api/contactFolders'; import { ChevronRight, Folder, MoreVertical, Palette, Pencil, Pin, Plus, Tag, Trash2, Users } from 'lucide-react'; @@ -81,7 +82,6 @@ function FolderDropdown({ { label: 'L\u00f6schen', icon: Trash2, action: () => { onDelete(state.folderId); onClose(); }, danger: true }, ]; - // Position dropdown below the button, aligned right const top = state.anchorRect.bottom + 4; const left = Math.max(4, state.anchorRect.right - 160); @@ -116,12 +116,20 @@ function FolderTreeItem({ selectedFilter, onSelectFolder, onMoreClick, + isDragOver, + onDragOver, + onDragLeave, + onDrop, }: { node: ContactFolderTreeNode; depth: number; selectedFilter: ContactFilter; onSelectFolder: (folderId: string) => void; onMoreClick: (e: React.MouseEvent, folderId: string) => void; + isDragOver: boolean; + onDragOver: (e: React.DragEvent, folderId: string) => void; + onDragLeave: (folderId: string) => void; + onDrop: (e: React.DragEvent, folderId: string) => void; }) { const [expanded, setExpanded] = useState(true); const folderKey = `folder:${node.id}` as ContactFilter; @@ -130,10 +138,15 @@ function FolderTreeItem({ return (
onDragOver(e, node.id)} + onDragLeave={() => onDragLeave(node.id)} + onDrop={(e) => onDrop(e, node.id)} className={clsx( 'group flex items-center gap-1.5 px-2 py-1.5 text-sm font-medium rounded-md cursor-pointer min-h-touch', 'transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500', - isActive + isDragOver + ? 'bg-primary-100 ring-2 ring-primary-400' + : isActive ? 'bg-primary-50 text-primary-700' : 'text-secondary-700 hover:bg-secondary-100', )} @@ -167,6 +180,10 @@ function FolderTreeItem({ selectedFilter={selectedFilter} onSelectFolder={onSelectFolder} onMoreClick={onMoreClick} + isDragOver={false} + onDragOver={onDragOver} + onDragLeave={onDragLeave} + onDrop={onDrop} /> ))}
@@ -185,11 +202,13 @@ export function ContactFolderTree({ const { t } = useTranslation(); const [tagsOpen, setTagsOpen] = useState(true); const [dropdown, setDropdown] = useState(null); + const [dragOverFolderId, setDragOverFolderId] = useState(null); const { data: folders, isLoading: foldersLoading } = useContactFolders(); const createFolderMut = useCreateContactFolder(); const updateFolderMut = useUpdateContactFolder(); const deleteFolderMut = useDeleteContactFolder(); + const moveContactMut = useMoveContactToFolder(); const folderList = folders ?? []; const tree = buildFolderTree(folderList); @@ -238,6 +257,37 @@ export function ContactFolderTree({ updateFolderMut.mutate({ id, data: { pinned: !pinned } as any }); }; + // Drag and Drop handlers + const handleDragOver = (e: React.DragEvent, folderId: string) => { + e.preventDefault(); + e.stopPropagation(); + setDragOverFolderId(folderId); + }; + + const handleDragLeave = (folderId: string) => { + if (dragOverFolderId === folderId) setDragOverFolderId(null); + }; + + const handleDrop = (e: React.DragEvent, folderId: string) => { + e.preventDefault(); + e.stopPropagation(); + setDragOverFolderId(null); + const contactId = e.dataTransfer.getData('text/plain'); + if (!contactId) return; + moveContactMut.mutate({ contactId, folderId }); + }; + + const handleRootDrop = (e: React.DragEvent) => { + e.preventDefault(); + const contactId = e.dataTransfer.getData('text/plain'); + if (!contactId) return; + moveContactMut.mutate({ contactId, folderId: null }); + }; + + const handleRootDragOver = (e: React.DragEvent) => { + e.preventDefault(); + }; + const handleSelectFolder = useCallback((folderId: string) => { onSelect(`folder:${folderId}` as ContactFilter); }, [onSelect]); @@ -267,8 +317,11 @@ export function ContactFolderTree({
- {/* Folder tree */} -
+ {/* Folder tree with drag-drop */} +
{tree.map((node) => ( ))} + {/* Root drop zone */} +
+ {(foldersLoading || loading) && (
{t('common.loading')}
)}