diff --git a/frontend/src/features/workspace/WorkspaceView.tsx b/frontend/src/features/workspace/WorkspaceView.tsx index 283ce20..6640a05 100644 --- a/frontend/src/features/workspace/WorkspaceView.tsx +++ b/frontend/src/features/workspace/WorkspaceView.tsx @@ -246,9 +246,9 @@ export const WorkspaceView: React.FC = ({ } }; - const ensureLock = async (): Promise => { + const ensureLock = async (force = false): Promise => { if (!selectedDoc || selectedDoc.permission === 'read') return false; - if (holdsLockRef.current) { + if (holdsLockRef.current && !force) { return true; } try { @@ -287,10 +287,14 @@ export const WorkspaceView: React.FC = ({ if (idleUnlockTimerRef.current) { clearTimeout(idleUnlockTimerRef.current); } + // If a block is currently being dragged, do not schedule an unlock + if (isDraggingBlockRef.current) { + return; + } // Short grace period: if user clicked into another block in the doc, focus will cancel this. // If user clicked outside or stopped editing, save and release the lock immediately! idleUnlockTimerRef.current = setTimeout(async () => { - if (!isEditorFocusedRef.current && holdsLockRef.current) { + if (!isEditorFocusedRef.current && !isDraggingBlockRef.current && holdsLockRef.current) { await flushPendingSave(); await releaseMyLock(); } @@ -523,13 +527,13 @@ export const WorkspaceView: React.FC = ({ // Window blur / visibility change listener: release lock when user leaves tab/window useEffect(() => { const handleWindowBlur = () => { - if (!isEditorFocusedRef.current && holdsLockRef.current) { + if (!isEditorFocusedRef.current && !isDraggingBlockRef.current && holdsLockRef.current) { flushPendingSave(); releaseMyLock(); } }; const handleVisibilityChange = () => { - if (document.visibilityState === 'hidden') { + if (document.visibilityState === 'hidden' && !isDraggingBlockRef.current) { isEditorFocusedRef.current = false; flushPendingSave(); releaseMyLock(); @@ -1957,8 +1961,9 @@ export const WorkspaceView: React.FC = ({ clearTimeout(idleUnlockTimerRef.current); idleUnlockTimerRef.current = null; } - // Activate write lock immediately upon dragging - ensureLock(); + // Activate write lock immediately upon dragging (optimistic UI + backend broadcast) + setHoldsLock(true); + ensureLock(true); e.dataTransfer.setData('application/mindsift-block-id', blockId); e.dataTransfer.setData('text/plain', blockId); @@ -1966,6 +1971,21 @@ export const WorkspaceView: React.FC = ({ setDraggingBlockId(blockId); }; + const handleBlockDragEnd = () => { + isDraggingBlockRef.current = false; + setDraggingBlockId(null); + setDragOverBlockTarget(null); + if (idleUnlockTimerRef.current) { + clearTimeout(idleUnlockTimerRef.current); + } + idleUnlockTimerRef.current = setTimeout(async () => { + if (!isEditorFocusedRef.current && !isDraggingBlockRef.current && holdsLockRef.current) { + await flushPendingSave(); + await releaseMyLock(); + } + }, 2000); + }; + const handleBlockDragOver = (e: React.DragEvent, targetBlockId: string) => { e.preventDefault(); e.stopPropagation(); @@ -3596,25 +3616,11 @@ export const WorkspaceView: React.FC = ({ clearTimeout(idleUnlockTimerRef.current); idleUnlockTimerRef.current = null; } - isDraggingBlockRef.current = true; - ensureLock(); + ensureLock(true); } }} onDragStart={(e) => !isReadOnly && handleBlockDragStart(e, block.id)} - onDragEnd={() => { - isDraggingBlockRef.current = false; - setDraggingBlockId(null); - setDragOverBlockTarget(null); - if (idleUnlockTimerRef.current) { - clearTimeout(idleUnlockTimerRef.current); - } - idleUnlockTimerRef.current = setTimeout(async () => { - if (!isEditorFocusedRef.current && !isDraggingBlockRef.current && holdsLockRef.current) { - await flushPendingSave(); - await releaseMyLock(); - } - }, 2500); - }} + onDragEnd={handleBlockDragEnd} className="opacity-0 group-hover:opacity-100 flex items-center pt-1 text-faint hover:text-accent cursor-grab active:cursor-grabbing flex-shrink-0 -ml-1 mr-0.5" title="Ziehen zum Verschieben" > @@ -3648,25 +3654,11 @@ export const WorkspaceView: React.FC = ({ clearTimeout(idleUnlockTimerRef.current); idleUnlockTimerRef.current = null; } - isDraggingBlockRef.current = true; - ensureLock(); + ensureLock(true); } }} onDragStart={(e) => !isReadOnly && handleBlockDragStart(e, block.id)} - onDragEnd={() => { - isDraggingBlockRef.current = false; - setDraggingBlockId(null); - setDragOverBlockTarget(null); - if (idleUnlockTimerRef.current) { - clearTimeout(idleUnlockTimerRef.current); - } - idleUnlockTimerRef.current = setTimeout(async () => { - if (!isEditorFocusedRef.current && !isDraggingBlockRef.current && holdsLockRef.current) { - await flushPendingSave(); - await releaseMyLock(); - } - }, 2500); - }} + onDragEnd={handleBlockDragEnd} onDragOver={(e) => { if (!isReadOnly && draggingBlockId && draggingBlockId !== block.id) { e.preventDefault();