UNPKG

schyma

Version:

JSON Schemas Visualizer React component

238 lines 13.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const react_1 = tslib_1.__importStar(require("react")); const react_flow_smart_edge_1 = require("@tisoap/react-flow-smart-edge"); const reactflow_1 = require("reactflow"); const reusables_1 = require("../utils/reusables"); const types_1 = require("../types"); const dagreLayout_1 = require("../utils/dagreLayout"); const CustomNode_1 = tslib_1.__importDefault(require("./CustomNode")); const node_1 = require("../constants/node"); // Define node and edge types outside component to prevent re-renders const edgeTypes = { smart: react_flow_smart_edge_1.SmartBezierEdge, }; const nodeTypes = { schema: CustomNode_1.default, }; function Flow({ initialNode, nNodes, setnNodes, setCurrentNode, schema, isPanelCollapsed }) { const { nodes: layoutedNodes, edges: layoutedEdges } = (0, dagreLayout_1.getLayoutedElements)([initialNode], node_1.initialEdges); const [nodes, setNodes, onNodesChange] = (0, reactflow_1.useNodesState)(layoutedNodes); const [edges, setEdges, onEdgesChange] = (0, reactflow_1.useEdgesState)(layoutedEdges); const [hoveredCompositionNode, setHoveredCompositionNode] = (0, react_1.useState)(null); const { setCenter, getViewport, setViewport } = (0, reactflow_1.useReactFlow)(); const onInit = (0, react_1.useCallback)(() => { setTimeout(() => { if (!isPanelCollapsed) { const viewport = getViewport(); const panelWidth = window.innerWidth * 0.45; const screenOffset = panelWidth / 2; setViewport({ x: viewport.x - screenOffset, y: viewport.y, zoom: viewport.zoom }, { duration: 200 }); } }, 100); }, [isPanelCollapsed, getViewport, setViewport]); const styleCompositionEdges = (0, react_1.useMemo)(() => { // Skip styling for allOf: (since we are flattening allOf by default, there's no reason to style it) - all properties are just regular required properties if (!hoveredCompositionNode || hoveredCompositionNode.compositionType === types_1.CompositionType.AllOf) { return edges; } return edges.map((edge) => { var _a; if (edge.source === hoveredCompositionNode.nodeId) { const targetNode = nodes.find((n) => n.id === edge.target); const targetCompositionSource = (_a = targetNode === null || targetNode === void 0 ? void 0 : targetNode.data) === null || _a === void 0 ? void 0 : _a.compositionSource; if (targetCompositionSource === hoveredCompositionNode.compositionType) { return Object.assign(Object.assign({}, edge), { style: { stroke: node_1.compositionEdgeColors[hoveredCompositionNode.compositionType], strokeWidth: 2, }, animated: true }); } } return edge; }); }, [edges, hoveredCompositionNode, nodes]); const onConnect = (0, react_1.useCallback)((connection) => setEdges((eds) => (0, reactflow_1.addEdge)(Object.assign(Object.assign({}, connection), { type: reactflow_1.ConnectionLineType.SmoothStep, animated: true }), eds)), // eslint-disable-next-line react-hooks/exhaustive-deps []); const extractChildren = (props, parent) => tslib_1.__awaiter(this, void 0, void 0, function* () { const children = []; for (const prop in props) { const id = String(Math.floor(Math.random() * 1000000)); const propData = props[prop]; const compositionSource = propData._compositionSource; const directComposition = (0, reusables_1.getCompositionType)(propData); if (propData.$ref) { const res = yield (0, reusables_1.resolveRef)(propData.$ref, schema); children.push({ id, type: directComposition ? 'schema' : 'default', data: Object.assign(Object.assign(Object.assign(Object.assign({}, propData), { label: prop, parent: parent.id, relations: Object.assign(Object.assign({}, parent.relations), { [parent.id]: 'node' }) }), res), { children: [], compositionType: directComposition, compositionSource }), position: node_1.position, sourcePosition: reactflow_1.Position.Right, targetPosition: reactflow_1.Position.Left, }); } else { children.push({ id, type: directComposition ? 'schema' : 'default', data: Object.assign(Object.assign({}, propData), { label: prop, id, parent: parent.id, relations: Object.assign(Object.assign({}, parent.relations), { [parent.id]: 'node' }), children: [], compositionType: directComposition, compositionSource }), position: node_1.position, sourcePosition: reactflow_1.Position.Right, targetPosition: reactflow_1.Position.Left, }); } } return children; }); const fetchInitialChildren = () => tslib_1.__awaiter(this, void 0, void 0, function* () { const newNodes = []; const properties = initialNode.data.properties; const children = yield extractChildren(properties, initialNode); const nodeType = initialNode.data.compositionType ? 'schema' : 'input'; newNodes.push({ id: initialNode.id, type: nodeType, data: { children, label: initialNode.data.label, description: initialNode.data.description, properties: initialNode.data.properties, relations: initialNode.data.relations, compositionType: initialNode.data.compositionType, isRoot: true, }, position: { x: 0, y: 0 }, sourcePosition: reactflow_1.Position.Right, targetPosition: reactflow_1.Position.Left, }); setNodes(newNodes); }); (0, react_1.useEffect)(() => { fetchInitialChildren(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Adjust viewport when panel collapses/expands (0, react_1.useEffect)(() => { const viewport = getViewport(); const panelWidth = window.innerWidth * 0.45; const screenOffset = panelWidth / 2; if (isPanelCollapsed) { setViewport({ x: viewport.x + screenOffset, y: viewport.y, zoom: viewport.zoom }, { duration: 200 }); } else { setViewport({ x: viewport.x - screenOffset, y: viewport.y, zoom: viewport.zoom }, { duration: 200 }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPanelCollapsed]); const focusNode = (children, zoom) => { if (children.length === 0) return; let middleChild = children[Math.floor(children.length / 2)]; const middleChildWithLatestPosition = nodes.filter((a) => a.id == middleChild.id)[0]; if (middleChildWithLatestPosition) { middleChild = middleChildWithLatestPosition; } let targetX = middleChild.position.x; if (!isPanelCollapsed) { const panelWidth = window.innerWidth * 0.45; const offsetInFlowCoords = panelWidth / 2 / zoom; targetX = targetX + offsetInFlowCoords; } setCenter(targetX, middleChild.position.y, { zoom, duration: 1000 }); }; const nodeClick = (_event, node) => tslib_1.__awaiter(this, void 0, void 0, function* () { const findChildren = nodes.filter((item) => { var _a; return ((_a = item === null || item === void 0 ? void 0 : item.data) === null || _a === void 0 ? void 0 : _a.parent) === node.id; }); if (!findChildren.length) { const itemChildren = node.data.children; const newEdges = [ ...edges, ...itemChildren.map((item) => { var _a; return { id: String(Math.floor(Math.random() * 1000000)), source: (_a = item === null || item === void 0 ? void 0 : item.data) === null || _a === void 0 ? void 0 : _a.parent, target: item === null || item === void 0 ? void 0 : item.id, markerEnd: { type: reactflow_1.MarkerType.ArrowClosed, }, }; }), ]; //TODO: Fix nodes type error const newNodes = nodes.concat(itemChildren); const { nodes: layoutedNodes, edges: layoutedEdges } = (0, dagreLayout_1.getLayoutedElements)(newNodes, newEdges, 'LR'); setNodes([...layoutedNodes]); setEdges([...layoutedEdges]); if (itemChildren.length > 0) { focusNode(itemChildren, 0.9); } } else { const newNodes = (0, reusables_1.removeElementsByParent)(nodes, node.id); const newEdges = (0, reusables_1.removeEdgesByParent)(edges, node.id); const { nodes: layoutedNodes, edges: layoutedEdges } = (0, dagreLayout_1.getLayoutedElements)(newNodes, newEdges, 'LR'); setNodes([...layoutedNodes]); setEdges([...layoutedEdges]); focusNode([node], 0.9); } }); function handleMouseEnter(_e, node) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (!nNodes[node.id]) { const itemChildren = []; const nodeChildren = node.data.children; yield Promise.all(nodeChildren.map((item) => tslib_1.__awaiter(this, void 0, void 0, function* () { let children = []; const label = item.data.label; const extractProps = (0, reusables_1.propMerge)(item.data, label); const nestedComposition = extractProps._nestedComposition; delete extractProps._nestedComposition; if (Object.keys(extractProps).length > 0) { const res = yield extractChildren(extractProps, item); children = res; } const relations = Object.assign(Object.assign({}, node.data.relations), item.data.relations); // Check for direct composition or nested composition (from items/additionalProperties) const directComposition = (0, reusables_1.getCompositionType)(item.data); const compositionType = directComposition || nestedComposition || null; // Get composition source tag if this child came from a composition const compositionSource = item.data._compositionSource; // Use custom schema node type if node has composition, otherwise use default types const nodeType = compositionType ? 'schema' : (children === null || children === void 0 ? void 0 : children.length) > 0 ? 'default' : 'output'; itemChildren.push({ id: item.id, type: nodeType, data: Object.assign(Object.assign({}, item.data), { label: `${item.data.label}`, children: children, relations: relations, compositionType, compositionSource }), position: node_1.position, sourcePosition: reactflow_1.Position.Right, targetPosition: reactflow_1.Position.Left, }); }))); node.data.children = itemChildren; nNodes[node.id] = node; setnNodes(nNodes); } const nodeData = node.data; if (nodeData.compositionType) { setHoveredCompositionNode({ nodeId: node.id, compositionType: nodeData.compositionType, }); } setCurrentNode(node); }); } function handleMouseLeave() { setHoveredCompositionNode(null); } return (react_1.default.createElement(reactflow_1.ReactFlow, { nodes: nodes, edges: styleCompositionEdges, edgeTypes: edgeTypes, nodeTypes: nodeTypes, onNodesChange: onNodesChange, connectionLineType: reactflow_1.ConnectionLineType.SmoothStep, onEdgesChange: onEdgesChange, onConnect: onConnect, onNodeMouseEnter: handleMouseEnter, onNodeMouseLeave: handleMouseLeave, onNodeClick: nodeClick, onInit: onInit, fitView: true, defaultViewport: { x: 1, y: 1, zoom: 0.9 } }, react_1.default.createElement(reactflow_1.MiniMap, null), react_1.default.createElement(reactflow_1.Controls, null), react_1.default.createElement(reactflow_1.Background, null))); } exports.default = ({ setCurrentNode, setnNodes, nNodes, initialNode, schema, isPanelCollapsed }) => (react_1.default.createElement(reactflow_1.ReactFlowProvider, null, react_1.default.createElement(Flow, { setnNodes: setnNodes, nNodes: nNodes, setCurrentNode: setCurrentNode, initialNode: initialNode, schema: schema, isPanelCollapsed: isPanelCollapsed }))); //# sourceMappingURL=Nodes.js.map