2026-07-20 17:31:04 +02:00
|
|
|
/**
|
|
|
|
|
* 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)
|
2026-07-20 22:33:51 +00:00
|
|
|
*
|
|
|
|
|
* Folders are drop targets (accept files) and draggable (for reparenting).
|
2026-07-20 17:31:04 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
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;
|
2026-07-20 22:33:51 +00:00
|
|
|
onDropFiles: (fileIds: string[], targetFolderId: string) => void;
|
|
|
|
|
onDropFolder: (sourceFolderId: string, targetFolderId: string) => void;
|
2026-07-20 17:31:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SourceTreeFolderItem({
|
|
|
|
|
folder,
|
|
|
|
|
level,
|
|
|
|
|
selectedFolderId,
|
|
|
|
|
onSelect,
|
2026-07-20 22:33:51 +00:00
|
|
|
onDropFiles,
|
|
|
|
|
onDropFolder,
|
2026-07-20 17:31:04 +02:00
|
|
|
}: SourceTreeFolderItemProps) {
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const [expanded, setExpanded] = useState(true);
|
2026-07-20 22:33:51 +00:00
|
|
|
const [isDropTarget, setIsDropTarget] = useState(false);
|
2026-07-20 17:31:04 +02:00
|
|
|
const hasChildren = folder.children && folder.children.length > 0;
|
|
|
|
|
const isSelected = selectedFolderId === folder.id;
|
|
|
|
|
|
2026-07-20 22:33:51 +00:00
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 17:31:04 +02:00
|
|
|
return (
|
|
|
|
|
<li role="treeitem" aria-expanded={hasChildren ? expanded : undefined} aria-selected={isSelected}>
|
|
|
|
|
<div
|
|
|
|
|
className={clsx(
|
2026-07-20 22:33:51 +00:00
|
|
|
'flex items-center gap-1 px-2 py-1.5 rounded-md text-sm cursor-pointer',
|
2026-07-20 17:31:04 +02:00
|
|
|
'hover:bg-secondary-100 motion-safe:transition-colors',
|
|
|
|
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
|
|
|
isSelected && 'bg-primary-50 text-primary-700 font-medium',
|
2026-07-20 22:33:51 +00:00
|
|
|
isDropTarget && 'ring-2 ring-primary-400 bg-primary-50',
|
2026-07-20 17:31:04 +02:00
|
|
|
)}
|
|
|
|
|
style={{ paddingLeft: `${level * 12 + 8}px` }}
|
|
|
|
|
onClick={() => onSelect(folder.id)}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
onSelect(folder.id);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
role="button"
|
|
|
|
|
aria-label={folder.name}
|
2026-07-20 22:33:51 +00:00
|
|
|
draggable
|
|
|
|
|
onDragStart={handleFolderDragStart}
|
|
|
|
|
onDragOver={handleDragOver}
|
|
|
|
|
onDragLeave={handleDragLeave}
|
|
|
|
|
onDrop={handleDrop}
|
2026-07-20 17:31:04 +02:00
|
|
|
>
|
|
|
|
|
{hasChildren && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setExpanded((prev) => !prev);
|
|
|
|
|
}}
|
|
|
|
|
className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600"
|
|
|
|
|
aria-label={expanded ? t('dms.folders') : t('dms.folders')}
|
|
|
|
|
>
|
|
|
|
|
<svg className="w-3 h-3 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{!hasChildren && <span className="w-4 flex-shrink-0" aria-hidden="true" />}
|
2026-07-20 22:33:51 +00:00
|
|
|
<svg className={clsx('w-4 h-4 flex-shrink-0', isDropTarget ? 'text-primary-500' : 'text-warning-500')} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
2026-07-20 17:31:04 +02:00
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span className="truncate">{folder.name}</span>
|
|
|
|
|
{typeof folder.file_count === 'number' && folder.file_count > 0 && (
|
|
|
|
|
<span className="ml-auto text-xs text-secondary-400">{folder.file_count}</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{hasChildren && expanded && (
|
2026-07-20 22:33:51 +00:00
|
|
|
<ul role="group" className="space-y-0">
|
2026-07-20 17:31:04 +02:00
|
|
|
{folder.children!.map((child) => (
|
|
|
|
|
<SourceTreeFolderItem
|
|
|
|
|
key={child.id}
|
|
|
|
|
folder={child}
|
|
|
|
|
level={level + 1}
|
|
|
|
|
selectedFolderId={selectedFolderId}
|
|
|
|
|
onSelect={onSelect}
|
2026-07-20 22:33:51 +00:00
|
|
|
onDropFiles={onDropFiles}
|
|
|
|
|
onDropFolder={onDropFolder}
|
2026-07-20 17:31:04 +02:00
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 (
|
|
|
|
|
<div className="mb-1">
|
|
|
|
|
<div
|
|
|
|
|
className={clsx(
|
2026-07-20 22:33:51 +00:00
|
|
|
'flex items-center gap-1 px-2 py-1.5 rounded-md text-sm font-semibold cursor-pointer',
|
2026-07-20 17:31:04 +02:00
|
|
|
'hover:bg-secondary-100 motion-safe:transition-colors',
|
|
|
|
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
|
|
|
selected && 'bg-primary-50 text-primary-700',
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onClick();
|
|
|
|
|
setExpanded(true);
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
onClick();
|
|
|
|
|
setExpanded(true);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
role="button"
|
|
|
|
|
aria-label={title}
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setExpanded((prev) => !prev);
|
|
|
|
|
}}
|
|
|
|
|
className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600"
|
|
|
|
|
aria-label={expanded ? 'Collapse' : 'Expand'}
|
|
|
|
|
>
|
|
|
|
|
<svg className="w-3 h-3 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
{icon}
|
|
|
|
|
<span className="truncate">{title}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{expanded && children && (
|
2026-07-20 22:33:51 +00:00
|
|
|
<ul role="group" className="space-y-0 mt-0.5">
|
2026-07-20 17:31:04 +02:00
|
|
|
{children}
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SourceTreeProps {
|
|
|
|
|
folders: DmsFolder[];
|
|
|
|
|
sharedFiles: DmsFile[];
|
|
|
|
|
selectedFolderId: string | null;
|
|
|
|
|
selectedSource: string | null;
|
|
|
|
|
onSelectFolder: (folderId: string | null) => void;
|
|
|
|
|
onSelectSource: (source: string) => void;
|
|
|
|
|
loading?: boolean;
|
2026-07-20 22:33:51 +00:00
|
|
|
onDropFiles: (fileIds: string[], targetFolderId: string | null) => void;
|
|
|
|
|
onDropFolder: (sourceFolderId: string, targetFolderId: string | null) => void;
|
2026-07-20 17:31:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function SourceTree({
|
|
|
|
|
folders,
|
|
|
|
|
sharedFiles,
|
|
|
|
|
selectedFolderId,
|
|
|
|
|
selectedSource,
|
|
|
|
|
onSelectFolder,
|
|
|
|
|
onSelectSource,
|
|
|
|
|
loading = false,
|
2026-07-20 22:33:51 +00:00
|
|
|
onDropFiles,
|
|
|
|
|
onDropFolder,
|
2026-07-20 17:31:04 +02:00
|
|
|
}: SourceTreeProps) {
|
|
|
|
|
const { t } = useTranslation();
|
2026-07-20 22:33:51 +00:00
|
|
|
const [isRootDropTarget, setIsRootDropTarget] = useState(false);
|
2026-07-20 17:31:04 +02:00
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-2 p-2" data-testid="source-tree-loading">
|
|
|
|
|
{[1, 2, 3].map((i) => (
|
|
|
|
|
<div key={i} className="h-8 bg-secondary-100 rounded animate-pulse" />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const localSelected = selectedSource === 'local' || (selectedSource === null && selectedFolderId !== null);
|
|
|
|
|
const sharedSelected = selectedSource === 'shared';
|
|
|
|
|
const externalSelected = selectedSource === 'external';
|
|
|
|
|
|
2026-07-20 22:33:51 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 17:31:04 +02:00
|
|
|
return (
|
|
|
|
|
<nav aria-label={t('dms.sources')} data-testid="source-tree" className="p-2">
|
2026-07-20 22:33:51 +00:00
|
|
|
{/* All Files entry — also a drop target (move to root) */}
|
2026-07-20 17:31:04 +02:00
|
|
|
<div
|
|
|
|
|
className={clsx(
|
2026-07-20 22:33:51 +00:00
|
|
|
'flex items-center gap-2 px-2 py-1.5 rounded-md text-sm cursor-pointer mb-1',
|
2026-07-20 17:31:04 +02:00
|
|
|
'hover:bg-secondary-100 motion-safe:transition-colors',
|
|
|
|
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
|
|
|
selectedSource === null && selectedFolderId === null && 'bg-primary-50 text-primary-700 font-medium',
|
2026-07-20 22:33:51 +00:00
|
|
|
isRootDropTarget && 'ring-2 ring-primary-400 bg-primary-50',
|
2026-07-20 17:31:04 +02:00
|
|
|
)}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onSelectFolder(null);
|
|
|
|
|
onSelectSource('local');
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
onSelectFolder(null);
|
|
|
|
|
onSelectSource('local');
|
|
|
|
|
}
|
|
|
|
|
}}
|
2026-07-20 22:33:51 +00:00
|
|
|
onDragOver={handleRootDragOver}
|
|
|
|
|
onDragLeave={handleRootDragLeave}
|
|
|
|
|
onDrop={handleRootDrop}
|
2026-07-20 17:31:04 +02:00
|
|
|
tabIndex={0}
|
|
|
|
|
role="button"
|
|
|
|
|
aria-label={t('dms.allFiles')}
|
|
|
|
|
>
|
|
|
|
|
<svg className="w-4 h-4 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span>{t('dms.allFiles')}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Meine Dateien section */}
|
|
|
|
|
<SourceSection
|
|
|
|
|
title={t('dms.myFiles')}
|
|
|
|
|
selected={localSelected}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onSelectSource('local');
|
|
|
|
|
if (!selectedFolderId) {
|
|
|
|
|
onSelectFolder(null);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
icon={
|
|
|
|
|
<svg className="w-4 h-4 text-primary-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{folders.length === 0 ? (
|
|
|
|
|
<li className="px-2 py-1 text-xs text-secondary-400 italic">{t('dms.noFiles')}</li>
|
|
|
|
|
) : (
|
|
|
|
|
folders.map((folder) => (
|
|
|
|
|
<SourceTreeFolderItem
|
|
|
|
|
key={folder.id}
|
|
|
|
|
folder={folder}
|
|
|
|
|
level={0}
|
|
|
|
|
selectedFolderId={selectedFolderId}
|
|
|
|
|
onSelect={onSelectFolder}
|
2026-07-20 22:33:51 +00:00
|
|
|
onDropFiles={onDropFiles}
|
|
|
|
|
onDropFolder={onDropFolder}
|
2026-07-20 17:31:04 +02:00
|
|
|
/>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</SourceSection>
|
|
|
|
|
|
|
|
|
|
{/* Geteilte Ordner section */}
|
|
|
|
|
<SourceSection
|
|
|
|
|
title={t('dms.sharedFolders')}
|
|
|
|
|
selected={sharedSelected}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onSelectSource('shared');
|
|
|
|
|
onSelectFolder(null);
|
|
|
|
|
}}
|
|
|
|
|
icon={
|
|
|
|
|
<svg className="w-4 h-4 text-accent-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{sharedFiles.length === 0 ? (
|
|
|
|
|
<li className="px-2 py-1 text-xs text-secondary-400 italic">{t('dms.noFiles')}</li>
|
|
|
|
|
) : (
|
|
|
|
|
sharedFiles.map((file) => (
|
|
|
|
|
<li key={file.id} role="treeitem" aria-selected={false}>
|
|
|
|
|
<div
|
|
|
|
|
className={clsx(
|
2026-07-20 22:33:51 +00:00
|
|
|
'flex items-center gap-1 px-2 py-1.5 rounded-md text-sm cursor-pointer',
|
2026-07-20 17:31:04 +02:00
|
|
|
'hover:bg-secondary-100 motion-safe:transition-colors',
|
|
|
|
|
)}
|
|
|
|
|
style={{ paddingLeft: '20px' }}
|
|
|
|
|
onClick={() => onSelectSource('shared')}
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
role="button"
|
|
|
|
|
aria-label={file.name}
|
|
|
|
|
>
|
|
|
|
|
<svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span className="truncate">{file.name}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</SourceSection>
|
|
|
|
|
|
|
|
|
|
{/* Externe Speicher section */}
|
|
|
|
|
<SourceSection
|
|
|
|
|
title={t('dms.externalStorage')}
|
|
|
|
|
selected={externalSelected}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onSelectSource('external');
|
|
|
|
|
onSelectFolder(null);
|
|
|
|
|
}}
|
|
|
|
|
icon={
|
|
|
|
|
<svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12H3l9-9 9 9h-2M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7M9 21v-6h6v6" />
|
|
|
|
|
</svg>
|
|
|
|
|
}
|
|
|
|
|
defaultExpanded={false}
|
|
|
|
|
>
|
|
|
|
|
<li className="px-2 py-1 text-xs text-secondary-400 italic" style={{ paddingLeft: '20px' }}>
|
|
|
|
|
{t('dms.externalStorageHint')}
|
|
|
|
|
</li>
|
|
|
|
|
</SourceSection>
|
|
|
|
|
</nav>
|
|
|
|
|
);
|
|
|
|
|
}
|