/**
* 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 &&
}
{folder.name}
{typeof folder.file_count === 'number' && folder.file_count > 0 && (
{folder.file_count}
)}
{hasChildren && expanded && (
{folder.children!.map((child) => (
))}
)}
);
}
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 (
);
}