Dev #2
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user