Dev #2

Merged
sandrodach merged 37 commits from dev into main 2026-10-25 12:14:23 +01:00
Showing only changes of commit d338c4cb8a - Show all commits
@@ -113,8 +113,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const [newFolderParent, setNewFolderParent] = useState<string | null>(null);
const [newFolderName, setNewFolderName] = useState('');
// Sidebar Document Drag and Drop state
// Sidebar Document & Folder Drag and Drop state
const [draggingDocId, setDraggingDocId] = useState<string | null>(null);
const [draggingFolderPath, setDraggingFolderPath] = useState<string | null>(null);
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
// Active focused block ref
@@ -583,6 +584,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
useEffect(() => {
const handleGlobalDragEnd = () => {
setDraggingDocId(null);
setDraggingFolderPath(null);
setDragOverFolder(null);
setDraggingBlockId(null);
};
@@ -594,6 +596,100 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
};
}, []);
const handleMoveFolder = async (sourcePath: string, targetParentPath: string) => {
const cleanSource = sourcePath.trim();
const cleanTarget = targetParentPath.trim();
// Cannot move to self or into own subfolder
if (!cleanSource || cleanSource === cleanTarget || cleanTarget.startsWith(cleanSource + '/')) {
return;
}
// Name of the folder being moved
const folderName = cleanSource.split('/').pop() || cleanSource;
const newFolderPath = cleanTarget ? `${cleanTarget}/${folderName}` : folderName;
if (newFolderPath === cleanSource) return;
triggerHaptic(15);
// 1. Update all documents that are in cleanSource or any subfolder of cleanSource
const docsToUpdate = docs.filter((d) => {
const f = getDocFolder(d);
return f === cleanSource || f.startsWith(cleanSource + '/');
});
const updatedDocsList = docs.map((d) => {
const f = getDocFolder(d);
if (f === cleanSource) {
const nextFolder = newFolderPath;
const updatedTags = (d.tags || []).filter((t) => !t.startsWith('folder:'));
if (nextFolder) updatedTags.push(`folder:${nextFolder}`);
return { ...d, folderPath: nextFolder, tags: updatedTags };
} else if (f.startsWith(cleanSource + '/')) {
const suffix = f.slice(cleanSource.length);
const nextFolder = `${newFolderPath}${suffix}`;
const updatedTags = (d.tags || []).filter((t) => !t.startsWith('folder:'));
if (nextFolder) updatedTags.push(`folder:${nextFolder}`);
return { ...d, folderPath: nextFolder, tags: updatedTags };
}
return d;
});
setDocs(updatedDocsList);
if (selectedDoc) {
const found = updatedDocsList.find((d) => d.id === selectedDoc.id);
if (found) setSelectedDoc(found);
}
// Persist all affected docs to backend
for (const d of docsToUpdate) {
const f = getDocFolder(d);
let nextFolder = newFolderPath;
if (f.startsWith(cleanSource + '/')) {
const suffix = f.slice(cleanSource.length);
nextFolder = `${newFolderPath}${suffix}`;
}
const updatedTags = (d.tags || []).filter((t) => !t.startsWith('folder:'));
if (nextFolder) updatedTags.push(`folder:${nextFolder}`);
api
.updateDocument(d.id, {
folderPath: nextFolder,
tags: updatedTags,
})
.catch((err) => console.error('Failed to update doc folder', err));
}
// 2. Update customFolders in state & localStorage
const updatedCustomFolders = customFolders.map((p) => {
if (p === cleanSource) return newFolderPath;
if (p.startsWith(cleanSource + '/')) {
return `${newFolderPath}${p.slice(cleanSource.length)}`;
}
return p;
});
if (!updatedCustomFolders.includes(newFolderPath)) {
updatedCustomFolders.push(newFolderPath);
}
const sortedCustom = Array.from(new Set(updatedCustomFolders)).sort((a, b) =>
a.localeCompare(b)
);
setCustomFolders(sortedCustom);
try {
localStorage.setItem('mindsift_workspace_folders', JSON.stringify(sortedCustom));
} catch {}
// Expand the new folder path and its parent
setExpandedFolders((prev) => {
const next = new Set(prev);
next.add(newFolderPath);
if (cleanTarget) next.add(cleanTarget);
return next;
});
};
const handleDropDocToFolder = async (folderPath: string, e: React.DragEvent) => {
const docId = e.dataTransfer.getData('application/mindsift-doc-id') || draggingDocId;
setDraggingDocId(null);
@@ -1239,6 +1335,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const isExpanded = expandedFolders.has(node.path);
const isCreatingSubfolder = isCreatingFolder && newFolderParent === node.path;
const isDragOver = dragOverFolder === node.path;
const isDraggingThisFolder = draggingFolderPath === node.path;
const totalItems = node.docs.length + node.subfolders.length;
return (
@@ -1246,7 +1343,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
key={node.path}
className="select-none"
onDragOver={(e) => {
if (draggingDocId) {
if (
draggingDocId ||
(draggingFolderPath &&
draggingFolderPath !== node.path &&
!node.path.startsWith(draggingFolderPath + '/'))
) {
e.preventDefault();
e.stopPropagation();
setDragOverFolder(node.path);
@@ -1261,19 +1363,38 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
onDrop={(e) => {
e.preventDefault();
e.stopPropagation();
const droppedFolder =
e.dataTransfer.getData('application/mindsift-folder-path') || draggingFolderPath;
setDragOverFolder(null);
setDraggingDocId(null);
handleDropDocToFolder(node.path, e);
setDraggingFolderPath(null);
if (droppedFolder) {
handleMoveFolder(droppedFolder, node.path);
} else {
handleDropDocToFolder(node.path, e);
}
}}
>
<div
draggable
onDragStart={(e) => {
e.stopPropagation();
e.dataTransfer.setData('application/mindsift-folder-path', node.path);
e.dataTransfer.setData('text/plain', node.path);
setDraggingFolderPath(node.path);
}}
onDragEnd={() => setDraggingFolderPath(null)}
onClick={() => toggleFolder(node.path)}
style={{ paddingLeft: `${node.depth * 14 + 8}px` }}
className={`group flex items-center justify-between py-1.5 pr-2 cursor-pointer transition-colors text-xs ${
isDraggingThisFolder ? 'opacity-40 scale-[0.98]' : ''
} ${
isDragOver
? 'bg-accent/20 border-l-2 border-accent text-main font-medium'
: 'hover:bg-panel-elevated text-muted'
}`}
title={`${node.path} (Klick: Öffnen/Schließen · Ziehen: In anderen Ordner verschieben)`}
>
<div className="flex items-center gap-1.5 min-w-0">
<span className="text-faint hover:text-muted">
@@ -1535,8 +1656,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</div>
) : (
<div className="py-1 max-h-[calc(100vh-240px)] overflow-y-auto">
{/* Drag target for Root when dragging a note */}
{draggingDocId && (
{/* Drag target for Root when dragging a note or a folder */}
{(draggingDocId || draggingFolderPath) && (
<div
onDragOver={(e) => {
e.preventDefault();
@@ -1550,9 +1671,18 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
onDrop={(e) => {
e.preventDefault();
e.stopPropagation();
const droppedFolder =
e.dataTransfer.getData('application/mindsift-folder-path') ||
draggingFolderPath;
setDragOverFolder(null);
setDraggingDocId(null);
handleDropDocToFolder('', e);
setDraggingFolderPath(null);
if (droppedFolder) {
handleMoveFolder(droppedFolder, '');
} else {
handleDropDocToFolder('', e);
}
}}
className={`mx-2 my-1.5 p-2 border-2 border-dashed rounded-btn text-center text-xs transition-colors ${
dragOverFolder === '__root__'
@@ -1560,7 +1690,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
: 'border-border text-muted bg-canvas'
}`}
>
📁 In Hauptverzeichnis verschieben (ohne Ordner)
{draggingFolderPath
? '📁 Ordner in Hauptverzeichnis verschieben'
: '📁 In Hauptverzeichnis verschieben (ohne Ordner)'}
</div>
)}