fix: dashboard tree drag&drop on folder items + collapsible + subfolders + context menu
This commit is contained in:
+164
-108
@@ -5,8 +5,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
|
|||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { NotificationPanel } from '../components/NotificationPanel';
|
import { NotificationPanel } from '../components/NotificationPanel';
|
||||||
import { ShareDialog } from '../components/ShareDialog';
|
import { ShareDialog } from '../components/ShareDialog';
|
||||||
import TreeView from '../components/TreeView';
|
|
||||||
import type { TreeNode } from '../types/ui.types';
|
|
||||||
import {
|
import {
|
||||||
getProjects, deleteProject, updateProject,
|
getProjects, deleteProject, updateProject,
|
||||||
getProjectFolders, createProjectFolder, renameProjectFolder, deleteProjectFolder,
|
getProjectFolders, createProjectFolder, renameProjectFolder, deleteProjectFolder,
|
||||||
@@ -15,7 +13,7 @@ import {
|
|||||||
} from '../services/api';
|
} from '../services/api';
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE || '';
|
const API_BASE = import.meta.env.VITE_API_BASE || '';
|
||||||
const ROOT_FOLDER_ID = '__root__';
|
const UNFILED_ID = '__unfiled__';
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
onOpenProject: (projectId: string) => void;
|
onOpenProject: (projectId: string) => void;
|
||||||
@@ -31,7 +29,7 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
|||||||
const [newName, setNewName] = useState('');
|
const [newName, setNewName] = useState('');
|
||||||
const [newDesc, setNewDesc] = useState('');
|
const [newDesc, setNewDesc] = useState('');
|
||||||
const [shareProjectId, setShareProjectId] = useState<string | null>(null);
|
const [shareProjectId, setShareProjectId] = useState<string | null>(null);
|
||||||
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(ROOT_FOLDER_ID);
|
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
||||||
const [shares, setShares] = useState<ProjectShare[]>([]);
|
const [shares, setShares] = useState<ProjectShare[]>([]);
|
||||||
const [sharesLoading, setSharesLoading] = useState(false);
|
const [sharesLoading, setSharesLoading] = useState(false);
|
||||||
@@ -46,6 +44,7 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
|||||||
const [editingProjectDesc, setEditingProjectDesc] = useState('');
|
const [editingProjectDesc, setEditingProjectDesc] = useState('');
|
||||||
const [savingProject, setSavingProject] = useState(false);
|
const [savingProject, setSavingProject] = useState(false);
|
||||||
const [dragOverFolderId, setDragOverFolderId] = useState<string | null>(null);
|
const [dragOverFolderId, setDragOverFolderId] = useState<string | null>(null);
|
||||||
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const fetchAll = useCallback(async () => {
|
const fetchAll = useCallback(async () => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
@@ -68,10 +67,17 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
|||||||
fetchAll();
|
fetchAll();
|
||||||
}, [fetchAll]);
|
}, [fetchAll]);
|
||||||
|
|
||||||
|
// Default: expand all folders on first load or when folders change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedFolderId === ROOT_FOLDER_ID) {
|
if (folders.length > 0 && expandedFolders.size === 0) {
|
||||||
|
setExpandedFolders(new Set(folders.map(f => f.id)));
|
||||||
|
}
|
||||||
|
}, [folders, expandedFolders.size]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedFolderId === null) {
|
||||||
setProjects(allProjects);
|
setProjects(allProjects);
|
||||||
} else if (selectedFolderId === null) {
|
} else if (selectedFolderId === UNFILED_ID) {
|
||||||
setProjects(allProjects.filter(p => p.folder_id === null));
|
setProjects(allProjects.filter(p => p.folder_id === null));
|
||||||
} else {
|
} else {
|
||||||
setProjects(allProjects.filter(p => p.folder_id === selectedFolderId));
|
setProjects(allProjects.filter(p => p.folder_id === selectedFolderId));
|
||||||
@@ -102,45 +108,31 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
|||||||
}
|
}
|
||||||
}, [selectedProject]);
|
}, [selectedProject]);
|
||||||
|
|
||||||
const treeNodes = useMemo((): TreeNode[] => {
|
|
||||||
const rootProjectsCount = allProjects.filter(p => p.folder_id === null).length;
|
|
||||||
const root: TreeNode = {
|
|
||||||
id: ROOT_FOLDER_ID,
|
|
||||||
name: 'Alle Projekte',
|
|
||||||
count: allProjects.length,
|
|
||||||
expanded: true,
|
|
||||||
children: [
|
|
||||||
{ id: '__unfiled__', name: 'Ohne Ordner', count: rootProjectsCount },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const buildFolderNode = (folder: ProjectFolder): TreeNode => {
|
|
||||||
const childFolders = folders.filter(f => f.parent_id === folder.id);
|
|
||||||
const projCount = allProjects.filter(p => p.folder_id === folder.id).length;
|
|
||||||
return {
|
|
||||||
id: folder.id,
|
|
||||||
name: folder.name,
|
|
||||||
count: projCount,
|
|
||||||
expanded: true,
|
|
||||||
children: childFolders.map(buildFolderNode),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const topFolders = folders.filter(f => f.parent_id === null);
|
|
||||||
root.children!.push(...topFolders.map(buildFolderNode));
|
|
||||||
return [root];
|
|
||||||
}, [folders, allProjects]);
|
|
||||||
|
|
||||||
const folderNameById = useMemo(() => {
|
const folderNameById = useMemo(() => {
|
||||||
const map = new Map<string, string>();
|
const map = new Map<string, string>();
|
||||||
map.set(ROOT_FOLDER_ID, 'Alle Projekte');
|
map.set(UNFILED_ID, 'Ohne Ordner');
|
||||||
map.set('__unfiled__', 'Ohne Ordner');
|
|
||||||
for (const f of folders) map.set(f.id, f.name);
|
for (const f of folders) map.set(f.id, f.name);
|
||||||
return map;
|
return map;
|
||||||
}, [folders]);
|
}, [folders]);
|
||||||
|
|
||||||
|
const currentFolderName = useMemo(() => {
|
||||||
|
if (selectedFolderId === null) return 'Alle Projekte';
|
||||||
|
return folderNameById.get(selectedFolderId) ?? 'Projekte';
|
||||||
|
}, [selectedFolderId, folderNameById]);
|
||||||
|
|
||||||
|
const toggleFolder = (id: string) => {
|
||||||
|
setExpandedFolders(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
if (!newName.trim() || !token) return;
|
if (!newName.trim() || !token) return;
|
||||||
try {
|
try {
|
||||||
const folderId = selectedFolderId === ROOT_FOLDER_ID || selectedFolderId === '__unfiled__'
|
const folderId = selectedFolderId === null || selectedFolderId === UNFILED_ID
|
||||||
? null : selectedFolderId;
|
? null : selectedFolderId;
|
||||||
const res = await fetch(`${API_BASE}/api/projects`, {
|
const res = await fetch(`${API_BASE}/api/projects`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -171,20 +163,13 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFolderSelect = (id: string) => {
|
const handleFolderSelect = (id: string | null) => {
|
||||||
if (id === ROOT_FOLDER_ID) {
|
setSelectedFolderId(id);
|
||||||
setSelectedFolderId(ROOT_FOLDER_ID);
|
|
||||||
} else if (id === '__unfiled__') {
|
|
||||||
setSelectedFolderId(null);
|
|
||||||
} else {
|
|
||||||
setSelectedFolderId(id);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateFolder = async () => {
|
const handleCreateFolder = async () => {
|
||||||
if (!newFolderName.trim() || !token) return;
|
if (!newFolderName.trim() || !token) return;
|
||||||
const parentId = newFolderParent === ROOT_FOLDER_ID || newFolderParent === '__unfiled__'
|
const parentId = newFolderParent === UNFILED_ID ? null : newFolderParent;
|
||||||
? null : newFolderParent;
|
|
||||||
try {
|
try {
|
||||||
await createProjectFolder(token, newFolderName, parentId);
|
await createProjectFolder(token, newFolderName, parentId);
|
||||||
setNewFolderName('');
|
setNewFolderName('');
|
||||||
@@ -213,7 +198,7 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
|||||||
if (!confirm('Ordner loeschen? Projekte darin werden nach "Ohne Ordner" verschoben.')) return;
|
if (!confirm('Ordner loeschen? Projekte darin werden nach "Ohne Ordner" verschoben.')) return;
|
||||||
try {
|
try {
|
||||||
await deleteProjectFolder(token, id);
|
await deleteProjectFolder(token, id);
|
||||||
if (selectedFolderId === id) setSelectedFolderId(ROOT_FOLDER_ID);
|
if (selectedFolderId === id) setSelectedFolderId(null);
|
||||||
fetchAll();
|
fetchAll();
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
@@ -247,25 +232,24 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
|||||||
e.dataTransfer.setData('text/plain', `project:${projectId}`);
|
e.dataTransfer.setData('text/plain', `project:${projectId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFolderDragOver = (e: React.DragEvent, folderId: string) => {
|
const handleFolderDragOver = (e: React.DragEvent, folderId: string | null) => {
|
||||||
if (!draggedProjectId) return;
|
if (!draggedProjectId) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.dataTransfer.dropEffect = 'move';
|
e.dataTransfer.dropEffect = 'move';
|
||||||
setDragOverFolderId(folderId);
|
setDragOverFolderId(folderId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFolderDragLeave = (_e: React.DragEvent, folderId: string) => {
|
const handleFolderDragLeave = (folderId: string | null) => {
|
||||||
if (dragOverFolderId === folderId) {
|
if (dragOverFolderId === folderId) {
|
||||||
setDragOverFolderId(null);
|
setDragOverFolderId(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFolderDrop = async (e: React.DragEvent, folderId: string) => {
|
const handleFolderDrop = async (e: React.DragEvent, folderId: string | null) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
(e.currentTarget as HTMLElement).classList.remove('tree-drop-target');
|
|
||||||
if (!draggedProjectId || !token) return;
|
if (!draggedProjectId || !token) return;
|
||||||
const targetFolderId = folderId === ROOT_FOLDER_ID || folderId === '__unfiled__' ? null : folderId;
|
const targetFolderId = folderId === UNFILED_ID ? null : folderId;
|
||||||
try {
|
try {
|
||||||
await moveProjectToFolder(token, draggedProjectId, targetFolderId);
|
await moveProjectToFolder(token, draggedProjectId, targetFolderId);
|
||||||
fetchAll();
|
fetchAll();
|
||||||
@@ -288,6 +272,9 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
|||||||
if (action === 'new') {
|
if (action === 'new') {
|
||||||
setNewFolderParent(folderId);
|
setNewFolderParent(folderId);
|
||||||
setShowCreateFolder(true);
|
setShowCreateFolder(true);
|
||||||
|
} else if (action === 'newSubfolder' && folderId) {
|
||||||
|
setNewFolderParent(folderId);
|
||||||
|
setShowCreateFolder(true);
|
||||||
} else if (action === 'rename' && folderId) {
|
} else if (action === 'rename' && folderId) {
|
||||||
const folder = folders.find(f => f.id === folderId);
|
const folder = folders.find(f => f.id === folderId);
|
||||||
if (folder) {
|
if (folder) {
|
||||||
@@ -306,29 +293,89 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFolderActions = (node: TreeNode) => {
|
const folderIcon = (
|
||||||
if (node.id === ROOT_FOLDER_ID || node.id === '__unfiled__') return null;
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderFolderItem = (folder: ProjectFolder, depth: number): React.ReactNode => {
|
||||||
|
const children = folders.filter(f => f.parent_id === folder.id);
|
||||||
|
const isExpanded = expandedFolders.has(folder.id);
|
||||||
|
const projectCount = allProjects.filter(p => p.folder_id === folder.id).length;
|
||||||
|
const isSelected = selectedFolderId === folder.id;
|
||||||
|
const isDragOver = dragOverFolderId === folder.id;
|
||||||
|
const isRenaming = renamingFolderId === folder.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="folder-action-buttons">
|
<div key={folder.id}>
|
||||||
<button
|
<div
|
||||||
className="folder-action-btn"
|
className={`folder-tree-item${isSelected ? ' selected' : ''}${isDragOver ? ' drag-over' : ''}`}
|
||||||
title="Umbenennen"
|
draggable
|
||||||
onClick={(e) => {
|
onDragOver={(e) => handleFolderDragOver(e, folder.id)}
|
||||||
e.stopPropagation();
|
onDragLeave={() => handleFolderDragLeave(folder.id)}
|
||||||
setRenamingFolderId(node.id);
|
onDrop={(e) => handleFolderDrop(e, folder.id)}
|
||||||
const folder = folders.find(f => f.id === node.id);
|
onClick={() => handleFolderSelect(folder.id)}
|
||||||
if (folder) setRenameValue(folder.name);
|
onContextMenu={(e) => handleContextMenu(e, folder.id)}
|
||||||
}}
|
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||||
>✎</button>
|
>
|
||||||
<button
|
{children.length > 0 ? (
|
||||||
className="folder-action-btn delete"
|
<button
|
||||||
title="Loeschen"
|
className="folder-chevron"
|
||||||
onClick={(e) => {
|
onClick={(e) => { e.stopPropagation(); toggleFolder(folder.id); }}
|
||||||
e.stopPropagation();
|
aria-label={isExpanded ? 'Einklappen' : 'Ausklappen'}
|
||||||
handleDeleteFolder(node.id);
|
>
|
||||||
}}
|
{isExpanded ? '▼' : '▶'}
|
||||||
>✕</button>
|
</button>
|
||||||
</span>
|
) : (
|
||||||
|
<span className="folder-chevron-placeholder" />
|
||||||
|
)}
|
||||||
|
{folderIcon}
|
||||||
|
{isRenaming ? (
|
||||||
|
<input
|
||||||
|
className="folder-rename-inline"
|
||||||
|
type="text"
|
||||||
|
value={renameValue}
|
||||||
|
onChange={e => setRenameValue(e.target.value)}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter') handleRenameFolder(folder.id);
|
||||||
|
if (e.key === 'Escape') { setRenamingFolderId(null); setRenameValue(''); }
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (renameValue.trim()) handleRenameFolder(folder.id);
|
||||||
|
else { setRenamingFolderId(null); setRenameValue(''); }
|
||||||
|
}}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="folder-tree-name">{folder.name}</span>
|
||||||
|
)}
|
||||||
|
{projectCount > 0 && <span className="folder-count">({projectCount})</span>}
|
||||||
|
{!isRenaming && (
|
||||||
|
<span className="folder-action-buttons">
|
||||||
|
<button
|
||||||
|
className="folder-action-btn"
|
||||||
|
title="Umbenennen"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setRenamingFolderId(folder.id);
|
||||||
|
setRenameValue(folder.name);
|
||||||
|
}}
|
||||||
|
>✎</button>
|
||||||
|
<button
|
||||||
|
className="folder-action-btn delete"
|
||||||
|
title="Loeschen"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteFolder(folder.id);
|
||||||
|
}}
|
||||||
|
>✕</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isExpanded && children.map(child => renderFolderItem(child, depth + 1))}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -360,7 +407,7 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
|||||||
<div className="folder-create-form">
|
<div className="folder-create-form">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Ordnername"
|
placeholder={newFolderParent ? 'Unterordnername' : 'Ordnername'}
|
||||||
value={newFolderName}
|
value={newFolderName}
|
||||||
onChange={e => setNewFolderName(e.target.value)}
|
onChange={e => setNewFolderName(e.target.value)}
|
||||||
onKeyDown={e => { if (e.key === 'Enter') handleCreateFolder(); if (e.key === 'Escape') setShowCreateFolder(false); }}
|
onKeyDown={e => { if (e.key === 'Enter') handleCreateFolder(); if (e.key === 'Escape') setShowCreateFolder(false); }}
|
||||||
@@ -368,47 +415,50 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
|||||||
/>
|
/>
|
||||||
<div className="folder-create-actions">
|
<div className="folder-create-actions">
|
||||||
<button onClick={handleCreateFolder}>OK</button>
|
<button onClick={handleCreateFolder}>OK</button>
|
||||||
<button onClick={() => { setShowCreateFolder(false); setNewFolderName(''); }}>×</button>
|
<button onClick={() => { setShowCreateFolder(false); setNewFolderName(''); setNewFolderParent(null); }}>×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TreeView
|
{/* Root: Alle Projekte */}
|
||||||
nodes={treeNodes}
|
<div
|
||||||
selectedId={selectedFolderId ?? undefined}
|
className={`folder-tree-item${selectedFolderId === null ? ' selected' : ''}${dragOverFolderId === null && draggedProjectId ? ' drag-over' : ''}`}
|
||||||
onSelect={handleFolderSelect}
|
onClick={() => handleFolderSelect(null)}
|
||||||
renderActions={renderFolderActions}
|
onDragOver={(e) => handleFolderDragOver(e, null)}
|
||||||
/>
|
onDragLeave={() => handleFolderDragLeave(null)}
|
||||||
|
onDrop={(e) => handleFolderDrop(e, null)}
|
||||||
{renamingFolderId && (
|
onContextMenu={(e) => handleContextMenu(e, null)}
|
||||||
<div className="folder-rename-form">
|
style={{ paddingLeft: '8px' }}
|
||||||
<input
|
>
|
||||||
type="text"
|
<span className="folder-chevron-placeholder" />
|
||||||
value={renameValue}
|
{folderIcon}
|
||||||
onChange={e => setRenameValue(e.target.value)}
|
<span className="folder-tree-name">Alle Projekte</span>
|
||||||
onKeyDown={e => { if (e.key === 'Enter') handleRenameFolder(renamingFolderId); if (e.key === 'Escape') { setRenamingFolderId(null); setRenameValue(''); } }}
|
<span className="folder-count">({allProjects.length})</span>
|
||||||
onBlur={() => { if (renameValue.trim()) handleRenameFolder(renamingFolderId); else { setRenamingFolderId(null); setRenameValue(''); } }}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="treeview-drop-overlay">
|
|
||||||
{folders.map(folder => (
|
|
||||||
<div
|
|
||||||
key={folder.id}
|
|
||||||
className={`treeview-drop-zone${dragOverFolderId === folder.id ? ' drag-over' : ''}`}
|
|
||||||
onDragOver={(e) => handleFolderDragOver(e, folder.id)}
|
|
||||||
onDragLeave={(e) => handleFolderDragLeave(e, folder.id)}
|
|
||||||
onDrop={(e) => handleFolderDrop(e, folder.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Ohne Ordner node */}
|
||||||
|
<div
|
||||||
|
className={`folder-tree-item${selectedFolderId === UNFILED_ID ? ' selected' : ''}${dragOverFolderId === UNFILED_ID ? ' drag-over' : ''}`}
|
||||||
|
onClick={() => handleFolderSelect(UNFILED_ID)}
|
||||||
|
onDragOver={(e) => handleFolderDragOver(e, UNFILED_ID)}
|
||||||
|
onDragLeave={() => handleFolderDragLeave(UNFILED_ID)}
|
||||||
|
onDrop={(e) => handleFolderDrop(e, UNFILED_ID)}
|
||||||
|
onContextMenu={(e) => handleContextMenu(e, null)}
|
||||||
|
style={{ paddingLeft: '24px' }}
|
||||||
|
>
|
||||||
|
<span className="folder-chevron-placeholder" />
|
||||||
|
{folderIcon}
|
||||||
|
<span className="folder-tree-name">Ohne Ordner</span>
|
||||||
|
<span className="folder-count">({allProjects.filter(p => p.folder_id === null).length})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top-level folders rendered recursively */}
|
||||||
|
{folders.filter(f => !f.parent_id).map(folder => renderFolderItem(folder, 1))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="dashboard-content">
|
<div className="dashboard-content">
|
||||||
<div className="dashboard-section-header">
|
<div className="dashboard-section-header">
|
||||||
<h2>{folderNameById.get(selectedFolderId ?? ROOT_FOLDER_ID) ?? 'Projekte'}</h2>
|
<h2>{currentFolderName}</h2>
|
||||||
<button className="dashboard-create-btn" onClick={() => setShowCreate(!showCreate)}>
|
<button className="dashboard-create-btn" onClick={() => setShowCreate(!showCreate)}>
|
||||||
+ Neues Projekt
|
+ Neues Projekt
|
||||||
</button>
|
</button>
|
||||||
@@ -561,7 +611,13 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
|
|||||||
className="context-menu-item"
|
className="context-menu-item"
|
||||||
onClick={() => handleContextAction('new', contextMenu.folderId)}
|
onClick={() => handleContextAction('new', contextMenu.folderId)}
|
||||||
>Neuer Ordner</button>
|
>Neuer Ordner</button>
|
||||||
{contextMenu.folderId && contextMenu.folderId !== ROOT_FOLDER_ID && contextMenu.folderId !== '__unfiled__' && (
|
{contextMenu.folderId && (
|
||||||
|
<button
|
||||||
|
className="context-menu-item"
|
||||||
|
onClick={() => handleContextAction('newSubfolder', contextMenu.folderId)}
|
||||||
|
>Neuer Unterordner</button>
|
||||||
|
)}
|
||||||
|
{contextMenu.folderId && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className="context-menu-item"
|
className="context-menu-item"
|
||||||
|
|||||||
@@ -282,18 +282,90 @@
|
|||||||
.folder-action-btn:hover { color: var(--color-primary); background: rgba(37, 99, 235, 0.1); }
|
.folder-action-btn:hover { color: var(--color-primary); background: rgba(37, 99, 235, 0.1); }
|
||||||
.folder-action-btn.delete:hover { color: #e74c3c; background: rgba(231, 76, 60, 0.1); }
|
.folder-action-btn.delete:hover { color: #e74c3c; background: rgba(231, 76, 60, 0.1); }
|
||||||
|
|
||||||
/* Drag-over visual feedback on tree nodes */
|
/* Folder tree items (recursive, native drag targets) */
|
||||||
.tree-node.drag-over {
|
.folder-tree-item {
|
||||||
background: var(--color-primary) !important;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text);
|
||||||
|
transition: background 0.15s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-tree-item:hover {
|
||||||
|
background: var(--color-surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-tree-item.selected {
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-tree-item.drag-over {
|
||||||
|
background: var(--color-primary);
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
border: 2px dashed var(--color-primary);
|
border: 2px dashed var(--color-primary);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.treeview-drop-zone.drag-over {
|
.folder-chevron {
|
||||||
background: rgba(37, 99, 235, 0.15);
|
background: none;
|
||||||
outline: 2px dashed var(--color-primary);
|
border: none;
|
||||||
border-radius: 4px;
|
cursor: pointer;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
padding: 0;
|
||||||
|
width: 16px;
|
||||||
|
font-size: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-chevron:hover {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-tree-item.selected .folder-chevron {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-chevron-placeholder {
|
||||||
|
width: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-tree-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-count {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-tree-item.selected .folder-count {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-rename-inline {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-menu-overlay {
|
.context-menu-overlay {
|
||||||
|
|||||||
Reference in New Issue
Block a user