UNPKG

@ichigo_san/graphing

Version:

A lightweight UML-style diagram editor built with React Flow and Tailwind CSS

1,385 lines (1,337 loc) 89.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = _interopRequireWildcard(require("react")); var _lucideReact = require("lucide-react"); var _reactflow = _interopRequireWildcard(require("reactflow")); require("reactflow/dist/style.css"); var _DiamondNode = _interopRequireDefault(require("../nodes/DiamondNode")); var _CircleNode = _interopRequireDefault(require("../nodes/CircleNode")); var _HexagonNode = _interopRequireDefault(require("../nodes/HexagonNode")); var _TriangleNode = _interopRequireDefault(require("../nodes/TriangleNode")); var _ContainerNode = _interopRequireDefault(require("../nodes/ContainerNode")); var _ComponentNode = _interopRequireDefault(require("../nodes/ComponentNode")); var _UniversalShapeNode = _interopRequireDefault(require("../nodes/UniversalShapeNode")); var _edges = require("../edges"); var _OptimizedOrthogonalEdge = _interopRequireDefault(require("../edges/OptimizedOrthogonalEdge")); var _PromptModal = _interopRequireDefault(require("../modals/PromptModal")); var _ConfirmModal = _interopRequireDefault(require("../modals/ConfirmModal")); var _ContainerSelectorModal = _interopRequireDefault(require("../modals/ContainerSelectorModal")); var _ShapeSelectorModal = _interopRequireDefault(require("../modals/ShapeSelectorModal")); var _JsonPasteModal = _interopRequireDefault(require("../modals/JsonPasteModal")); var _JsonValidatorModal = _interopRequireDefault(require("../modals/JsonValidatorModal")); var _ExportModal = _interopRequireDefault(require("../modals/ExportModal")); var _TailwindPropertyEditor = _interopRequireDefault(require("./TailwindPropertyEditor")); var _TechnicalDetailsPanel = _interopRequireDefault(require("./TechnicalDetailsPanel")); var _ShapeLibraryPanel = _interopRequireDefault(require("./ShapeLibraryPanel")); var _LayoutSettingsPanel = _interopRequireDefault(require("./LayoutSettingsPanel")); var _EnhancedMenuBar = _interopRequireDefault(require("./EnhancedMenuBar")); var _autoLayout = require("../utils/autoLayout"); var _ServiceFactory = require("../../services/ServiceFactory"); var _EnhancedEdgeManager = _interopRequireDefault(require("../../services/EnhancedEdgeManager")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } // Import node components // Import modal components // Import editor components // Import new service layer const ArchitectureDiagramEditorContent = ({ initialDiagram, onToggleTheme, showThemeToggle, onToggleFullscreen, isFullscreen, onToggleMini, showMiniToggle }) => { // Service layer integration const [serviceFactory, setServiceFactory] = (0, _react.useState)(null); const [isServiceInitialized, setIsServiceInitialized] = (0, _react.useState)(false); // Initialize service layer and enhanced edge manager (0, _react.useEffect)(() => { const initializeServices = async () => { try { console.log('Starting service layer initialization...'); const factory = _ServiceFactory.ServiceFactory.create(); console.log('ServiceFactory created, initializing...'); await factory.initialize(); console.log('ServiceFactory initialized, setting state...'); setServiceFactory(factory); setIsServiceInitialized(true); // Initialize enhanced edge manager console.log('Initializing Enhanced Edge Manager...'); await _EnhancedEdgeManager.default.initialize({ enablePerformanceMonitoring: true, enableLayoutAwareRouting: true, enableBatchProcessing: true, virtualBendsEnabled: true, intersectionDetectionEnabled: true, debounceTime: 100 }); console.log('Enhanced Edge Manager initialized successfully'); console.log('All services initialized successfully'); } catch (error) { console.error('Failed to initialize services:', error); console.error('Error details:', error.stack); } }; initializeServices(); }, []); // State for nodes and edges const [nodes, setNodes] = (0, _react.useState)([]); const [edges, setEdges] = (0, _react.useState)([]); const [selectedElements, setSelectedElements] = (0, _react.useState)({ nodes: [], edges: [] }); const [history, setHistory] = (0, _react.useState)({ past: [], present: { nodes: [], edges: [] }, future: [] }); const [isInitialized, setIsInitialized] = (0, _react.useState)(false); const [clipboardData, setClipboardData] = (0, _react.useState)(null); const [propertyPanelOpen, setPropertyPanelOpen] = (0, _react.useState)(true); const [propertyPanelMinimized, setPropertyPanelMinimized] = (0, _react.useState)(false); const [statsPanelOpen, setStatsPanelOpen] = (0, _react.useState)(true); const [panMode, setPanMode] = (0, _react.useState)(false); const [shapeLibraryOpen, setShapeLibraryOpen] = (0, _react.useState)(false); // Technical details state const [technicalDetailsPanelOpen, setTechnicalDetailsPanelOpen] = (0, _react.useState)(false); const [selectedElementForTechnicalDetails, setSelectedElementForTechnicalDetails] = (0, _react.useState)(null); const [technicalDetailsEnabled, setTechnicalDetailsEnabled] = (0, _react.useState)(true); // NEW const [panelOffset, setPanelOffset] = (0, _react.useState)({ x: 0, y: 0 }); // NEW // Layout settings state const [layoutPanelOpen, setLayoutPanelOpen] = (0, _react.useState)(false); const [currentLayoutAlgorithm, setCurrentLayoutAlgorithm] = (0, _react.useState)('circular'); const getDiagramBounds = (0, _react.useCallback)(() => { if (nodes.length === 0) return { x: 0, y: 0, width: 0, height: 0 }; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; nodes.forEach(n => { var _n$__rf, _n$style, _n$__rf2, _n$style2; const width = ((_n$__rf = n.__rf) === null || _n$__rf === void 0 ? void 0 : _n$__rf.width) || ((_n$style = n.style) === null || _n$style === void 0 ? void 0 : _n$style.width) || 150; const height = ((_n$__rf2 = n.__rf) === null || _n$__rf2 === void 0 ? void 0 : _n$__rf2.height) || ((_n$style2 = n.style) === null || _n$style2 === void 0 ? void 0 : _n$style2.height) || 80; minX = Math.min(minX, n.position.x); minY = Math.min(minY, n.position.y); maxX = Math.max(maxX, n.position.x + width); maxY = Math.max(maxY, n.position.y + height); }); return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }; }, [nodes]); const updateNodeInternals = (0, _reactflow.useUpdateNodeInternals)(); const { screenToFlowPosition } = (0, _reactflow.useReactFlow)(); // State for modals const [promptModal, setPromptModal] = (0, _react.useState)({ isOpen: false, title: '', message: '', defaultValue: '', onConfirm: null }); const [confirmModal, setConfirmModal] = (0, _react.useState)({ isOpen: false, title: '', message: '', onConfirm: null }); const [containerSelectorModal, setContainerSelectorModal] = (0, _react.useState)({ isOpen: false, title: '', message: '', containers: [], onSelect: null }); const [shapeSelectorModal, setShapeSelectorModal] = (0, _react.useState)({ isOpen: false }); const [jsonPasteModal, setJsonPasteModal] = (0, _react.useState)({ isOpen: false, onConfirm: null }); const [jsonValidatorModal, setJsonValidatorModal] = (0, _react.useState)({ isOpen: false }); const [exportModal, setExportModal] = (0, _react.useState)({ isOpen: false }); // Refs const reactFlowWrapper = (0, _react.useRef)(null); const saveTimeoutRef = (0, _react.useRef)(null); const togglePropertyPanel = (0, _react.useCallback)(() => { setPropertyPanelOpen(prev => { const next = !prev; if (next) setPropertyPanelMinimized(false); return next; }); }, []); const togglePropertyPanelMinimized = (0, _react.useCallback)(() => { setPropertyPanelMinimized(prev => !prev); }, []); const toggleStatsPanel = (0, _react.useCallback)(() => { setStatsPanelOpen(prev => !prev); }, []); const togglePanMode = (0, _react.useCallback)(() => { setPanMode(prev => !prev); }, []); const handleContextMenu = (0, _react.useCallback)(event => { if (panMode) { event.preventDefault(); } }, [panMode]); // Custom node types const nodeTypes = (0, _react.useMemo)(() => ({ diamond: _DiamondNode.default, circle: _CircleNode.default, hexagon: _HexagonNode.default, triangle: _TriangleNode.default, container: _ContainerNode.default, component: _ComponentNode.default, universalShape: _UniversalShapeNode.default }), []); const edgeTypes = (0, _react.useMemo)(() => ({ adjustable: _OptimizedOrthogonalEdge.default, enhanced: _OptimizedOrthogonalEdge.default, smart: _OptimizedOrthogonalEdge.default, drawio: _OptimizedOrthogonalEdge.default, orthogonal: _OptimizedOrthogonalEdge.default, segment: _OptimizedOrthogonalEdge.default, elbow: _OptimizedOrthogonalEdge.default, straight: _OptimizedOrthogonalEdge.default }), []); // Stable node label change handler const handleNodeLabelChange = (0, _react.useCallback)((nodeId, label) => { setNodes(nds => nds.map(node => { if (node.id === nodeId) { return { ...node, data: { ...node.data, label } }; } return node; })); // Debounced history save if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); } saveTimeoutRef.current = setTimeout(() => { setHistory(prev => ({ past: [...prev.past, prev.present], present: { nodes, edges }, future: [] })); }, 500); }, [nodes, edges]); // Conversion functions for service layer const reactFlowToJson = (0, _react.useCallback)((reactFlowNodes, reactFlowEdges) => { return { containers: reactFlowNodes.filter(node => node.type === 'container').map(container => { var _container$style, _container$style2; return { id: container.id, label: container.data.label, position: container.position, size: { width: ((_container$style = container.style) === null || _container$style === void 0 ? void 0 : _container$style.width) || 400, height: ((_container$style2 = container.style) === null || _container$style2 === void 0 ? void 0 : _container$style2.height) || 300 }, color: container.data.color, bgColor: container.data.bgColor, borderColor: container.data.borderColor, icon: container.data.icon, description: container.data.description, zIndex: container.zIndex || 1 }; }), nodes: reactFlowNodes.filter(node => node.type !== 'container').map(node => { var _node$style, _node$style2; return { id: node.id, label: node.data.label, type: node.type, position: node.position, parentContainer: node.parentNode, size: { width: ((_node$style = node.style) === null || _node$style === void 0 ? void 0 : _node$style.width) || 150, height: ((_node$style2 = node.style) === null || _node$style2 === void 0 ? void 0 : _node$style2.height) || 80 }, color: node.data.color, borderColor: node.data.borderColor, icon: node.data.icon, description: node.data.description, zIndex: node.zIndex || 10 }; }), connections: reactFlowEdges.map(edge => { var _edge$data, _edge$data2, _edge$data3, _edge$data4, _edge$style, _edge$style2; return { id: edge.id, source: edge.source, target: edge.target, label: (_edge$data = edge.data) === null || _edge$data === void 0 ? void 0 : _edge$data.label, type: edge.type, animated: edge.animated, description: (_edge$data2 = edge.data) === null || _edge$data2 === void 0 ? void 0 : _edge$data2.description, waypoints: (_edge$data3 = edge.data) === null || _edge$data3 === void 0 ? void 0 : _edge$data3.waypoints, markerStart: edge.markerStart, markerEnd: edge.markerEnd, intersection: (_edge$data4 = edge.data) === null || _edge$data4 === void 0 ? void 0 : _edge$data4.intersection, style: { strokeWidth: ((_edge$style = edge.style) === null || _edge$style === void 0 ? void 0 : _edge$style.strokeWidth) || 2, strokeDasharray: (_edge$style2 = edge.style) === null || _edge$style2 === void 0 ? void 0 : _edge$style2.strokeDasharray }, zIndex: edge.zIndex || 5 }; }) }; }, []); const jsonToReactFlow = (0, _react.useCallback)(data => { const reactFlowNodes = []; const reactFlowEdges = []; // Convert containers if (data.containers) { data.containers.forEach(container => { var _container$size, _container$size2; reactFlowNodes.push({ id: container.id, type: 'container', position: container.position, data: { label: container.label, color: container.color || '#f5f5f5', bgColor: container.bgColor || '#f5f5f5', borderColor: container.borderColor || '#ddd', icon: container.icon, description: container.description, onLabelChange: handleNodeLabelChange }, style: { width: ((_container$size = container.size) === null || _container$size === void 0 ? void 0 : _container$size.width) || 400, height: ((_container$size2 = container.size) === null || _container$size2 === void 0 ? void 0 : _container$size2.height) || 300, zIndex: container.zIndex || 1 }, draggable: true, selectable: true, zIndex: container.zIndex || 1 }); }); } // Convert nodes if (data.nodes) { data.nodes.forEach(node => { var _node$size, _node$size2; reactFlowNodes.push({ id: node.id, type: 'component', position: node.position, parentNode: node.parentContainer, data: { label: node.label, color: node.color || '#E3F2FD', borderColor: node.borderColor || '#90CAF9', icon: node.icon, description: node.description, onLabelChange: handleNodeLabelChange }, style: { width: ((_node$size = node.size) === null || _node$size === void 0 ? void 0 : _node$size.width) || 150, height: ((_node$size2 = node.size) === null || _node$size2 === void 0 ? void 0 : _node$size2.height) || 80, zIndex: node.zIndex || 10 }, draggable: true, selectable: true, zIndex: node.zIndex || 10 }); }); } // Convert connections if (data.connections) { data.connections.forEach(connection => { var _connection$style, _connection$style2; // Convert old control points to waypoints and ensure we use adjustable edge type let waypoints = []; if (connection.waypoints) { waypoints = connection.waypoints; } else if (connection.control) { // Migrate old single control point to waypoints array waypoints = [connection.control]; } reactFlowEdges.push({ id: connection.id, source: connection.source, target: connection.target, type: 'enhanced', // Use enhanced type for Draw.io-style functionality animated: connection.animated || false, style: { strokeWidth: ((_connection$style = connection.style) === null || _connection$style === void 0 ? void 0 : _connection$style.strokeWidth) || 2, stroke: ((_connection$style2 = connection.style) === null || _connection$style2 === void 0 ? void 0 : _connection$style2.stroke) || '#2563eb', zIndex: connection.zIndex || 5 }, zIndex: connection.zIndex || 5, markerStart: connection.markerStart, markerEnd: connection.markerEnd || { type: 'arrow' }, data: { label: connection.label, description: connection.description || '', waypoints: waypoints, intersection: connection.intersection || 'none' } }); }); } return { nodes: reactFlowNodes, edges: reactFlowEdges }; }, [handleNodeLabelChange]); // Migrate edges to use enhanced orthogonal edge functionality const migrateEdgesToEnhanced = (0, _react.useCallback)(edges => { return edges.map(edge => { var _edge$style3, _edge$style4, _edge$data5, _edge$data6, _edge$data7; // Ensure all edges use enhanced type for Draw.io-style functionality const migratedEdge = { ...edge, type: 'enhanced', style: { ...edge.style, stroke: ((_edge$style3 = edge.style) === null || _edge$style3 === void 0 ? void 0 : _edge$style3.stroke) || '#2563eb', strokeWidth: ((_edge$style4 = edge.style) === null || _edge$style4 === void 0 ? void 0 : _edge$style4.strokeWidth) || 2 }, data: { ...edge.data, waypoints: ((_edge$data5 = edge.data) === null || _edge$data5 === void 0 ? void 0 : _edge$data5.waypoints) || ((_edge$data6 = edge.data) !== null && _edge$data6 !== void 0 && _edge$data6.control ? [edge.data.control] : []), intersection: ((_edge$data7 = edge.data) === null || _edge$data7 === void 0 ? void 0 : _edge$data7.intersection) || 'none' } }; return migratedEdge; }); }, []); // Register edges with enhanced edge manager (0, _react.useEffect)(() => { if (_EnhancedEdgeManager.default.isInitialized && edges.length > 0 && nodes.length > 0) { edges.forEach(edge => { if (!_EnhancedEdgeManager.default.getEdgeInfo(edge.id)) { _EnhancedEdgeManager.default.registerEdge(edge.id, edge, nodes); } }); } }, [edges, nodes]); // Initialize diagram from the provided configuration (0, _react.useEffect)(() => { if (!isInitialized) { const config = initialDiagram || { containers: [], nodes: [], connections: [] }; const { nodes: initialNodes, edges: initialEdges } = jsonToReactFlow(config); // Migrate edges to ensure waypoint functionality - now use enhanced edges const enhancedEdges = initialEdges.map(edge => ({ ...edge, type: 'enhanced' // Use our new enhanced edge type })); // Apply circular layout by default for initial diagrams const laidOut = (0, _autoLayout.autoLayoutNodes)(initialNodes, 'circular'); setNodes(laidOut); setEdges(enhancedEdges); setHistory({ past: [], present: { nodes: laidOut, edges: enhancedEdges }, future: [] }); laidOut.forEach(n => updateNodeInternals(n.id)); setIsInitialized(true); } }, [isInitialized, jsonToReactFlow, initialDiagram, updateNodeInternals]); // Optimized save to history function const saveToHistory = (0, _react.useCallback)(() => { if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); } saveTimeoutRef.current = setTimeout(() => { setHistory(prev => ({ past: [...prev.past, prev.present], present: { nodes, edges }, future: [] })); }, 300); }, [nodes, edges]); // Helper functions for modals const showPromptModal = (0, _react.useCallback)((title, message, defaultValue, onConfirm) => { setPromptModal({ isOpen: true, title, message, defaultValue, onConfirm: value => { onConfirm(value); setPromptModal(prev => ({ ...prev, isOpen: false })); } }); }, []); const showConfirmModal = (0, _react.useCallback)((title, message, onConfirm) => { setConfirmModal({ isOpen: true, title, message, onConfirm: () => { onConfirm(); setConfirmModal(prev => ({ ...prev, isOpen: false })); } }); }, []); const showContainerSelectorModal = (0, _react.useCallback)((title, message, containers, onSelect) => { setContainerSelectorModal({ isOpen: true, title, message, containers, onSelect: containerId => { onSelect(containerId); setContainerSelectorModal(prev => ({ ...prev, isOpen: false })); } }); }, []); const showShapeSelectorModal = (0, _react.useCallback)(() => { setShapeSelectorModal({ isOpen: true }); }, []); const showJsonPasteModal = (0, _react.useCallback)(onConfirm => { setJsonPasteModal({ isOpen: true, onConfirm }); }, []); const showJsonValidatorModal = (0, _react.useCallback)(() => { setJsonValidatorModal({ isOpen: true }); }, []); // Optimized change handlers const handleNodesChange = (0, _react.useCallback)(changes => { setNodes(nds => (0, _reactflow.applyNodeChanges)(changes, nds)); }, []); const onEdgesChange = (0, _react.useCallback)(changes => { setEdges(eds => (0, _reactflow.applyEdgeChanges)(changes, eds)); }, []); const onNodeDragStop = (0, _react.useCallback)((event, node) => { const updatedNode = { ...node, position: node.position }; setNodes(prevNodes => prevNodes.map(n => n.id === node.id ? updatedNode : n)); saveToHistory(); }, [saveToHistory]); const onEdgeUpdateStart = (0, _react.useCallback)(() => {}, []); const onEdgeUpdateEnd = (0, _react.useCallback)(() => { saveToHistory(); }, [saveToHistory]); const onEdgeUpdate = (0, _react.useCallback)((oldEdge, newConnection) => { setEdges(eds => (0, _reactflow.reconnectEdge)(oldEdge, newConnection, eds)); }, []); const onConnectStart = (0, _react.useCallback)(() => {}, []); const onConnectEnd = (0, _react.useCallback)(() => {}, []); const onEdgeDoubleClick = (0, _react.useCallback)((event, edge) => { event.preventDefault(); // Prevent default browser behavior (e.g., text selection) event.stopPropagation(); // Stop event propagation const position = screenToFlowPosition({ x: event.clientX, y: event.clientY }); setEdges(eds => eds.map(e => { if (e.id === edge.id) { const newWaypoints = [...(e.data.waypoints || []), position]; return { ...e, data: { ...e.data, waypoints: newWaypoints } }; } return e; })); saveToHistory(); }, [screenToFlowPosition, setEdges, saveToHistory]); const defaultEdgeOptions = (0, _react.useMemo)(() => ({ type: 'enhanced', animated: true, style: { strokeWidth: 2, stroke: '#2563eb', strokeDasharray: '5 5' }, data: { intersection: 'none' } }), []); // Handle new connections const onConnect = (0, _react.useCallback)(params => { const sourceNode = nodes.find(n => n.id === params.source); const targetNode = nodes.find(n => n.id === params.target); let defaultWaypoint = []; if (sourceNode && targetNode) { const sourceX = sourceNode.position.x + (sourceNode.width || 150) / 2; const sourceY = sourceNode.position.y + (sourceNode.height || 80) / 2; const targetX = targetNode.position.x + (targetNode.width || 150) / 2; const targetY = targetNode.position.y + (targetNode.height || 80) / 2; // Create orthogonal waypoints for step-like routing const midX = (sourceX + targetX) / 2; const midY = (sourceY + targetY) / 2; // Create a simple L-shaped path (step routing) if (Math.abs(sourceX - targetX) > Math.abs(sourceY - targetY)) { // Horizontal routing preference defaultWaypoint = [{ x: midX, y: sourceY }, { x: midX, y: targetY }]; } else { // Vertical routing preference defaultWaypoint = [{ x: sourceX, y: midY }, { x: targetX, y: midY }]; } } const newEdge = { ...params, id: `edge-${Date.now()}`, type: 'enhanced', animated: true, style: { strokeWidth: 2, stroke: '#2563eb', strokeDasharray: '5 5', zIndex: 5 }, zIndex: 5, markerEnd: { type: 'arrow' }, data: { label: '', description: '', intersection: 'none', waypoints: defaultWaypoint } }; setEdges(eds => (0, _reactflow.addEdge)(newEdge, eds)); saveToHistory(); }, [saveToHistory, nodes]); // Handle selection changes - now includes edges const onSelectionChange = (0, _react.useCallback)(({ nodes: selectedNodes, edges: selectedEdges }) => { setSelectedElements({ nodes: selectedNodes || [], edges: selectedEdges || [] }); // Update technical details panel if (technicalDetailsEnabled) { if (selectedNodes.length === 1) { setSelectedElementForTechnicalDetails({ ...selectedNodes[0], type: 'node' }); setTechnicalDetailsPanelOpen(true); } else if (selectedEdges.length === 1) { setSelectedElementForTechnicalDetails({ ...selectedEdges[0], type: 'edge' }); setTechnicalDetailsPanelOpen(true); } else { setTechnicalDetailsPanelOpen(false); setSelectedElementForTechnicalDetails(null); } } else { setTechnicalDetailsPanelOpen(false); setSelectedElementForTechnicalDetails(null); } }, [technicalDetailsEnabled]); // Handle edge click - now uses property panel instead of modal const onEdgeClick = (0, _react.useCallback)((event, edge) => { event.stopPropagation(); setSelectedElements({ nodes: [], edges: [edge] }); // Update technical details only if enabled if (technicalDetailsEnabled) { setSelectedElementForTechnicalDetails({ ...edge, type: 'edge' }); setTechnicalDetailsPanelOpen(true); } }, [technicalDetailsEnabled]); // Handle property changes for both nodes and edges const handleElementPropertyChange = (0, _react.useCallback)((elementType, property, value) => { if (elementType === 'node' && selectedElements.nodes.length === 1) { const nodeId = selectedElements.nodes[0].id; setNodes(nds => nds.map(node => { if (node.id === nodeId) { if (property === 'label') { return { ...node, data: { ...node.data, label: value } }; } else if (property === 'zIndex') { return { ...node, style: { ...node.style, zIndex: parseInt(value) }, zIndex: parseInt(value) }; } else { return { ...node, data: { ...node.data, [property]: value } }; } } return node; })); } else if (elementType === 'edge' && selectedElements.edges.length === 1) { const edgeId = selectedElements.edges[0].id; setEdges(eds => eds.map(edge => { if (edge.id === edgeId) { if (property === 'label') { return { ...edge, label: value, data: { ...edge.data, label: value } }; } else if (property === 'zIndex') { return { ...edge, style: { ...edge.style, zIndex: parseInt(value) }, zIndex: parseInt(value) }; } else if (property === 'type' || property === 'animated') { return { ...edge, [property]: value }; } else if (property === 'markerStart' || property === 'markerEnd') { return { ...edge, [property]: value }; } else if (property.startsWith('style.')) { const styleProp = property.replace('style.', ''); return { ...edge, style: { ...edge.style, [styleProp]: value } }; } else { return { ...edge, data: { ...edge.data, [property]: value } }; } } return edge; })); } saveToHistory(); }, [selectedElements, saveToHistory]); const applyAutoLayout = (0, _react.useCallback)(async (currentNodes, algorithm = 'circular') => { if (!serviceFactory || !isServiceInitialized) { console.warn('Service layer not initialized, falling back to old method'); const laidOut = (0, _autoLayout.autoLayoutNodes)(currentNodes, algorithm); setNodes(laidOut); laidOut.forEach(n => updateNodeInternals(n.id)); return; } try { const diagramData = reactFlowToJson(currentNodes, edges); const result = await serviceFactory.layoutDiagram(diagramData, { algorithm: algorithm, options: { spacing: 50 } }); if (result && result.success) { const { nodes: laidOutNodes } = jsonToReactFlow(result.diagramData); setNodes(laidOutNodes); laidOutNodes.forEach(n => updateNodeInternals(n.id)); } else { const errorMessage = (result === null || result === void 0 ? void 0 : result.error) || 'Unknown layout error'; console.error('Auto-layout failed:', errorMessage); // Fallback to old method const laidOut = (0, _autoLayout.autoLayoutNodes)(currentNodes, algorithm); setNodes(laidOut); laidOut.forEach(n => updateNodeInternals(n.id)); } } catch (error) { console.error('Error applying auto-layout:', error.message || error); // Fallback to old method const laidOut = (0, _autoLayout.autoLayoutNodes)(currentNodes, algorithm); setNodes(laidOut); laidOut.forEach(n => updateNodeInternals(n.id)); } }, [updateNodeInternals, serviceFactory, isServiceInitialized, edges]); // Add new container node const addContainerNode = (0, _react.useCallback)(() => { showPromptModal('Add Container', 'Enter container name:', 'New Container', name => { let position = { x: 0, y: 0 }; if (reactFlowWrapper.current) { const { left, top, width, height } = reactFlowWrapper.current.getBoundingClientRect(); const center = { x: left + width / 2, y: top + height / 2 }; position = screenToFlowPosition({ x: center.x, y: center.y }); } const newNode = { id: `container-${Date.now()}`, type: 'container', position, data: { label: name, icon: '📦', color: '#f5f5f5', borderColor: '#ddd', description: '', onLabelChange: handleNodeLabelChange }, style: { width: 400, height: 300, zIndex: 1 }, draggable: true, selectable: true, zIndex: 1 }; setNodes(nds => [...nds, newNode]); saveToHistory(); }); }, [nodes, handleNodeLabelChange, saveToHistory, showPromptModal, screenToFlowPosition]); // Add new component node const addComponentNode = (0, _react.useCallback)(() => { const containerNodes = nodes.filter(node => node.type === 'container'); if (containerNodes.length > 0) { showContainerSelectorModal('Select Container', 'Choose a container for this component:', containerNodes, containerId => { showPromptModal('Add Component', 'Enter component name:', 'New Component', name => { const container = nodes.find(node => node.id === containerId); let position = { x: 50, y: 50 }; if (container) { const componentsInContainer = nodes.filter(node => node.parentNode === containerId).length; position = { x: 50 + componentsInContainer % 2 * 170, y: 50 + Math.floor(componentsInContainer / 2) * 100 }; } const newNode = { id: `component-${Date.now()}`, type: 'component', position, parentNode: containerId, data: { label: name, icon: '🔹', color: '#E3F2FD', borderColor: '#90CAF9', description: '', onLabelChange: handleNodeLabelChange }, style: { width: 150, height: 80, zIndex: 10 }, draggable: true, selectable: true, zIndex: 10 }; setNodes(nds => [...nds, newNode]); saveToHistory(); }); }); } else { showPromptModal('Add Component', 'Enter component name:', 'New Component', name => { const newNode = { id: `component-${Date.now()}`, type: 'component', position: { x: Math.random() * 300 + 100, y: Math.random() * 200 + 100 }, data: { label: name, icon: '🔹', color: '#E3F2FD', borderColor: '#90CAF9', description: '', onLabelChange: handleNodeLabelChange }, style: { width: 150, height: 80, zIndex: 10 }, draggable: true, selectable: true, zIndex: 10 }; setNodes(nds => [...nds, newNode]); saveToHistory(); }); } }, [nodes, handleNodeLabelChange, saveToHistory, showContainerSelectorModal, showPromptModal]); // Duplicate a node - Removed unused function to fix ESLint warning // Add new shape node const addShapeNode = (0, _react.useCallback)(shapeType => { showPromptModal('Add Shape', 'Enter shape name:', 'New Shape', name => { const newNode = { id: `${shapeType}-${Date.now()}`, type: shapeType, position: { x: Math.random() * 300 + 100, y: Math.random() * 200 + 100 }, data: { label: name, icon: shapeType === 'diamond' ? '♦️' : shapeType === 'circle' ? '⭕' : shapeType === 'triangle' ? '🔺' : '⬢', color: shapeType === 'diamond' ? '#81D4FA' : shapeType === 'circle' ? '#C5E1A5' : shapeType === 'triangle' ? '#FFD54F' : '#FFCC80', borderColor: '#ddd', description: '', onLabelChange: handleNodeLabelChange }, style: { width: 100, height: 100, zIndex: 15 }, draggable: true, selectable: true, zIndex: 15 }; setNodes(nds => [...nds, newNode]); saveToHistory(); }); }, [handleNodeLabelChange, saveToHistory, showPromptModal]); // Handle shape selection from shape library const handleShapeSelect = (0, _react.useCallback)(shapeData => { var _shapeData$defaultSiz, _shapeData$defaultSiz2; let position = { x: 100, y: 100 }; // Try to position at viewport center if (reactFlowWrapper.current) { const { left, top, width, height } = reactFlowWrapper.current.getBoundingClientRect(); const center = { x: left + width / 2, y: top + height / 2 }; position = screenToFlowPosition({ x: center.x, y: center.y }); } const newNode = { id: `shape-${Date.now()}`, type: shapeData.type, position, data: { ...shapeData, onLabelChange: handleNodeLabelChange }, style: { width: ((_shapeData$defaultSiz = shapeData.defaultSize) === null || _shapeData$defaultSiz === void 0 ? void 0 : _shapeData$defaultSiz.width) || 100, height: ((_shapeData$defaultSiz2 = shapeData.defaultSize) === null || _shapeData$defaultSiz2 === void 0 ? void 0 : _shapeData$defaultSiz2.height) || 80, zIndex: 15 }, draggable: true, selectable: true, zIndex: 15 }; setNodes(nds => [...nds, newNode]); setShapeLibraryOpen(false); saveToHistory(); }, [handleNodeLabelChange, saveToHistory, screenToFlowPosition]); // Copy selected elements const copySelected = (0, _react.useCallback)(() => { if (selectedElements.nodes.length > 0 || selectedElements.edges.length > 0) { setClipboardData({ nodes: selectedElements.nodes, edges: selectedElements.edges }); } }, [selectedElements]); // Paste elements const pasteElements = (0, _react.useCallback)(() => { if (clipboardData) { const newNodes = clipboardData.nodes.map(node => ({ ...node, id: `${node.type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, position: { x: node.position.x + 50, y: node.position.y + 50 }, selected: false })); const newEdges = clipboardData.edges.map(edge => ({ ...edge, id: `edge-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, selected: false })); setNodes(nds => [...nds, ...newNodes]); setEdges(eds => [...eds, ...newEdges]); saveToHistory(); } }, [clipboardData, saveToHistory]); // Delete selected elements const deleteSelected = (0, _react.useCallback)(() => { if (selectedElements.nodes.length === 0 && selectedElements.edges.length === 0) return; const selectedNodeIds = selectedElements.nodes.map(node => node.id); const edgesToKeep = edges.filter(edge => !selectedNodeIds.includes(edge.source) && !selectedNodeIds.includes(edge.target) && !selectedElements.edges.map(e => e.id).includes(edge.id)); const nodesToKeep = nodes.filter(node => { return !selectedNodeIds.includes(node.id) && (!node.parentNode || !selectedNodeIds.includes(node.parentNode)); }); setNodes(nodesToKeep); setEdges(edgesToKeep); setSelectedElements({ nodes: [], edges: [] }); saveToHistory(); }, [selectedElements, nodes, edges, saveToHistory]); // Undo/Redo functions const undo = (0, _react.useCallback)(() => { setHistory(prev => { if (prev.past.length === 0) return prev; const newPresent = prev.past[prev.past.length - 1]; const newPast = prev.past.slice(0, prev.past.length - 1); setNodes(newPresent.nodes); setEdges(newPresent.edges); return { past: newPast, present: newPresent, future: [prev.present, ...prev.future] }; }); }, []); const redo = (0, _react.useCallback)(() => { setHistory(prev => { if (prev.future.length === 0) return prev; const newPresent = prev.future[0]; const newFuture = prev.future.slice(1); setNodes(newPresent.nodes); setEdges(newPresent.edges); return { past: [...prev.past, prev.present], present: newPresent, future: newFuture }; }); }, []); (0, _react.useEffect)(() => { const handleKeyDown = event => { if (event.target.closest('input, textarea, [contenteditable="true"]')) { return; // allow default behavior inside inputs and editable areas } if (event.ctrlKey || event.metaKey) { switch (event.key) { case 'c': event.preventDefault(); copySelected(); break; case 'v': event.preventDefault(); pasteElements(); break; case 'z': event.preventDefault(); if (event.shiftKey) { redo(); } else { undo(); } break; default: break; } } else if (event.key === 'Delete' || event.key === 'Backspace') { event.preventDefault(); deleteSelected(); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [copySelected, pasteElements, deleteSelected, undo, redo]); // Export and import functions using service layer const exportJSON = (0, _react.useCallback)(async () => { if (!serviceFactory || !isServiceInitialized) { console.warn('Service layer not initialized, falling back to old method'); // Fallback to old method const diagramData = { metadata: { name: 'Architecture Diagram', description: 'Exported architecture diagram', version: '1.0', exportDate: new Date().toISOString() }, containers: nodes.filter(node => node.type === 'container').map(container => { var _container$style3, _container$style4; return { id: container.id, label: container.data.label, position: container.position, size: { width: ((_container$style3 = container.style) === null || _container$style3 === void 0 ? void 0 : _container$style3.width) || 400, height: ((_container$style4 = container.style) === null || _container$style4 === void 0 ? void 0 : _container$style4.height) || 300 }, color: container.data.color, bgColor: container.data.bgColor, borderColor: container.data.borderColor, icon: container.data.icon, description: container.data.description, zIndex: container.zIndex || 1 }; }), nodes: nodes.filter(node => node.type !== 'container').map(node => { var _node$style3, _node$style4; return { id: node.id, label: node.data.label, type: node.type, position: node.position, parentContainer: node.parentNode, size: { width: ((_node$style3 = node.style) === null || _node$style3 === void 0 ? void 0 : _node$style3.width) || 150, height: ((_node$style4 = node.style) === null || _node$style4 === void 0 ? void 0 : _node$style4.height) || 80 }, color: node.data.color, borderColor: node.data.borderColor, icon: node.data.icon, description: node.data.description, zIndex: node.zIndex || 10 }; }), connections: edges.map(edge => { var _edge$data8, _edge$data9, _edge$data0, _edge$data1, _edge$style5, _edge$style6; return { id: edge.id, source: edge.source, target: edge.target, label: (_edge$data8 = edge.data) === null || _edge$data8 === void 0 ? void 0 : _edge$data8.label, type: edge.type, animated: edge.animated, description: (_edge$data9 = edge.data) === null || _edge$data9 === void 0 ? void 0 : _edge$data9.description, waypoints: (_edge$data0 = edge.data) === null || _edge$data0 === void 0 ? void 0 : _edge$data0.waypoints, markerStart: edge.markerStart, markerEnd: edge.markerEnd, intersection: (_edge$data1 = edge.data) === null || _edge$data1 === void 0 ? void 0 : _edge$data1.intersection, style: { strokeWidth: ((_edge$style5 = edge.style) === null || _edge$style5 === void 0 ? void 0 : _edge$style5.strokeWidth) || 2, strokeDasharray: (_edge$style6 = edge.style) === null || _edge$style6 === void 0 ? void 0 : _edge$style6.strokeDasharray }, zIndex: edge.zIndex || 5 }; }) }; const dataStr = JSON.stringify(diagramData, null, 2); const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr); const exportFileDefaultName = 'architecture-diagram.json'; const linkElement = document.createElement('a'); linkElement.setAttribute('href', dataUri); linkElement.setAttribute('download', exportFileDefaultName); linkElement.click(); return; } try { const diagramData = reactFlowToJson(nodes, edges); const result = await serviceFactory.exportDiagram(diagramData, 'json', { filename: 'architecture-diagram.json', prettyPrint: true }); if (result.success) { const linkElement = document.createElement('a'); linkElement.setAttribute('href', result.dataUri); linkElement.setAttribute('download', result.filename); linkElement.click(); } else { console.error('Export failed:', result.error); alert('Export failed. Please try again.'); } } catch (error) { console.error('Error exporting JSON:', error); alert('Error exporting diagram. Please try again.'); } }, [nodes, edges, serviceFactory, isServiceInitialized]); // Import from draw.io XML const importFromDrawioXML = (0, _react.useCallback)(() => { const inputElement = document.createElement('input'); inputElement.type = 'file'; inputElement.accept = '.drawio,.xml'; inputElement.onchange = event => { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = e => { try { const xmlContent = e.target.result; const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlContent, 'text/xml'); const cells = xmlDoc.querySelectorAll('mxCell'); const tempNodes = []; const tempEdges = []; cells.forEach(cell => { const id = cell.getAttribute('id'); if (id === '0' || id === '1') return; // skip root const value = cell.getAttribute('value') || ''; const style = cell.getAttribute('style') || ''; const vertex = cell.getAttribute('vertex'); const edge = cell.getAttribute('edge'); const geometry = cell.querySelector('mxGeometry'); if (!geometry) return; const x = parseFloat(geometry.getAttribute('x')) || 0; const y = parseFloat(geometry.getAttribute('y')) || 0; const width = parseFloat(geometry.getAttribute('width')) || 120; const height = parseFloat(geometry.getAttribute('height')) || 80; if (vertex) { tempNodes.push({ oldId: id, value, style, x, y, width, height, parent: cell.getAttribute('parent') }); } else if (edge) { tempEdges.push({ oldId: id, value, style, source: cell.getAttribute('source'), target: cell.getAttribute('target') }); } }); const cellIdMap = new Map(); const importedNodes = []; tempNodes.forEach(n => { const newId = `imported-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; cellIdMap.set(n.oldId, newId); let nodeType = 'component'; if (n.style.includes('swimlane')) nodeType = 'container';else if (n.style.includes('rhombus')) nodeType = 'diamond';else if (n.style.includes('ellipse')) nodeType = 'circle';else if (n.style.includes('hexagon')) nodeType = 'hexagon';else if (n.style.includes('triangle')) nodeType = 'triangle'; const fillColorMatch = n.style.match(/fillColor=([^;]+)/); const strokeColorMatch = n.style.match(/strokeColor=([^;]+)/); const fillColor = fillColorMatch ? fillColorMatch[1] : '#ffffff'; const strokeColor = strokeColorMatch ? strokeColorMatch[1] : '#000000'; const [firstLine, ...restLines] = n.value.split('\n'); importedNodes.push({ id: newId, type: nodeType, position: { x: n.x, y: n.y }, data: { label: firstLine, color: fillColor, borderColor: strokeColor,