@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
JavaScript
"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,