fix: MEDIUM issues #23-#30 — stale deps, export modal, cache race, StrictMode dedup, SeatingService ref, SVG import persist, group-as-block multi-select, session expiry
This commit is contained in:
+59
-23
@@ -62,9 +62,6 @@ const initialKISuggestions: KISuggestion[] = [
|
||||
{ id: 'sug-3', label: 'Flächen berechnen' },
|
||||
];
|
||||
|
||||
// Prevent duplicate drawing creation across StrictMode double-render
|
||||
const loadedProjects = new Set<string>();
|
||||
|
||||
// ─── CAD Editor Component ───────────────────────────────
|
||||
interface CADEditorProps {
|
||||
projectId: string;
|
||||
@@ -163,15 +160,18 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
||||
// History panel
|
||||
const [historyPanelOpen, setHistoryPanelOpen] = useState(false);
|
||||
|
||||
// Export format selector (cross-browser replacement for nwsave)
|
||||
const [exportFormatOpen, setExportFormatOpen] = useState(false);
|
||||
|
||||
// Elements + undo/redo (HistoryManager)
|
||||
const [elements, setElements] = useState<CADElement[]>([]);
|
||||
const historyManagerRef = React.useRef<HistoryManager>(new HistoryManager());
|
||||
const [historyEntries, setHistoryEntries] = useState<HistoryEntry[]>([]);
|
||||
const [toolState, setToolState] = useState<ToolState | null>(null);
|
||||
|
||||
const seatingServiceRef = useRef<SeatingService>(new SeatingService());
|
||||
const seatCount = useMemo(() => {
|
||||
const svc = new SeatingService();
|
||||
return svc.countSeats(elements).total;
|
||||
return seatingServiceRef.current.countSeats(elements).total;
|
||||
}, [elements]);
|
||||
|
||||
// Groups
|
||||
@@ -479,7 +479,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
||||
setSavedStatus('Fehler beim Speichern');
|
||||
});
|
||||
}
|
||||
}, [elements, layers, blocks, groups, bgConfig, syncHistory, token, collab]);
|
||||
}, [layers, blocks, groups, bgConfig, syncHistory, token, collab]);
|
||||
|
||||
const handleElementsModified = useCallback((modified: CADElement[]) => {
|
||||
setElements((prev) => {
|
||||
@@ -582,11 +582,30 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
||||
const svc = new BlockService();
|
||||
const block = svc.importSVG(svg, name, category);
|
||||
setBlocks((prev) => [...prev, block]);
|
||||
// Push to Yjs CRDT for real-time sync
|
||||
collab.setBlock(block);
|
||||
// Persist to backend
|
||||
if (drawingId && token) {
|
||||
setSavedStatus('Speichert…');
|
||||
createBlockTyped(token, drawingId, block)
|
||||
.then(() => setSavedStatus('gespeichert'))
|
||||
.catch((err) => {
|
||||
console.error('Failed to save imported SVG block:', err);
|
||||
setSavedStatus('Fehler beim Speichern');
|
||||
});
|
||||
}
|
||||
setCommandHistory((prev) => [...prev, { prefix: '·', text: `SVG importiert: ${name}`, type: 'info' }]);
|
||||
}, []);
|
||||
}, [collab, drawingId, token]);
|
||||
|
||||
const handleSaveGroupAsBlock = useCallback((name: string) => {
|
||||
const selectedEls = selectedElement ? [selectedElement] : [];
|
||||
const selectedIds = selectedElementIdsRef.current;
|
||||
const selectedEls = selectedIds.length > 0
|
||||
? elementsRef.current.filter(e => selectedIds.includes(e.id))
|
||||
: (selectedElement ? [selectedElement] : []);
|
||||
if (selectedEls.length === 0) {
|
||||
setCommandHistory((prev) => [...prev, { prefix: '·', text: 'Keine Elemente ausgewählt', type: 'info' }]);
|
||||
return;
|
||||
}
|
||||
setBlocks((prev) => {
|
||||
const newId = `blk_grp_${Date.now()}`;
|
||||
const block: BlockDefinition = {
|
||||
@@ -596,10 +615,18 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
||||
category: 'Custom',
|
||||
elements: selectedEls.map(el => ({ ...el, id: `el_${Date.now()}_${Math.random().toString(36).slice(2, 7)}` })),
|
||||
};
|
||||
// Push to Yjs CRDT for real-time sync
|
||||
collab.setBlock(block);
|
||||
// Persist to backend
|
||||
if (drawingId && token) {
|
||||
createBlockTyped(token, drawingId, block).catch((err) => {
|
||||
console.error('Failed to save group block:', err);
|
||||
});
|
||||
}
|
||||
return [...prev, block];
|
||||
});
|
||||
setCommandHistory((prev) => [...prev, { prefix: '·', text: `Block erstellt: ${name} (${selectedEls.length} Elemente)`, type: 'info' }]);
|
||||
}, [selectedElement]);
|
||||
}, [selectedElement, collab, drawingId, token]);
|
||||
|
||||
const handleBlockCategoryChange = useCallback((cat: string) => {
|
||||
setBlockCategory(cat);
|
||||
@@ -763,20 +790,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
||||
return;
|
||||
}
|
||||
if (action === 'export') {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.setAttribute('nwsave', '');
|
||||
input.accept = '.dxf,.svg,.pdf,.png,.json';
|
||||
input.onchange = () => {
|
||||
const name = input.value || 'cad-export';
|
||||
const ext = name.split('.').pop()?.toLowerCase() as ExportFormat;
|
||||
if (ext && ['dxf', 'svg', 'pdf', 'png', 'json'].includes(ext)) {
|
||||
handleExport(ext);
|
||||
} else {
|
||||
handleExport('dxf');
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
setExportFormatOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1425,6 +1439,28 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
||||
onClose={() => setHistoryPanelOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{exportFormatOpen && (
|
||||
<div className="export-format-overlay" onClick={() => setExportFormatOpen(false)}>
|
||||
<div className="export-format-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<h3>Export als…</h3>
|
||||
<div className="export-format-buttons">
|
||||
{(['dxf', 'svg', 'pdf', 'png', 'json'] as ExportFormat[]).map(fmt => (
|
||||
<button
|
||||
key={fmt}
|
||||
className="export-format-btn"
|
||||
onClick={() => {
|
||||
setExportFormatOpen(false);
|
||||
handleExport(fmt);
|
||||
}}
|
||||
>
|
||||
{fmt.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button className="export-format-cancel" onClick={() => setExportFormatOpen(false)}>Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user