/** * SourceTree - left sidebar directory tree for DMS. * Replaces FolderTree with a "sources" concept: * ▼ Meine Dateien (local folders) * ▼ Geteilte Ordner (shared files from other users) * ▼ Externe Speicher (placeholder for future plugins) * * Folders are drop targets (accept files) and draggable (for reparenting). */ import React, { useState } from 'react'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import type { DmsFolder, DmsFile } from '@/api/dms'; interface SourceTreeFolderItemProps { folder: DmsFolder; level: number; selectedFolderId: string | null; onSelect: (folderId: string) => void; onDropFiles: (fileIds: string[], targetFolderId: string) => void; onDropFolder: (sourceFolderId: string, targetFolderId: string) => void; } function SourceTreeFolderItem({ folder, level, selectedFolderId, onSelect, onDropFiles, onDropFolder, }: SourceTreeFolderItemProps) { const { t } = useTranslation(); const [expanded, setExpanded] = useState(true); const [isDropTarget, setIsDropTarget] = useState(false); const hasChildren = folder.children && folder.children.length > 0; const isSelected = selectedFolderId === folder.id; function handleDragOver(e: React.DragEvent) { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'move'; if (!isDropTarget) setIsDropTarget(true); } function handleDragLeave(e: React.DragEvent) { e.stopPropagation(); setIsDropTarget(false); } function handleDrop(e: React.DragEvent) { e.preventDefault(); e.stopPropagation(); setIsDropTarget(false); try { const data = JSON.parse(e.dataTransfer.getData('application/json')); if (data.type === 'files' && data.ids) { onDropFiles(data.ids, folder.id); } else if (data.type === 'folder' && data.id) { if (data.id !== folder.id) { onDropFolder(data.id, folder.id); } } } catch { // ignore invalid drop data } } function handleFolderDragStart(e: React.DragEvent) { e.stopPropagation(); e.dataTransfer.setData('application/json', JSON.stringify({ type: 'folder', id: folder.id })); e.dataTransfer.effectAllowed = 'move'; } return (
  • onSelect(folder.id)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(folder.id); } }} tabIndex={0} role="button" aria-label={folder.name} draggable onDragStart={handleFolderDragStart} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} > {hasChildren && ( )} {!hasChildren &&
    {hasChildren && expanded && ( )}
  • ); } interface SourceSectionProps { title: string; icon: React.ReactNode; selected: boolean; onClick: () => void; children?: React.ReactNode; defaultExpanded?: boolean; } function SourceSection({ title, icon, selected, onClick, children, defaultExpanded = true }: SourceSectionProps) { const [expanded, setExpanded] = useState(defaultExpanded); return (
    { onClick(); setExpanded(true); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(); setExpanded(true); } }} tabIndex={0} role="button" aria-label={title} > {icon} {title}
    {expanded && children && ( )}
    ); } export interface SourceTreeProps { folders: DmsFolder[]; sharedFiles: DmsFile[]; selectedFolderId: string | null; selectedSource: string | null; onSelectFolder: (folderId: string | null) => void; onSelectSource: (source: string) => void; loading?: boolean; onDropFiles: (fileIds: string[], targetFolderId: string | null) => void; onDropFolder: (sourceFolderId: string, targetFolderId: string | null) => void; } export function SourceTree({ folders, sharedFiles, selectedFolderId, selectedSource, onSelectFolder, onSelectSource, loading = false, onDropFiles, onDropFolder, }: SourceTreeProps) { const { t } = useTranslation(); const [isRootDropTarget, setIsRootDropTarget] = useState(false); if (loading) { return (
    {[1, 2, 3].map((i) => (
    ))}
    ); } const localSelected = selectedSource === 'local' || (selectedSource === null && selectedFolderId !== null); const sharedSelected = selectedSource === 'shared'; const externalSelected = selectedSource === 'external'; function handleRootDragOver(e: React.DragEvent) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (!isRootDropTarget) setIsRootDropTarget(true); } function handleRootDragLeave(e: React.DragEvent) { e.stopPropagation(); setIsRootDropTarget(false); } function handleRootDrop(e: React.DragEvent) { e.preventDefault(); setIsRootDropTarget(false); try { const data = JSON.parse(e.dataTransfer.getData('application/json')); if (data.type === 'files' && data.ids) { onDropFiles(data.ids, null); } else if (data.type === 'folder' && data.id) { onDropFolder(data.id, null); } } catch { // ignore invalid drop data } } return ( ); }