schyma
Version:
JSON Schemas Visualizer React component
238 lines • 13.5 kB
JavaScript
"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