@inngest/workflow-kit
Version:
Durable visual workflows in your app, instantly
185 lines • 9.64 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Editor = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const react_2 = require("@xyflow/react");
const layout_1 = require("./layout");
const nodes_1 = require("./nodes");
const Provider_1 = require("./Provider");
const Editor = (props) => {
// Force the correct enum if the user passes in a string via non-TS usage.
const direction = props.direction === "right" ? "right" : "down";
const sidebarPosition = (0, Provider_1.useSidebarPosition)();
let className = sidebarPosition === "left" ? "wf-editor-left-sidebar" : "";
return ((0, jsx_runtime_1.jsx)(react_2.ReactFlowProvider, { children: (0, jsx_runtime_1.jsxs)("div", { className: `wf-editor ${className}`, children: [(0, jsx_runtime_1.jsx)(EditorUI, Object.assign({}, props, { direction: direction })), props.children] }) }));
};
exports.Editor = Editor;
const EditorUI = ({ direction = "down" }) => {
var _a, _b, _c, _d;
const { workflow, trigger, setSelectedNode, blankNode, setBlankNode } = (0, Provider_1.useProvider)();
const nodesInitialized = (0, react_2.useNodesInitialized)();
// Retain the initial node measurement for computing layout when Workflow is refreshed.
const [defaultNodeMeasure, setDefaultNodeMeasure] = (0, react_1.useState)(undefined);
// Store a reference to the parent div to compute layout
const ref = (0, react_1.useRef)(null);
const { nodes: initialNodes, edges: initialEdges } = (0, react_1.useMemo)(() => (0, layout_1.parseWorkflow)({ workflow, trigger }), []);
const [nodes, setNodes, onNodesChange] = (0, react_2.useNodesState)(initialNodes);
const [edges, setEdges, onEdgesChange] = (0, react_2.useEdgesState)(initialEdges);
// Lay out the nodes in the graph.
const layoutRect = (0, layout_1.useLayout)({
nodes: nodes,
edges: edges,
width: (_b = (_a = ref.current) === null || _a === void 0 ? void 0 : _a.offsetWidth) !== null && _b !== void 0 ? _b : 0,
height: (_d = (_c = ref.current) === null || _c === void 0 ? void 0 : _c.offsetHeight) !== null && _d !== void 0 ? _d : 0,
direction,
setNodes,
setEdges,
nodesInitialized,
defaultNodeMeasure,
});
useHandleBlankNode(nodes, edges, setNodes, setEdges, direction, defaultNodeMeasure);
useCenterGraph(layoutRect, ref);
// When the workflow changes, we need to re-layout the graph.
(0, react_1.useEffect)(() => {
const { nodes, edges } = (0, layout_1.parseWorkflow)({ workflow, trigger });
setNodes(nodes);
setEdges(edges);
}, [JSON.stringify((workflow === null || workflow === void 0 ? void 0 : workflow.edges) || [])]);
const nodeTypes = (0, react_1.useMemo)(() => ({
trigger: (node) => {
const { trigger } = node.data;
return (0, jsx_runtime_1.jsx)(nodes_1.TriggerNode, { trigger: trigger, node: node, direction: direction });
},
action: (node) => {
const { action } = node.data;
return (0, jsx_runtime_1.jsx)(nodes_1.ActionNode, { action: action, node: node, direction: direction });
},
blank: () => {
return (0, jsx_runtime_1.jsx)(nodes_1.BlankNode, { direction: direction });
}
}), [direction]);
return ((0, jsx_runtime_1.jsx)("div", { className: "wf-editor-parent", ref: ref, children: (0, jsx_runtime_1.jsx)(react_2.ReactFlow, { nodeTypes: nodeTypes, nodes: nodes, edges: edges, edgesFocusable: false, edgesReconnectable: false, onClick: (event) => {
// If the event target is not a node, set the selected node to undefined.
let target = event.target, isNode = false, isBlank = false;
const results = searchParents(target, ["wf-node", "wf-blank-node"], ref.current);
if (!results["wf-blank-node"] && !!blankNode) {
// Remove the blank node, as we've clicked elsewhere.
setBlankNode(undefined);
}
if (!results["wf-node"]) {
// Unselect any selected node, as we're not clicking on a node.
setSelectedNode(undefined);
}
}, onNodeClick: (event, node) => {
// Ensure we're not clicking the "add" handle. When we click
// the add handle, we automatically select the blank node. Selecting
// the node here would override that selection.
const results = searchParents(event.target, ["wf-add-handle"], ref.current);
if (results["wf-add-handle"]) {
return;
}
setSelectedNode(node);
event.preventDefault();
}, onNodesChange: (args) => {
var _a;
// Required to store .measured in nodes for computing layout.
onNodesChange(args);
if (!defaultNodeMeasure && args.length > 0 && ((_a = args[0]) === null || _a === void 0 ? void 0 : _a.type) === 'dimensions') {
const item = args[0]; // TODO: Fix types
setDefaultNodeMeasure(item === null || item === void 0 ? void 0 : item.dimensions);
}
}, proOptions: { hideAttribution: true } }, direction) }));
};
const useCenterGraph = (layoutRect, ref) => {
const flow = (0, react_2.useReactFlow)();
const nodesInitialized = (0, react_2.useNodesInitialized)();
const [centered, setCentered] = (0, react_1.useState)(false);
(0, react_1.useEffect)(() => {
var _a, _b, _c, _d, _e, _f, _g, _h;
if (!nodesInitialized) {
return;
}
if (centered) {
return;
}
// Only do this once per render.
setCentered(true);
// If the workflow is too big for the current viewport, zoom out.
// Otherwise, don't zoom in and center the current graph.
if (((layoutRect === null || layoutRect === void 0 ? void 0 : layoutRect.width) > ((_b = (_a = ref.current) === null || _a === void 0 ? void 0 : _a.offsetWidth) !== null && _b !== void 0 ? _b : 0))
|| ((layoutRect === null || layoutRect === void 0 ? void 0 : layoutRect.height) > ((_d = (_c = ref.current) === null || _c === void 0 ? void 0 : _c.offsetHeight) !== null && _d !== void 0 ? _d : 0))) {
flow.fitView();
return;
}
const w = (_f = (_e = ref.current) === null || _e === void 0 ? void 0 : _e.offsetWidth) !== null && _f !== void 0 ? _f : 0;
const h = (_h = (_g = ref.current) === null || _g === void 0 ? void 0 : _g.offsetHeight) !== null && _h !== void 0 ? _h : 0;
if (w === 0 || h === 0) {
return;
}
const fitRect = {
x: -1 * (w - layoutRect.width) / 2, // center the node rect in the viewport
y: -1 * (h - layoutRect.height) / 2,
width: w, // use viewport width
height: h, // use viewport height
};
flow.fitBounds(fitRect);
}, [nodesInitialized]);
};
// useHandleBlankNode is a hook that handles the logic for adding and removing
// blank nodes in a graph.
//
// Blank nodes are added when clicking the "AddHandle". This mutates the Provider
// state, which we then listen to here in order to manipulate react flow.
const useHandleBlankNode = (nodes, edges, setNodes, setEdges, direction, defaultNodeMeasure) => {
const { blankNode } = (0, Provider_1.useProvider)();
(0, react_1.useEffect)(() => {
var _a;
// We must manually update the react-flow nodes and edges as they're controlled
// via internal state.
if (blankNode) {
// Add the blank node and its edge.
// Ensure that the blank node's measured entry is filled. This fixes a layout bug shift.
// Measured is undefined when a node is being added, and is filled after react-flow renders
// the node for the first time.
if (!blankNode.measured) {
blankNode.measured = ((_a = nodes[0]) === null || _a === void 0 ? void 0 : _a.measured) || defaultNodeMeasure;
}
const newNodes = [...nodes, blankNode];
const newEdges = [...edges, {
id: `blank-node-edge`,
source: blankNode.data.parent.id,
target: '$blank',
type: 'smoothstep',
}];
// For each node, ensure there's a measured entry.
// Re-layout the graph prior to re-rendering.
const result = (0, layout_1.getLayoutedElements)(newNodes, newEdges, direction);
setNodes(result.nodes);
setEdges(result.edges);
}
else {
// Remove the blank node and its edge.
setNodes(nodes.filter((node) => node.id !== '$blank'));
setEdges(edges.filter((edge) => edge.target !== '$blank'));
}
}, [blankNode]);
};
// searchParents is a utility to search parent elements for given clasnames. It returns
// a record of whether each class was found.
const searchParents = (target, search, until) => {
const result = {};
while (target !== until) {
for (const key of search) {
if (target.classList.contains(key)) {
result[key] = true;
}
}
target = target.parentElement;
if (!target) {
break;
}
}
return result;
};
//# sourceMappingURL=Editor.js.map