fix: stale closure bug preventing canvas tools from creating elements
- CanvasArea: add useEffect to refresh InteractionEngine callbacks when props change - App.tsx: handleElementCreated/Deleted/Modified now use functional setElements updates - Root cause: CanvasArea init useEffect had empty deps, capturing stale callbacks at mount - handleElementCreated used [...elements, el] with stale elements=[] closure - Each new element replaced all previous ones instead of appending - All 343 tests pass, tsc clean
This commit is contained in:
+25
-19
@@ -293,11 +293,13 @@ 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 newElements = [...elements, el];
|
setElements((prev) => {
|
||||||
historyManagerRef.current.pushSnapshot({
|
const newElements = [...prev, el];
|
||||||
elements: newElements, layers, blocks, groups, bgConfig,
|
historyManagerRef.current.pushSnapshot({
|
||||||
}, 'Element erstellt');
|
elements: newElements, layers, blocks, groups, bgConfig,
|
||||||
setElements(newElements);
|
}, 'Element erstellt');
|
||||||
|
return newElements;
|
||||||
|
});
|
||||||
syncHistory();
|
syncHistory();
|
||||||
// Push to Yjs CRDT for real-time sync
|
// Push to Yjs CRDT for real-time sync
|
||||||
collab.setElement(el);
|
collab.setElement(el);
|
||||||
@@ -311,14 +313,16 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
setSavedStatus('Fehler beim Speichern');
|
setSavedStatus('Fehler beim Speichern');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [elements, layers, blocks, groups, bgConfig, syncHistory, drawingId, token, collab]);
|
}, [layers, blocks, groups, bgConfig, syncHistory, drawingId, token, collab]);
|
||||||
|
|
||||||
const handleElementsDeleted = useCallback((ids: string[]) => {
|
const handleElementsDeleted = useCallback((ids: string[]) => {
|
||||||
const newElements = elements.filter((e) => !ids.includes(e.id));
|
setElements((prev) => {
|
||||||
historyManagerRef.current.pushSnapshot({
|
const newElements = prev.filter((e) => !ids.includes(e.id));
|
||||||
elements: newElements, layers, blocks, groups, bgConfig,
|
historyManagerRef.current.pushSnapshot({
|
||||||
}, `${ids.length} Element(e) gelöscht`);
|
elements: newElements, layers, blocks, groups, bgConfig,
|
||||||
setElements(newElements);
|
}, `${ids.length} Element(e) gelöscht`);
|
||||||
|
return newElements;
|
||||||
|
});
|
||||||
syncHistory();
|
syncHistory();
|
||||||
// Push deletions to Yjs CRDT for real-time sync
|
// Push deletions to Yjs CRDT for real-time sync
|
||||||
ids.forEach(id => collab.deleteElement(id));
|
ids.forEach(id => collab.deleteElement(id));
|
||||||
@@ -335,14 +339,16 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
}, [elements, layers, blocks, groups, bgConfig, syncHistory, token, collab]);
|
}, [elements, layers, blocks, groups, bgConfig, syncHistory, token, collab]);
|
||||||
|
|
||||||
const handleElementsModified = useCallback((modified: CADElement[]) => {
|
const handleElementsModified = useCallback((modified: CADElement[]) => {
|
||||||
const newElements = elements.map((e) => {
|
setElements((prev) => {
|
||||||
const found = modified.find((m) => m.id === e.id);
|
const newElements = prev.map((e) => {
|
||||||
return found ?? e;
|
const found = modified.find((m) => m.id === e.id);
|
||||||
|
return found ?? e;
|
||||||
|
});
|
||||||
|
historyManagerRef.current.pushSnapshot({
|
||||||
|
elements: newElements, layers, blocks, groups, bgConfig,
|
||||||
|
}, `${modified.length} Element(e) geändert`);
|
||||||
|
return newElements;
|
||||||
});
|
});
|
||||||
historyManagerRef.current.pushSnapshot({
|
|
||||||
elements: newElements, layers, blocks, groups, bgConfig,
|
|
||||||
}, `${modified.length} Element(e) geändert`);
|
|
||||||
setElements(newElements);
|
|
||||||
syncHistory();
|
syncHistory();
|
||||||
// Push modifications to Yjs CRDT for real-time sync
|
// Push modifications to Yjs CRDT for real-time sync
|
||||||
modified.forEach(el => collab.setElement(el));
|
modified.forEach(el => collab.setElement(el));
|
||||||
@@ -356,7 +362,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
setSavedStatus('Fehler beim Speichern');
|
setSavedStatus('Fehler beim Speichern');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [elements, layers, blocks, groups, bgConfig, syncHistory, token, collab]);
|
}, [layers, blocks, groups, bgConfig, syncHistory, token, collab]);
|
||||||
|
|
||||||
const lastCursorUpdateRef = useRef(0);
|
const lastCursorUpdateRef = useRef(0);
|
||||||
const handleCursorMoved = useCallback((x: number, y: number) => {
|
const handleCursorMoved = useCallback((x: number, y: number) => {
|
||||||
|
|||||||
@@ -72,6 +72,23 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Update callbacks when they change (avoids stale closures)
|
||||||
|
useEffect(() => {
|
||||||
|
const interaction = interactionRef.current;
|
||||||
|
if (!interaction) return;
|
||||||
|
interaction.setCallbacks({
|
||||||
|
onElementCreated,
|
||||||
|
onElementsDeleted,
|
||||||
|
onElementsModified,
|
||||||
|
onCursorMoved,
|
||||||
|
onToolStateChanged,
|
||||||
|
onTextEdit,
|
||||||
|
onCommandTrigger,
|
||||||
|
onSelectionChange,
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [onElementCreated, onElementsDeleted, onElementsModified, onCursorMoved, onToolStateChanged, onTextEdit, onCommandTrigger, onSelectionChange]);
|
||||||
|
|
||||||
// Resize canvas to fill container
|
// Resize canvas to fill container
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
|
|||||||
Reference in New Issue
Block a user