From baba0cbb38c9be4ae68563f56dd8611783a8341b Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Mon, 29 Jun 2026 23:56:59 +0200 Subject: [PATCH] fix(frontend): remote sync improvements, save all elements, block drop backend sync (Issues #9, #10, #11) --- frontend/src/App.tsx | 85 +++++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fe071d5..01c0d0b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -310,36 +310,66 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack // Only update local state if remote data differs AND local is not ahead of remote // (prevents race condition where local element is overwritten by stale Yjs sync) + // Merge: update local elements with remote version when properties differ, add remote-only elements const localIds = new Set(elements.map(e => e.id)); const remoteIds = new Set(collab.elements.map(e => e.id)); - const localHasExtra = elements.some(e => !remoteIds.has(e.id)); - const remoteHasExtra = collab.elements.some(e => !localIds.has(e.id)); - // Only sync from remote if remote has elements local doesn't have AND local doesn't have unsaved elements - if (remoteHasExtra && !localHasExtra) { - setElements(collab.elements); - } else if (remoteHasExtra && localHasExtra) { - // Merge: keep local elements, update any that changed remotely - const merged = elements.map(el => { - const remote = collab.elements.find(e => e.id === el.id); - return remote || el; - }); - // Add remote-only elements - collab.elements.forEach(el => { if (!localIds.has(el.id)) merged.push(el); }); + const remoteMap = new Map(collab.elements.map(e => [e.id, e])); + let changed = false; + const merged = elements.map(el => { + const remote = remoteMap.get(el.id); + if (remote && JSON.stringify(remote) !== JSON.stringify(el)) { + changed = true; + return remote; + } + return el; + }); + // Add remote-only elements + collab.elements.forEach(el => { + if (!localIds.has(el.id)) { + changed = true; + merged.push(el); + } + }); + if (changed) { setElements(merged); } // Never overwrite local layers with empty remote layers (prevents losing initialLayers) if (collab.layers.length > 0) { - const sameLayers = collab.layers.length === layers.length && - collab.layers.every((l, i) => l.id === layers[i]?.id); - if (!sameLayers) setLayers(collab.layers); + // Compare by ID set, not by index ordering. Update if any layer properties differ. + const localLayerMap = new Map(layers.map(l => [l.id, l])); + let layersChanged = false; + const mergedLayers = collab.layers.map(rl => { + const local = localLayerMap.get(rl.id); + if (!local || JSON.stringify(local) !== JSON.stringify(rl)) { + layersChanged = true; + } + return rl; + }); + // Also check if local has layers not in remote + if (!layersChanged && layers.length !== collab.layers.length) { + layersChanged = true; + } + if (layersChanged) setLayers(mergedLayers); } // Never overwrite local blocks with empty remote blocks (prevents losing defaultBlocks) if (collab.blocks.length > 0) { - const sameBlocks = collab.blocks.length === blocks.length && - collab.blocks.every((b, i) => b.id === blocks[i]?.id); - if (!sameBlocks) setBlocks(collab.blocks); + // Compare by ID set, not by index ordering. Update if any block properties differ. + const localBlockMap = new Map(blocks.map(b => [b.id, b])); + let blocksChanged = false; + const mergedBlocks = collab.blocks.map(rb => { + const local = localBlockMap.get(rb.id); + if (!local || JSON.stringify(local) !== JSON.stringify(rb)) { + blocksChanged = true; + } + return rb; + }); + // Also check if local has blocks not in remote + if (!blocksChanged && blocks.length !== collab.blocks.length) { + blocksChanged = true; + } + if (blocksChanged) setBlocks(mergedBlocks); } // Sync groups from remote → local @@ -574,8 +604,22 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack if (instance) { setElements((prev) => [...prev, instance]); setCommandHistory((prev) => [...prev, { prefix: '·', text: `Block platziert: ${blockId} bei (${x.toFixed(2)}, ${y.toFixed(2)})`, type: 'info' }]); + // Push to Yjs CRDT for real-time sync + if (collab.status === 'connected') { + collab.setElement(instance); + } + // Save to backend + if (drawingId && token) { + setSavedStatus('Speichert…'); + createElementTyped(token, drawingId, instance).then(() => { + setSavedStatus('gespeichert'); + }).catch((err) => { + console.error('Failed to save block instance:', err); + setSavedStatus('Fehler beim Speichern'); + }); + } } - }, [blocks, activeLayerId]); + }, [blocks, activeLayerId, collab, drawingId, token]); const handleSelectionChange = useCallback((selectedIds: string[]) => { if (selectedIds.length === 1) { @@ -646,7 +690,6 @@ const CADEditor: React.FC = ({ projectId, token, onNavigateBack // Save elements to backend if drawingId exists if (drawingId && token) { elements.forEach((el) => { - if (!el.id.startsWith('el-')) return; updateElement(token, el.id, el).catch(() => {}); }); }