fix: element creation fails with empty layer_id (FOREIGN KEY constraint)

Three fixes:
1. interaction/index.ts: Add layerId to created element in confirmDraw()
2. CanvasArea.tsx: Sync layers to LayerManager via layerManagerRef
3. App.tsx: Fallback to activeLayerId in handleElementCreated if layerId missing

Root cause: LayerManager.activeLayerId was empty because layers were never
synced from React state to the interaction layer LayerManager. New elements
got layer_id="" which failed SQLite FOREIGN KEY constraint.

Also includes Yjs sync race condition fix from previous commit.
This commit is contained in:
2026-06-26 20:31:50 +02:00
parent bd8bedcfed
commit 7ad0e11873
3 changed files with 33 additions and 9 deletions
+24 -8
View File
@@ -277,10 +277,25 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
// Skip if collab data is empty (not yet loaded) // Skip if collab data is empty (not yet loaded)
if (collab.elements.length === 0 && collab.layers.length === 0 && collab.blocks.length === 0) return; if (collab.elements.length === 0 && collab.layers.length === 0 && collab.blocks.length === 0) return;
// Only update local state if remote data differs // Only update local state if remote data differs AND local is not ahead of remote
const sameElements = collab.elements.length === elements.length && // (prevents race condition where local element is overwritten by stale Yjs sync)
collab.elements.every((el, i) => el.id === elements[i]?.id); const localIds = new Set(elements.map(e => e.id));
if (!sameElements) setElements(collab.elements); 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); });
setElements(merged);
}
const sameLayers = collab.layers.length === layers.length && const sameLayers = collab.layers.length === layers.length &&
collab.layers.every((l, i) => l.id === layers[i]?.id); collab.layers.every((l, i) => l.id === layers[i]?.id);
@@ -293,8 +308,9 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
}, [collab.elements, collab.layers, collab.blocks, collab.status]); }, [collab.elements, collab.layers, collab.blocks, collab.status]);
const handleElementCreated = useCallback((el: CADElement) => { const handleElementCreated = useCallback((el: CADElement) => {
const elWithLayer = el.layerId ? el : { ...el, layerId: activeLayerId };
setElements((prev) => { setElements((prev) => {
const newElements = [...prev, el]; const newElements = [...prev, elWithLayer];
historyManagerRef.current.pushSnapshot({ historyManagerRef.current.pushSnapshot({
elements: newElements, layers, blocks, groups, bgConfig, elements: newElements, layers, blocks, groups, bgConfig,
}, 'Element erstellt'); }, 'Element erstellt');
@@ -302,18 +318,18 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
}); });
syncHistory(); syncHistory();
// Push to Yjs CRDT for real-time sync // Push to Yjs CRDT for real-time sync
collab.setElement(el); collab.setElement(elWithLayer);
// Save to backend // Save to backend
if (drawingId && token) { if (drawingId && token) {
setSavedStatus('Speichert…'); setSavedStatus('Speichert…');
createElementTyped(token, drawingId, el).then(() => { createElementTyped(token, drawingId, elWithLayer).then(() => {
setSavedStatus('gespeichert'); setSavedStatus('gespeichert');
}).catch((err) => { }).catch((err) => {
console.error('Failed to save element:', err); console.error('Failed to save element:', err);
setSavedStatus('Fehler beim Speichern'); setSavedStatus('Fehler beim Speichern');
}); });
} }
}, [layers, blocks, groups, bgConfig, syncHistory, drawingId, token, collab]); }, [layers, blocks, groups, bgConfig, syncHistory, drawingId, token, collab, activeLayerId]);
const handleElementsDeleted = useCallback((ids: string[]) => { const handleElementsDeleted = useCallback((ids: string[]) => {
setElements((prev) => { setElements((prev) => {
+8
View File
@@ -29,6 +29,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
const renderEngineRef = useRef<RenderEngine | null>(null); const renderEngineRef = useRef<RenderEngine | null>(null);
const interactionRef = useRef<InteractionEngine | null>(null); const interactionRef = useRef<InteractionEngine | null>(null);
const spatialIndexRef = useRef<SpatialIndex | null>(null); const spatialIndexRef = useRef<SpatialIndex | null>(null);
const layerManagerRef = useRef<LayerManager | null>(null);
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
@@ -48,6 +49,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
renderEngineRef.current = renderEngine; renderEngineRef.current = renderEngine;
interactionRef.current = interaction; interactionRef.current = interaction;
spatialIndexRef.current = spatialIndex; spatialIndexRef.current = spatialIndex;
layerManagerRef.current = layerManager;
interaction.setCallbacks({ interaction.setCallbacks({
onElementCreated, onElementCreated,
@@ -68,6 +70,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
renderEngineRef.current = null; renderEngineRef.current = null;
interactionRef.current = null; interactionRef.current = null;
spatialIndexRef.current = null; spatialIndexRef.current = null;
layerManagerRef.current = null;
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
@@ -133,7 +136,12 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
// Sync layers to LayerManager + render // Sync layers to LayerManager + render
useEffect(() => { useEffect(() => {
const renderEngine = renderEngineRef.current; const renderEngine = renderEngineRef.current;
const layerManager = layerManagerRef.current;
if (!renderEngine) return; if (!renderEngine) return;
if (layerManager) {
layerManager.clear();
layers.forEach(l => layerManager.addLayer(l));
}
renderEngine.setLayers(layers); renderEngine.setLayers(layers);
renderEngine.render(); renderEngine.render();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
+1 -1
View File
@@ -961,7 +961,7 @@ export class InteractionEngine {
} else if (this.state.activeTool === 'revcloud' && this.state.points.length >= 2) { } else if (this.state.activeTool === 'revcloud' && this.state.points.length >= 2) {
el = dimService.createRevCloud(this.state.points, layerId); el = dimService.createRevCloud(this.state.points, layerId);
} else { } else {
el = { ...this.state.previewElement, id: this.generateId() }; el = { ...this.state.previewElement, id: this.generateId(), layerId: layerId };
} }
this.onElementCreated?.(el); this.onElementCreated?.(el);