From c8858430ad600d74c20365c697a813d745591d7f Mon Sep 17 00:00:00 2001 From: A0 Orchestrator Date: Fri, 3 Jul 2026 18:58:14 +0200 Subject: [PATCH] fix: dashboard tree drag&drop on folder items + collapsible + subfolders + context menu --- frontend/src/pages/Dashboard.tsx | 272 +++++++++++++++++++------------ frontend/src/styles/auth.css | 86 +++++++++- 2 files changed, 243 insertions(+), 115 deletions(-) diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index b2a570e..25255cf 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -5,8 +5,6 @@ import { useState, useEffect, useCallback, useMemo } from 'react'; import { useAuth } from '../contexts/AuthContext'; import { NotificationPanel } from '../components/NotificationPanel'; import { ShareDialog } from '../components/ShareDialog'; -import TreeView from '../components/TreeView'; -import type { TreeNode } from '../types/ui.types'; import { getProjects, deleteProject, updateProject, getProjectFolders, createProjectFolder, renameProjectFolder, deleteProjectFolder, @@ -15,7 +13,7 @@ import { } from '../services/api'; const API_BASE = import.meta.env.VITE_API_BASE || ''; -const ROOT_FOLDER_ID = '__root__'; +const UNFILED_ID = '__unfiled__'; interface DashboardProps { onOpenProject: (projectId: string) => void; @@ -31,7 +29,7 @@ export function Dashboard({ onOpenProject }: DashboardProps) { const [newName, setNewName] = useState(''); const [newDesc, setNewDesc] = useState(''); const [shareProjectId, setShareProjectId] = useState(null); - const [selectedFolderId, setSelectedFolderId] = useState(ROOT_FOLDER_ID); + const [selectedFolderId, setSelectedFolderId] = useState(null); const [selectedProjectId, setSelectedProjectId] = useState(null); const [shares, setShares] = useState([]); const [sharesLoading, setSharesLoading] = useState(false); @@ -46,6 +44,7 @@ export function Dashboard({ onOpenProject }: DashboardProps) { const [editingProjectDesc, setEditingProjectDesc] = useState(''); const [savingProject, setSavingProject] = useState(false); const [dragOverFolderId, setDragOverFolderId] = useState(null); + const [expandedFolders, setExpandedFolders] = useState>(new Set()); const fetchAll = useCallback(async () => { if (!token) return; @@ -68,10 +67,17 @@ export function Dashboard({ onOpenProject }: DashboardProps) { fetchAll(); }, [fetchAll]); + // Default: expand all folders on first load or when folders change 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); - } else if (selectedFolderId === null) { + } else if (selectedFolderId === UNFILED_ID) { setProjects(allProjects.filter(p => p.folder_id === null)); } else { setProjects(allProjects.filter(p => p.folder_id === selectedFolderId)); @@ -102,45 +108,31 @@ export function Dashboard({ onOpenProject }: DashboardProps) { } }, [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 map = new Map(); - map.set(ROOT_FOLDER_ID, 'Alle Projekte'); - map.set('__unfiled__', 'Ohne Ordner'); + map.set(UNFILED_ID, 'Ohne Ordner'); for (const f of folders) map.set(f.id, f.name); return map; }, [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 () => { if (!newName.trim() || !token) return; try { - const folderId = selectedFolderId === ROOT_FOLDER_ID || selectedFolderId === '__unfiled__' + const folderId = selectedFolderId === null || selectedFolderId === UNFILED_ID ? null : selectedFolderId; const res = await fetch(`${API_BASE}/api/projects`, { method: 'POST', @@ -171,20 +163,13 @@ export function Dashboard({ onOpenProject }: DashboardProps) { } }; - const handleFolderSelect = (id: string) => { - if (id === ROOT_FOLDER_ID) { - setSelectedFolderId(ROOT_FOLDER_ID); - } else if (id === '__unfiled__') { - setSelectedFolderId(null); - } else { - setSelectedFolderId(id); - } + const handleFolderSelect = (id: string | null) => { + setSelectedFolderId(id); }; const handleCreateFolder = async () => { if (!newFolderName.trim() || !token) return; - const parentId = newFolderParent === ROOT_FOLDER_ID || newFolderParent === '__unfiled__' - ? null : newFolderParent; + const parentId = newFolderParent === UNFILED_ID ? null : newFolderParent; try { await createProjectFolder(token, newFolderName, parentId); setNewFolderName(''); @@ -213,7 +198,7 @@ export function Dashboard({ onOpenProject }: DashboardProps) { if (!confirm('Ordner loeschen? Projekte darin werden nach "Ohne Ordner" verschoben.')) return; try { await deleteProjectFolder(token, id); - if (selectedFolderId === id) setSelectedFolderId(ROOT_FOLDER_ID); + if (selectedFolderId === id) setSelectedFolderId(null); fetchAll(); } catch { // ignore @@ -247,25 +232,24 @@ export function Dashboard({ onOpenProject }: DashboardProps) { 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; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOverFolderId(folderId); }; - const handleFolderDragLeave = (_e: React.DragEvent, folderId: string) => { + const handleFolderDragLeave = (folderId: string | null) => { if (dragOverFolderId === folderId) { setDragOverFolderId(null); } }; - const handleFolderDrop = async (e: React.DragEvent, folderId: string) => { + const handleFolderDrop = async (e: React.DragEvent, folderId: string | null) => { e.preventDefault(); e.stopPropagation(); - (e.currentTarget as HTMLElement).classList.remove('tree-drop-target'); if (!draggedProjectId || !token) return; - const targetFolderId = folderId === ROOT_FOLDER_ID || folderId === '__unfiled__' ? null : folderId; + const targetFolderId = folderId === UNFILED_ID ? null : folderId; try { await moveProjectToFolder(token, draggedProjectId, targetFolderId); fetchAll(); @@ -288,6 +272,9 @@ export function Dashboard({ onOpenProject }: DashboardProps) { if (action === 'new') { setNewFolderParent(folderId); setShowCreateFolder(true); + } else if (action === 'newSubfolder' && folderId) { + setNewFolderParent(folderId); + setShowCreateFolder(true); } else if (action === 'rename' && folderId) { const folder = folders.find(f => f.id === folderId); if (folder) { @@ -306,29 +293,89 @@ export function Dashboard({ onOpenProject }: DashboardProps) { }); }; - const renderFolderActions = (node: TreeNode) => { - if (node.id === ROOT_FOLDER_ID || node.id === '__unfiled__') return null; + const folderIcon = ( + + + + ); + + 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 ( - - - - +
+
handleFolderDragOver(e, folder.id)} + onDragLeave={() => handleFolderDragLeave(folder.id)} + onDrop={(e) => handleFolderDrop(e, folder.id)} + onClick={() => handleFolderSelect(folder.id)} + onContextMenu={(e) => handleContextMenu(e, folder.id)} + style={{ paddingLeft: `${depth * 16 + 8}px` }} + > + {children.length > 0 ? ( + + ) : ( + + )} + {folderIcon} + {isRenaming ? ( + 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 + /> + ) : ( + {folder.name} + )} + {projectCount > 0 && ({projectCount})} + {!isRenaming && ( + + + + + )} +
+ {isExpanded && children.map(child => renderFolderItem(child, depth + 1))} +
); }; @@ -360,7 +407,7 @@ export function Dashboard({ onOpenProject }: DashboardProps) {
setNewFolderName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleCreateFolder(); if (e.key === 'Escape') setShowCreateFolder(false); }} @@ -368,47 +415,50 @@ export function Dashboard({ onOpenProject }: DashboardProps) { />
- +
)} - - - {renamingFolderId && ( -
- setRenameValue(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleRenameFolder(renamingFolderId); if (e.key === 'Escape') { setRenamingFolderId(null); setRenameValue(''); } }} - onBlur={() => { if (renameValue.trim()) handleRenameFolder(renamingFolderId); else { setRenamingFolderId(null); setRenameValue(''); } }} - autoFocus - /> -
- )} - -
- {folders.map(folder => ( -
handleFolderDragOver(e, folder.id)} - onDragLeave={(e) => handleFolderDragLeave(e, folder.id)} - onDrop={(e) => handleFolderDrop(e, folder.id)} - /> - ))} + {/* Root: Alle Projekte */} +
handleFolderSelect(null)} + onDragOver={(e) => handleFolderDragOver(e, null)} + onDragLeave={() => handleFolderDragLeave(null)} + onDrop={(e) => handleFolderDrop(e, null)} + onContextMenu={(e) => handleContextMenu(e, null)} + style={{ paddingLeft: '8px' }} + > + + {folderIcon} + Alle Projekte + ({allProjects.length})
+ + {/* Ohne Ordner node */} +
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' }} + > + + {folderIcon} + Ohne Ordner + ({allProjects.filter(p => p.folder_id === null).length}) +
+ + {/* Top-level folders rendered recursively */} + {folders.filter(f => !f.parent_id).map(folder => renderFolderItem(folder, 1))}
-

{folderNameById.get(selectedFolderId ?? ROOT_FOLDER_ID) ?? 'Projekte'}

+

{currentFolderName}

@@ -561,7 +611,13 @@ export function Dashboard({ onOpenProject }: DashboardProps) { className="context-menu-item" onClick={() => handleContextAction('new', contextMenu.folderId)} >Neuer Ordner - {contextMenu.folderId && contextMenu.folderId !== ROOT_FOLDER_ID && contextMenu.folderId !== '__unfiled__' && ( + {contextMenu.folderId && ( + + )} + {contextMenu.folderId && ( <>