@bian-womp/spark-workbench 0.3.87 → 0.3.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cjs/index.cjs +72 -103
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/esm/index.js +72 -103
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/src/adapters/cli/index.d.ts +22 -0
- package/lib/src/adapters/cli/index.d.ts.map +1 -0
- package/lib/src/adapters/cli/index.js +50 -0
- package/lib/src/adapters/cli/index.js.map +1 -0
- package/lib/src/core/AbstractWorkbench.d.ts +40 -0
- package/lib/src/core/AbstractWorkbench.d.ts.map +1 -0
- package/lib/src/core/AbstractWorkbench.js +15 -0
- package/lib/src/core/AbstractWorkbench.js.map +1 -0
- package/lib/src/core/InMemoryWorkbench.d.ts +304 -0
- package/lib/src/core/InMemoryWorkbench.d.ts.map +1 -0
- package/lib/src/core/InMemoryWorkbench.js +1016 -0
- package/lib/src/core/InMemoryWorkbench.js.map +1 -0
- package/lib/src/core/contracts.d.ts +172 -0
- package/lib/src/core/contracts.d.ts.map +1 -0
- package/lib/src/core/contracts.js +2 -0
- package/lib/src/core/contracts.js.map +1 -0
- package/lib/src/core/ui-extensions.d.ts +85 -0
- package/lib/src/core/ui-extensions.d.ts.map +1 -0
- package/lib/src/core/ui-extensions.js +111 -0
- package/lib/src/core/ui-extensions.js.map +1 -0
- package/lib/src/examples/cli.d.ts +2 -0
- package/lib/src/examples/cli.d.ts.map +1 -0
- package/lib/src/examples/cli.js +244 -0
- package/lib/src/examples/cli.js.map +1 -0
- package/lib/src/examples/reactflow/App.d.ts +2 -0
- package/lib/src/examples/reactflow/App.d.ts.map +1 -0
- package/lib/src/examples/reactflow/App.js +20 -0
- package/lib/src/examples/reactflow/App.js.map +1 -0
- package/lib/src/index.d.ts +30 -0
- package/lib/src/index.d.ts.map +1 -0
- package/lib/src/index.js +30 -0
- package/lib/src/index.js.map +1 -0
- package/lib/src/misc/DebugEvents.d.ts +7 -0
- package/lib/src/misc/DebugEvents.d.ts.map +1 -0
- package/lib/src/misc/DebugEvents.js +51 -0
- package/lib/src/misc/DebugEvents.js.map +1 -0
- package/lib/src/misc/DefaultEdge.d.ts +5 -0
- package/lib/src/misc/DefaultEdge.d.ts.map +1 -0
- package/lib/src/misc/DefaultEdge.js +15 -0
- package/lib/src/misc/DefaultEdge.js.map +1 -0
- package/lib/src/misc/DefaultNode.d.ts +5 -0
- package/lib/src/misc/DefaultNode.d.ts.map +1 -0
- package/lib/src/misc/DefaultNode.js +25 -0
- package/lib/src/misc/DefaultNode.js.map +1 -0
- package/lib/src/misc/DefaultNodeContent.d.ts +4 -0
- package/lib/src/misc/DefaultNodeContent.d.ts.map +1 -0
- package/lib/src/misc/DefaultNodeContent.js +58 -0
- package/lib/src/misc/DefaultNodeContent.js.map +1 -0
- package/lib/src/misc/DefaultNodeHeader.d.ts +13 -0
- package/lib/src/misc/DefaultNodeHeader.d.ts.map +1 -0
- package/lib/src/misc/DefaultNodeHeader.js +78 -0
- package/lib/src/misc/DefaultNodeHeader.js.map +1 -0
- package/lib/src/misc/Inspector.d.ts +12 -0
- package/lib/src/misc/Inspector.d.ts.map +1 -0
- package/lib/src/misc/Inspector.js +253 -0
- package/lib/src/misc/Inspector.js.map +1 -0
- package/lib/src/misc/IssueBadge.d.ts +7 -0
- package/lib/src/misc/IssueBadge.d.ts.map +1 -0
- package/lib/src/misc/IssueBadge.js +7 -0
- package/lib/src/misc/IssueBadge.js.map +1 -0
- package/lib/src/misc/KeyboardShortcutToast.d.ts +16 -0
- package/lib/src/misc/KeyboardShortcutToast.d.ts.map +1 -0
- package/lib/src/misc/KeyboardShortcutToast.js +40 -0
- package/lib/src/misc/KeyboardShortcutToast.js.map +1 -0
- package/lib/src/misc/NodeHandles.d.ts +18 -0
- package/lib/src/misc/NodeHandles.d.ts.map +1 -0
- package/lib/src/misc/NodeHandles.js +67 -0
- package/lib/src/misc/NodeHandles.js.map +1 -0
- package/lib/src/misc/SelectionActiveSync.d.ts +10 -0
- package/lib/src/misc/SelectionActiveSync.d.ts.map +1 -0
- package/lib/src/misc/SelectionActiveSync.js +21 -0
- package/lib/src/misc/SelectionActiveSync.js.map +1 -0
- package/lib/src/misc/WorkbenchCanvas.d.ts +23 -0
- package/lib/src/misc/WorkbenchCanvas.d.ts.map +1 -0
- package/lib/src/misc/WorkbenchCanvas.js +669 -0
- package/lib/src/misc/WorkbenchCanvas.js.map +1 -0
- package/lib/src/misc/WorkbenchStudio.d.ts +43 -0
- package/lib/src/misc/WorkbenchStudio.d.ts.map +1 -0
- package/lib/src/misc/WorkbenchStudio.js +463 -0
- package/lib/src/misc/WorkbenchStudio.js.map +1 -0
- package/lib/src/misc/constants.d.ts +4 -0
- package/lib/src/misc/constants.d.ts.map +1 -0
- package/lib/src/misc/constants.js +5 -0
- package/lib/src/misc/constants.js.map +1 -0
- package/lib/src/misc/context/WorkbenchContext.d.ts +133 -0
- package/lib/src/misc/context/WorkbenchContext.d.ts.map +1 -0
- package/lib/src/misc/context/WorkbenchContext.js +9 -0
- package/lib/src/misc/context/WorkbenchContext.js.map +1 -0
- package/lib/src/misc/context/WorkbenchContext.provider.d.ts +12 -0
- package/lib/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -0
- package/lib/src/misc/context/WorkbenchContext.provider.js +995 -0
- package/lib/src/misc/context/WorkbenchContext.provider.js.map +1 -0
- package/lib/src/misc/context-menu/ContextMenuButton.d.ts +8 -0
- package/lib/src/misc/context-menu/ContextMenuButton.d.ts.map +1 -0
- package/lib/src/misc/context-menu/ContextMenuButton.js +10 -0
- package/lib/src/misc/context-menu/ContextMenuButton.js.map +1 -0
- package/lib/src/misc/context-menu/ContextMenuHandlers.d.ts +85 -0
- package/lib/src/misc/context-menu/ContextMenuHandlers.d.ts.map +1 -0
- package/lib/src/misc/context-menu/ContextMenuHandlers.js +2 -0
- package/lib/src/misc/context-menu/ContextMenuHandlers.js.map +1 -0
- package/lib/src/misc/context-menu/ContextMenuHelpers.d.ts +45 -0
- package/lib/src/misc/context-menu/ContextMenuHelpers.d.ts.map +1 -0
- package/lib/src/misc/context-menu/ContextMenuHelpers.js +182 -0
- package/lib/src/misc/context-menu/ContextMenuHelpers.js.map +1 -0
- package/lib/src/misc/context-menu/DefaultContextMenu.d.ts +3 -0
- package/lib/src/misc/context-menu/DefaultContextMenu.d.ts.map +1 -0
- package/lib/src/misc/context-menu/DefaultContextMenu.js +111 -0
- package/lib/src/misc/context-menu/DefaultContextMenu.js.map +1 -0
- package/lib/src/misc/context-menu/NodeContextMenu.d.ts +3 -0
- package/lib/src/misc/context-menu/NodeContextMenu.d.ts.map +1 -0
- package/lib/src/misc/context-menu/NodeContextMenu.js +51 -0
- package/lib/src/misc/context-menu/NodeContextMenu.js.map +1 -0
- package/lib/src/misc/context-menu/SelectionContextMenu.d.ts +3 -0
- package/lib/src/misc/context-menu/SelectionContextMenu.d.ts.map +1 -0
- package/lib/src/misc/context-menu/SelectionContextMenu.js +47 -0
- package/lib/src/misc/context-menu/SelectionContextMenu.js.map +1 -0
- package/lib/src/misc/hooks.d.ts +18 -0
- package/lib/src/misc/hooks.d.ts.map +1 -0
- package/lib/src/misc/hooks.js +275 -0
- package/lib/src/misc/hooks.js.map +1 -0
- package/lib/src/misc/layout.d.ts +117 -0
- package/lib/src/misc/layout.d.ts.map +1 -0
- package/lib/src/misc/layout.js +205 -0
- package/lib/src/misc/layout.js.map +1 -0
- package/lib/src/misc/load.d.ts +5 -0
- package/lib/src/misc/load.d.ts.map +1 -0
- package/lib/src/misc/load.js +106 -0
- package/lib/src/misc/load.js.map +1 -0
- package/lib/src/misc/mapping.d.ts +128 -0
- package/lib/src/misc/mapping.d.ts.map +1 -0
- package/lib/src/misc/mapping.js +270 -0
- package/lib/src/misc/mapping.js.map +1 -0
- package/lib/src/misc/merge-utils.d.ts +12 -0
- package/lib/src/misc/merge-utils.d.ts.map +1 -0
- package/lib/src/misc/merge-utils.js +51 -0
- package/lib/src/misc/merge-utils.js.map +1 -0
- package/lib/src/misc/thumbnail-utils.d.ts +53 -0
- package/lib/src/misc/thumbnail-utils.d.ts.map +1 -0
- package/lib/src/misc/thumbnail-utils.js +629 -0
- package/lib/src/misc/thumbnail-utils.js.map +1 -0
- package/lib/src/misc/types.d.ts +18 -0
- package/lib/src/misc/types.d.ts.map +1 -0
- package/lib/src/misc/types.js +2 -0
- package/lib/src/misc/types.js.map +1 -0
- package/lib/src/misc/value.d.ts +16 -0
- package/lib/src/misc/value.d.ts.map +1 -0
- package/lib/src/misc/value.js +114 -0
- package/lib/src/misc/value.js.map +1 -0
- package/lib/src/misc/viewport-utils.d.ts +6 -0
- package/lib/src/misc/viewport-utils.d.ts.map +1 -0
- package/lib/src/misc/viewport-utils.js +18 -0
- package/lib/src/misc/viewport-utils.js.map +1 -0
- package/lib/src/runtime/AbstractGraphRunner.d.ts +61 -0
- package/lib/src/runtime/AbstractGraphRunner.d.ts.map +1 -0
- package/lib/src/runtime/AbstractGraphRunner.js +63 -0
- package/lib/src/runtime/AbstractGraphRunner.js.map +1 -0
- package/lib/src/runtime/IGraphRunner.d.ts +100 -0
- package/lib/src/runtime/IGraphRunner.d.ts.map +1 -0
- package/lib/src/runtime/IGraphRunner.js +2 -0
- package/lib/src/runtime/IGraphRunner.js.map +1 -0
- package/lib/src/runtime/LocalGraphRunner.d.ts +60 -0
- package/lib/src/runtime/LocalGraphRunner.d.ts.map +1 -0
- package/lib/src/runtime/LocalGraphRunner.js +294 -0
- package/lib/src/runtime/LocalGraphRunner.js.map +1 -0
- package/lib/src/runtime/RemoteGraphRunner.d.ts +109 -0
- package/lib/src/runtime/RemoteGraphRunner.d.ts.map +1 -0
- package/lib/src/runtime/RemoteGraphRunner.js +696 -0
- package/lib/src/runtime/RemoteGraphRunner.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useMemo, useState, useRef, useImperativeHandle, useCallback, useEffect } from "react";
|
|
3
|
+
import { ReactFlow, Background, Controls, MiniMap, BackgroundVariant, ReactFlowProvider, } from "@xyflow/react";
|
|
4
|
+
import lod from "lodash";
|
|
5
|
+
import { toReactFlow } from "./mapping";
|
|
6
|
+
import { useWorkbenchBridge } from "./hooks";
|
|
7
|
+
import { DefaultNode } from "./DefaultNode";
|
|
8
|
+
import { DefaultEdge } from "./DefaultEdge";
|
|
9
|
+
import { DefaultContextMenu } from "./context-menu/DefaultContextMenu";
|
|
10
|
+
import { NodeContextMenu } from "./context-menu/NodeContextMenu";
|
|
11
|
+
import { SelectionContextMenu } from "./context-menu/SelectionContextMenu";
|
|
12
|
+
import { useWorkbenchContext } from "./context/WorkbenchContext";
|
|
13
|
+
import { createNodeContextMenuHandlers, createSelectionContextMenuHandlers, createDefaultContextMenuHandlers, getBakeableOutputs, } from "./context-menu/ContextMenuHelpers";
|
|
14
|
+
import { KeyboardShortcutToast, useKeyboardShortcutToast } from "./KeyboardShortcutToast";
|
|
15
|
+
import SelectionActiveSync from "./SelectionActiveSync";
|
|
16
|
+
export const WorkbenchCanvasComponent = React.forwardRef((props, ref) => {
|
|
17
|
+
const { showValues, toString, toElement, getDefaultNodeSize, reactFlowProps, children } = props;
|
|
18
|
+
const { wb, handlesMap, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, registryVersion, runner, overrides, runNode, runFromHere, runMode, } = useWorkbenchContext();
|
|
19
|
+
const nodeValidation = validationByNode;
|
|
20
|
+
const edgeValidation = validationByEdge.errors;
|
|
21
|
+
const [historyState, setHistoryState] = useState(wb.getHistory());
|
|
22
|
+
const prevNodesRef = useRef([]);
|
|
23
|
+
const prevEdgesRef = useRef([]);
|
|
24
|
+
function retainStabilityById(prev, next, isSame) {
|
|
25
|
+
if (prev.length === 0)
|
|
26
|
+
return next;
|
|
27
|
+
const map = new Map();
|
|
28
|
+
for (const p of prev)
|
|
29
|
+
map.set(p.id, p);
|
|
30
|
+
const out = new Array(next.length);
|
|
31
|
+
for (let i = 0; i < next.length; i++) {
|
|
32
|
+
const n = next[i];
|
|
33
|
+
const p = map.get(n.id);
|
|
34
|
+
out[i] = p && isSame(p, n) ? p : n;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
const isSameNode = (a, b) => {
|
|
39
|
+
// Compare the parts that affect rendering
|
|
40
|
+
const pick = (n) => ({
|
|
41
|
+
position: n.position,
|
|
42
|
+
type: n.type,
|
|
43
|
+
selected: n.selected,
|
|
44
|
+
measured: n.measured,
|
|
45
|
+
data: n.data && {
|
|
46
|
+
typeId: n.data.typeId,
|
|
47
|
+
handles: n.data.handles,
|
|
48
|
+
inputHandles: n.data.inputHandles,
|
|
49
|
+
outputHandles: n.data.outputHandles,
|
|
50
|
+
showValues: n.data.showValues,
|
|
51
|
+
inputValues: n.data.inputValues,
|
|
52
|
+
outputValues: n.data.outputValues,
|
|
53
|
+
status: n.data.status,
|
|
54
|
+
validation: n.data.validation,
|
|
55
|
+
inputConnected: n.data.inputConnected,
|
|
56
|
+
custom: n.data.custom,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
return lod.isEqual(pick(a), pick(b));
|
|
60
|
+
};
|
|
61
|
+
const isSameEdge = (a, b) => {
|
|
62
|
+
const pick = (e) => ({
|
|
63
|
+
source: e.source,
|
|
64
|
+
target: e.target,
|
|
65
|
+
sourceHandle: e.sourceHandle,
|
|
66
|
+
targetHandle: e.targetHandle,
|
|
67
|
+
selected: e.selected,
|
|
68
|
+
animated: e.animated,
|
|
69
|
+
style: e.style,
|
|
70
|
+
label: e.label,
|
|
71
|
+
type: e.type,
|
|
72
|
+
data: e.data && {
|
|
73
|
+
edgeTypeId: e.data.edgeTypeId,
|
|
74
|
+
isRunning: e.data.isRunning,
|
|
75
|
+
hasError: e.data.hasError,
|
|
76
|
+
isInvalid: e.data.isInvalid,
|
|
77
|
+
isMissing: e.data.isMissing,
|
|
78
|
+
custom: e.data.custom,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
return lod.isEqual(pick(a), pick(b));
|
|
82
|
+
};
|
|
83
|
+
// Expose imperative API
|
|
84
|
+
const rfInstanceRef = useRef(null);
|
|
85
|
+
const containerRef = useRef(null);
|
|
86
|
+
useImperativeHandle(ref, () => ({
|
|
87
|
+
fitView: () => {
|
|
88
|
+
try {
|
|
89
|
+
rfInstanceRef.current?.fitView();
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
console.warn("Failed to fit view", err);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
setViewport: (viewport) => {
|
|
96
|
+
if (rfInstanceRef.current) {
|
|
97
|
+
rfInstanceRef.current.setViewport(lod.clone(viewport));
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
}), []);
|
|
101
|
+
const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete } = useWorkbenchBridge(wb, handlesMap);
|
|
102
|
+
const ui = useMemo(() => wb.getUI(), [wb, uiVersion]);
|
|
103
|
+
const { nodeTypes, resolveNodeType } = useMemo(() => {
|
|
104
|
+
// Build nodeTypes map using UI extension registry
|
|
105
|
+
const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
|
|
106
|
+
const allNodeRenderers = ui.getAllNodeRenderers();
|
|
107
|
+
for (const typeId of Object.keys(allNodeRenderers)) {
|
|
108
|
+
custom.set(typeId, allNodeRenderers[typeId]);
|
|
109
|
+
}
|
|
110
|
+
const types = {
|
|
111
|
+
"spark-default": DefaultNode,
|
|
112
|
+
default: DefaultNode,
|
|
113
|
+
};
|
|
114
|
+
for (const [typeId, comp] of custom.entries()) {
|
|
115
|
+
types[`spark-${typeId}`] = comp;
|
|
116
|
+
}
|
|
117
|
+
const resolver = (nodeTypeId) => (custom.has(nodeTypeId) ? `spark-${nodeTypeId}` : "spark-default");
|
|
118
|
+
return { nodeTypes: types, resolveNodeType: resolver };
|
|
119
|
+
// Include uiVersion to recompute when custom renderers are registered
|
|
120
|
+
// Include registryVersion to recompute when registry enums/types change
|
|
121
|
+
}, [wb, wb.registry, registryVersion, ui, uiVersion]);
|
|
122
|
+
const edgeTypes = useMemo(() => {
|
|
123
|
+
// Use default edge renderer override if registered, otherwise use DefaultEdge
|
|
124
|
+
const customEdgeRenderer = ui.getEdgeRenderer();
|
|
125
|
+
return { default: customEdgeRenderer || DefaultEdge };
|
|
126
|
+
}, [uiVersion, ui]);
|
|
127
|
+
// Track selection state to prevent unnecessary re-renders
|
|
128
|
+
const [selection, setSelection] = useState(() => wb.getSelection());
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const off = wb.on("selectionChanged", (sel) => {
|
|
131
|
+
setSelection({ nodes: sel.nodes, edges: sel.edges });
|
|
132
|
+
});
|
|
133
|
+
return () => off();
|
|
134
|
+
}, [wb]);
|
|
135
|
+
const rfData = useMemo(() => {
|
|
136
|
+
const out = toReactFlow(wb.def, wb.getPositions(), wb.getSizes(), wb.registry, {
|
|
137
|
+
showValues,
|
|
138
|
+
inputs: inputsMap,
|
|
139
|
+
inputDefaults: inputDefaultsMap,
|
|
140
|
+
outputs: outputsMap,
|
|
141
|
+
handles: handlesMap,
|
|
142
|
+
resolveNodeType,
|
|
143
|
+
toString,
|
|
144
|
+
toElement,
|
|
145
|
+
nodeStatus,
|
|
146
|
+
edgeStatus,
|
|
147
|
+
nodeValidation,
|
|
148
|
+
edgeValidation,
|
|
149
|
+
selectedNodeIds: new Set(selection.nodes),
|
|
150
|
+
selectedEdgeIds: new Set(selection.edges),
|
|
151
|
+
getDefaultNodeSize,
|
|
152
|
+
ui,
|
|
153
|
+
customData: wb.getCustomData(),
|
|
154
|
+
});
|
|
155
|
+
// Retain references for unchanged items
|
|
156
|
+
const stableNodes = retainStabilityById(prevNodesRef.current, out.nodes, isSameNode);
|
|
157
|
+
const stableEdges = retainStabilityById(prevEdgesRef.current, out.edges, isSameEdge);
|
|
158
|
+
// Debug: log updates/additions/removals (use value equality, not reference)
|
|
159
|
+
try {
|
|
160
|
+
const prevNodeIds = new Set(prevNodesRef.current.map((n) => n.id));
|
|
161
|
+
const nextNodeIds = new Set(out.nodes.map((n) => n.id));
|
|
162
|
+
const addedNodeIds = out.nodes.filter((n) => !prevNodeIds.has(n.id)).map((n) => n.id);
|
|
163
|
+
const removedNodeIds = prevNodesRef.current.filter((n) => !nextNodeIds.has(n.id)).map((n) => n.id);
|
|
164
|
+
const prevNodeMap = new Map(prevNodesRef.current.map((n) => [n.id, n]));
|
|
165
|
+
const changedNodeIds = out.nodes
|
|
166
|
+
.filter((n) => {
|
|
167
|
+
const p = prevNodeMap.get(n.id);
|
|
168
|
+
return p ? !isSameNode(p, n) : false;
|
|
169
|
+
})
|
|
170
|
+
.map((n) => n.id);
|
|
171
|
+
// Detect handle updates (ids/length changes) for targeted debug
|
|
172
|
+
const toIds = (arr) => Array.isArray(arr)
|
|
173
|
+
? arr.map((h) => (h && typeof h === "object" && "id" in h ? String(h.id) : "")).filter(Boolean)
|
|
174
|
+
: [];
|
|
175
|
+
const handlesEqual = (a, b) => {
|
|
176
|
+
const aIds = toIds(a);
|
|
177
|
+
const bIds = toIds(b);
|
|
178
|
+
if (aIds.length !== bIds.length)
|
|
179
|
+
return false;
|
|
180
|
+
for (let i = 0; i < aIds.length; i++) {
|
|
181
|
+
if (aIds[i] !== bIds[i])
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
};
|
|
186
|
+
const handleChanged = out.nodes
|
|
187
|
+
.filter((n) => {
|
|
188
|
+
const p = prevNodeMap.get(n.id);
|
|
189
|
+
if (!p)
|
|
190
|
+
return false;
|
|
191
|
+
const inChanged = !handlesEqual(p.data?.inputHandles, n.data?.inputHandles);
|
|
192
|
+
const outChanged = !handlesEqual(p.data?.outputHandles, n.data?.outputHandles);
|
|
193
|
+
return inChanged || outChanged;
|
|
194
|
+
})
|
|
195
|
+
.map((n) => n.id);
|
|
196
|
+
const prevEdgeIds = new Set(prevEdgesRef.current.map((e) => e.id));
|
|
197
|
+
const nextEdgeIds = new Set(out.edges.map((e) => e.id));
|
|
198
|
+
const addedEdgeIds = out.edges.filter((e) => !prevEdgeIds.has(e.id)).map((e) => e.id);
|
|
199
|
+
const removedEdgeIds = prevEdgesRef.current.filter((e) => !nextEdgeIds.has(e.id)).map((e) => e.id);
|
|
200
|
+
const prevEdgeMap = new Map(prevEdgesRef.current.map((e) => [e.id, e]));
|
|
201
|
+
const changedEdgeIds = out.edges
|
|
202
|
+
.filter((e) => {
|
|
203
|
+
const p = prevEdgeMap.get(e.id);
|
|
204
|
+
return p ? !isSameEdge(p, e) : false;
|
|
205
|
+
})
|
|
206
|
+
.map((e) => e.id);
|
|
207
|
+
if (addedNodeIds.length || removedNodeIds.length || changedNodeIds.length || handleChanged.length) {
|
|
208
|
+
// eslint-disable-next-line no-console
|
|
209
|
+
console.debug("[WorkbenchCanvas] node updates", {
|
|
210
|
+
added: addedNodeIds,
|
|
211
|
+
removed: removedNodeIds,
|
|
212
|
+
changed: changedNodeIds,
|
|
213
|
+
handleChanged,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (addedEdgeIds.length || removedEdgeIds.length || changedEdgeIds.length) {
|
|
217
|
+
// eslint-disable-next-line no-console
|
|
218
|
+
console.debug("[WorkbenchCanvas] edge updates", {
|
|
219
|
+
added: addedEdgeIds,
|
|
220
|
+
removed: removedEdgeIds,
|
|
221
|
+
changed: changedEdgeIds,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch { }
|
|
226
|
+
prevNodesRef.current = stableNodes;
|
|
227
|
+
prevEdgesRef.current = stableEdges;
|
|
228
|
+
return { nodes: stableNodes, edges: stableEdges };
|
|
229
|
+
}, [
|
|
230
|
+
showValues,
|
|
231
|
+
inputsMap,
|
|
232
|
+
inputDefaultsMap,
|
|
233
|
+
outputsMap,
|
|
234
|
+
handlesMap,
|
|
235
|
+
valuesTick,
|
|
236
|
+
toString,
|
|
237
|
+
toElement,
|
|
238
|
+
nodeStatus,
|
|
239
|
+
edgeStatus,
|
|
240
|
+
nodeValidation,
|
|
241
|
+
edgeValidation,
|
|
242
|
+
resolveNodeType,
|
|
243
|
+
selection,
|
|
244
|
+
ui,
|
|
245
|
+
getDefaultNodeSize,
|
|
246
|
+
wb,
|
|
247
|
+
uiVersion,
|
|
248
|
+
]);
|
|
249
|
+
const [menuState, setMenuState] = useState(null);
|
|
250
|
+
// Compute the rectangular screen-space bounds of the current selection
|
|
251
|
+
const getSelectionScreenBounds = () => {
|
|
252
|
+
if (typeof document === "undefined")
|
|
253
|
+
return null;
|
|
254
|
+
const selection = wb.getSelection();
|
|
255
|
+
if (!selection.nodes.length)
|
|
256
|
+
return null;
|
|
257
|
+
let bounds = null;
|
|
258
|
+
for (const nodeId of selection.nodes) {
|
|
259
|
+
const el = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`);
|
|
260
|
+
if (!el)
|
|
261
|
+
continue;
|
|
262
|
+
const rect = el.getBoundingClientRect();
|
|
263
|
+
if (!bounds) {
|
|
264
|
+
bounds = {
|
|
265
|
+
left: rect.left,
|
|
266
|
+
top: rect.top,
|
|
267
|
+
right: rect.right,
|
|
268
|
+
bottom: rect.bottom,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
bounds.left = Math.min(bounds.left, rect.left);
|
|
273
|
+
bounds.top = Math.min(bounds.top, rect.top);
|
|
274
|
+
bounds.right = Math.max(bounds.right, rect.right);
|
|
275
|
+
bounds.bottom = Math.max(bounds.bottom, rect.bottom);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return bounds;
|
|
279
|
+
};
|
|
280
|
+
const onContextMenu = (e) => {
|
|
281
|
+
e.preventDefault();
|
|
282
|
+
const selection = wb.getSelection();
|
|
283
|
+
const isSingleNodeSelected = selection.nodes.length === 1;
|
|
284
|
+
const hasMultipleNodesSelected = selection.nodes.length > 1;
|
|
285
|
+
// Determine if right-clicked over a node by hit-testing
|
|
286
|
+
const target = e.target?.closest(".react-flow__node");
|
|
287
|
+
if (target) {
|
|
288
|
+
// Resolve node id from data-id attribute React Flow sets
|
|
289
|
+
const nodeId = target.getAttribute("data-id");
|
|
290
|
+
if (!nodeId)
|
|
291
|
+
return;
|
|
292
|
+
const isSelected = selection.nodes.includes(nodeId);
|
|
293
|
+
if (isSelected && !isSingleNodeSelected) {
|
|
294
|
+
// Right-clicked on a node that's part of multi-selection - show selection menu
|
|
295
|
+
setMenuState({
|
|
296
|
+
type: "selection",
|
|
297
|
+
menuPos: { x: e.clientX, y: e.clientY },
|
|
298
|
+
});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// Right-clicked on a non-selected node - show node menu
|
|
303
|
+
setMenuState({
|
|
304
|
+
type: "node",
|
|
305
|
+
menuPos: { x: e.clientX, y: e.clientY },
|
|
306
|
+
nodeId,
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Check if right-clicked on a selected edge
|
|
312
|
+
const edgeTarget = e.target?.closest(".react-flow__edge");
|
|
313
|
+
if (edgeTarget) {
|
|
314
|
+
const edgeId = edgeTarget.getAttribute("data-id");
|
|
315
|
+
const isSelected = edgeId && selection.edges.includes(edgeId);
|
|
316
|
+
if (isSelected && isSingleNodeSelected) {
|
|
317
|
+
// Right-clicked on an edge, but only one node is selected - show node menu
|
|
318
|
+
const nodeId = selection.nodes[0];
|
|
319
|
+
setMenuState({
|
|
320
|
+
type: "node",
|
|
321
|
+
menuPos: { x: e.clientX, y: e.clientY },
|
|
322
|
+
nodeId,
|
|
323
|
+
});
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
else if (isSelected) {
|
|
327
|
+
// Right-clicked on a selected edge with multiple nodes - show selection menu
|
|
328
|
+
setMenuState({
|
|
329
|
+
type: "selection",
|
|
330
|
+
menuPos: { x: e.clientX, y: e.clientY },
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Check if the cursor is inside the rectangular bounds of the current selection
|
|
336
|
+
// (for multi-selection when right-clicking on empty space within selection bounds)
|
|
337
|
+
const selectionBounds = getSelectionScreenBounds();
|
|
338
|
+
if (selectionBounds && hasMultipleNodesSelected) {
|
|
339
|
+
const { left, top, right, bottom } = selectionBounds;
|
|
340
|
+
if (e.clientX >= left && e.clientX <= right && e.clientY >= top && e.clientY <= bottom) {
|
|
341
|
+
// If only one node is selected (even with edges), show node menu for empty space clicks
|
|
342
|
+
if (isSingleNodeSelected) {
|
|
343
|
+
const nodeId = selection.nodes[0];
|
|
344
|
+
setMenuState({
|
|
345
|
+
type: "node",
|
|
346
|
+
menuPos: { x: e.clientX, y: e.clientY },
|
|
347
|
+
nodeId,
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
setMenuState({
|
|
352
|
+
type: "selection",
|
|
353
|
+
menuPos: { x: e.clientX, y: e.clientY },
|
|
354
|
+
});
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// Right-clicked on empty space with no selection - show default menu
|
|
359
|
+
setMenuState({
|
|
360
|
+
type: "default",
|
|
361
|
+
menuPos: { x: e.clientX, y: e.clientY },
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
const addNodeAt = useCallback(async (typeId, opts) => wb.addNode({ typeId }, { inputs: opts.inputs, position: opts.position, commit: true }), [wb]);
|
|
365
|
+
const onCloseMenu = useCallback(() => {
|
|
366
|
+
setMenuState(null);
|
|
367
|
+
}, []);
|
|
368
|
+
const onCloseNodeMenu = useCallback(() => {
|
|
369
|
+
setMenuState(null);
|
|
370
|
+
}, []);
|
|
371
|
+
const onCloseSelectionMenu = useCallback(() => {
|
|
372
|
+
setMenuState(null);
|
|
373
|
+
}, []);
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
const off = wb.on("historyChanged", (event) => {
|
|
376
|
+
setHistoryState(event.history);
|
|
377
|
+
});
|
|
378
|
+
return () => off();
|
|
379
|
+
}, [wb]);
|
|
380
|
+
const nodeIds = useMemo(() => Array.from(wb.registry.nodes.keys()), [wb.registry, registryVersion]);
|
|
381
|
+
const defaultContextMenuHandlers = useMemo(() => {
|
|
382
|
+
// Get storage from override or use workbench's internal storage
|
|
383
|
+
const storage = overrides?.getCopiedDataStorage
|
|
384
|
+
? overrides.getCopiedDataStorage()
|
|
385
|
+
: {
|
|
386
|
+
get: () => wb.getCopiedData(),
|
|
387
|
+
set: (data) => wb.setCopiedData(data),
|
|
388
|
+
};
|
|
389
|
+
const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu, (position) => {
|
|
390
|
+
const data = storage.get();
|
|
391
|
+
if (!data)
|
|
392
|
+
return;
|
|
393
|
+
wb.pasteCopiedData(data, position, { commit: true, reason: "paste" });
|
|
394
|
+
onCloseMenu();
|
|
395
|
+
}, runner, () => storage.get(), () => storage.set(null), historyState, wb);
|
|
396
|
+
if (overrides?.getDefaultContextMenuHandlers) {
|
|
397
|
+
return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
|
|
398
|
+
}
|
|
399
|
+
return baseHandlers;
|
|
400
|
+
}, [addNodeAt, onCloseMenu, overrides, wb, runner, historyState]);
|
|
401
|
+
const selectionContextMenuHandlers = useMemo(() => {
|
|
402
|
+
// Get storage from override or use workbench's internal storage
|
|
403
|
+
const storage = overrides?.getCopiedDataStorage
|
|
404
|
+
? overrides.getCopiedDataStorage()
|
|
405
|
+
: {
|
|
406
|
+
get: () => wb.getCopiedData(),
|
|
407
|
+
set: (data) => wb.setCopiedData(data),
|
|
408
|
+
};
|
|
409
|
+
const baseHandlers = createSelectionContextMenuHandlers(wb, onCloseSelectionMenu, overrides?.getDefaultNodeSize, (data) => {
|
|
410
|
+
storage.set(data);
|
|
411
|
+
}, runner);
|
|
412
|
+
if (overrides?.getSelectionContextMenuHandlers) {
|
|
413
|
+
const selection = wb.getSelection();
|
|
414
|
+
return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, {
|
|
415
|
+
getDefaultNodeSize: overrides.getDefaultNodeSize,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
return baseHandlers;
|
|
419
|
+
}, [wb, runner, overrides, onCloseSelectionMenu]);
|
|
420
|
+
const nodeContextMenuHandlers = useMemo(() => {
|
|
421
|
+
if (menuState?.type !== "node")
|
|
422
|
+
return null;
|
|
423
|
+
const nodeAtMenu = menuState.nodeId;
|
|
424
|
+
// Get storage from override or use workbench's internal storage
|
|
425
|
+
const storage = overrides?.getCopiedDataStorage
|
|
426
|
+
? overrides.getCopiedDataStorage()
|
|
427
|
+
: {
|
|
428
|
+
get: () => wb.getCopiedData(),
|
|
429
|
+
set: (data) => wb.setCopiedData(data),
|
|
430
|
+
};
|
|
431
|
+
const baseHandlers = createNodeContextMenuHandlers(nodeAtMenu, wb, runner, wb.registry, outputsMap, outputTypesMap, onCloseNodeMenu, overrides?.getDefaultNodeSize, (data) => {
|
|
432
|
+
storage.set(data);
|
|
433
|
+
}, runNode, runFromHere);
|
|
434
|
+
if (overrides?.getNodeContextMenuHandlers) {
|
|
435
|
+
return overrides.getNodeContextMenuHandlers(wb, nodeAtMenu, baseHandlers);
|
|
436
|
+
}
|
|
437
|
+
return baseHandlers;
|
|
438
|
+
}, [
|
|
439
|
+
menuState,
|
|
440
|
+
wb,
|
|
441
|
+
runner,
|
|
442
|
+
wb.registry,
|
|
443
|
+
registryVersion,
|
|
444
|
+
outputsMap,
|
|
445
|
+
outputTypesMap,
|
|
446
|
+
onCloseNodeMenu,
|
|
447
|
+
overrides?.getDefaultNodeSize,
|
|
448
|
+
overrides?.getNodeContextMenuHandlers,
|
|
449
|
+
overrides?.getCopiedDataStorage,
|
|
450
|
+
]);
|
|
451
|
+
const bakeableOutputs = useMemo(() => {
|
|
452
|
+
if (menuState?.type !== "node")
|
|
453
|
+
return [];
|
|
454
|
+
return getBakeableOutputs(menuState.nodeId, wb, wb.registry, outputTypesMap);
|
|
455
|
+
}, [menuState, wb, wb.registry, registryVersion, outputTypesMap]);
|
|
456
|
+
// Keyboard shortcuts configuration
|
|
457
|
+
const keyboardShortcutEnables = useMemo(() => ({
|
|
458
|
+
undo: true,
|
|
459
|
+
redo: true,
|
|
460
|
+
copy: true,
|
|
461
|
+
paste: true,
|
|
462
|
+
duplicate: true,
|
|
463
|
+
selectAll: true,
|
|
464
|
+
...(overrides?.enableKeyboardShortcuts ?? {}),
|
|
465
|
+
}), [overrides?.enableKeyboardShortcuts]);
|
|
466
|
+
const isShortcutEnabled = useCallback((action) => keyboardShortcutEnables[action] !== false, [keyboardShortcutEnables]);
|
|
467
|
+
const hasEnabledKeyboardShortcuts = useMemo(() => Object.values(keyboardShortcutEnables).some(Boolean), [keyboardShortcutEnables]);
|
|
468
|
+
const keyboardShortcuts = useMemo(() => overrides?.keyboardShortcuts || {
|
|
469
|
+
undo: "⌘/Ctrl + Z",
|
|
470
|
+
redo: "⌘/Ctrl + Shift + Z",
|
|
471
|
+
copy: "⌘/Ctrl + C",
|
|
472
|
+
paste: "⌘/Ctrl + V",
|
|
473
|
+
duplicate: "⌘/Ctrl + E",
|
|
474
|
+
selectAll: "⌘/Ctrl + A",
|
|
475
|
+
delete: "Delete",
|
|
476
|
+
}, [overrides?.keyboardShortcuts]);
|
|
477
|
+
// Toast notification for keyboard shortcuts
|
|
478
|
+
const { toast, showToast, hideToast } = useKeyboardShortcutToast();
|
|
479
|
+
// Keyboard shortcut handler
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
if (!hasEnabledKeyboardShortcuts)
|
|
482
|
+
return;
|
|
483
|
+
const handleKeyDown = async (e) => {
|
|
484
|
+
// Check if target is inside WorkbenchCanvas container
|
|
485
|
+
const target = e.target;
|
|
486
|
+
if (!containerRef.current || !(containerRef.current.contains(target) || containerRef.current == target)) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
// Ignore if typing in input/textarea
|
|
490
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
// Detect Mac platform using userAgent (navigator.platform is deprecated)
|
|
494
|
+
const isMac = typeof navigator !== "undefined" && navigator.userAgent.toLowerCase().includes("mac");
|
|
495
|
+
const modKey = isMac ? e.metaKey : e.ctrlKey;
|
|
496
|
+
const key = e.key.toLowerCase();
|
|
497
|
+
// Undo: Cmd/Ctrl + Z
|
|
498
|
+
if (modKey && key === "z" && !e.shiftKey && !e.altKey) {
|
|
499
|
+
if (!isShortcutEnabled("undo"))
|
|
500
|
+
return;
|
|
501
|
+
e.preventDefault();
|
|
502
|
+
if (runner &&
|
|
503
|
+
"onUndo" in defaultContextMenuHandlers &&
|
|
504
|
+
defaultContextMenuHandlers.onUndo &&
|
|
505
|
+
defaultContextMenuHandlers.canUndo) {
|
|
506
|
+
if (defaultContextMenuHandlers.canUndo) {
|
|
507
|
+
const modKeyLabel = isMac ? "⌘" : "Ctrl";
|
|
508
|
+
showToast(`Undo (${modKeyLabel} + Z)`);
|
|
509
|
+
defaultContextMenuHandlers.onUndo();
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
// Redo: Cmd/Ctrl + Shift + Z
|
|
515
|
+
if (modKey && e.shiftKey && key === "z" && !e.altKey) {
|
|
516
|
+
if (!isShortcutEnabled("redo"))
|
|
517
|
+
return;
|
|
518
|
+
e.preventDefault();
|
|
519
|
+
if (runner &&
|
|
520
|
+
"onRedo" in defaultContextMenuHandlers &&
|
|
521
|
+
defaultContextMenuHandlers.onRedo &&
|
|
522
|
+
defaultContextMenuHandlers.canRedo) {
|
|
523
|
+
if (defaultContextMenuHandlers.canRedo) {
|
|
524
|
+
const modKeyLabel = isMac ? "⌘" : "Ctrl";
|
|
525
|
+
showToast(`Redo (${modKeyLabel} + Shift + Z)`);
|
|
526
|
+
defaultContextMenuHandlers.onRedo();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
// Copy: Cmd/Ctrl + C
|
|
532
|
+
if (modKey && key === "c" && !e.shiftKey && !e.altKey) {
|
|
533
|
+
if (!isShortcutEnabled("copy"))
|
|
534
|
+
return;
|
|
535
|
+
const selection = wb.getSelection();
|
|
536
|
+
if (selection.nodes.length > 0 || selection.edges.length > 0) {
|
|
537
|
+
e.preventDefault();
|
|
538
|
+
const modKeyLabel = isMac ? "⌘" : "Ctrl";
|
|
539
|
+
showToast(`Copy (${modKeyLabel} + C)`);
|
|
540
|
+
// If single node selected, use node context menu handler; otherwise use selection handler
|
|
541
|
+
if (selection.nodes.length === 1 && nodeContextMenuHandlers?.onCopy) {
|
|
542
|
+
nodeContextMenuHandlers.onCopy();
|
|
543
|
+
}
|
|
544
|
+
else if (selectionContextMenuHandlers.onCopy) {
|
|
545
|
+
selectionContextMenuHandlers.onCopy();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
// Duplicate: Cmd/Ctrl + E
|
|
551
|
+
if (modKey && key === "e" && !e.shiftKey && !e.altKey) {
|
|
552
|
+
if (!isShortcutEnabled("duplicate"))
|
|
553
|
+
return;
|
|
554
|
+
const selection = wb.getSelection();
|
|
555
|
+
if (selection.nodes.length === 1 && nodeContextMenuHandlers?.onDuplicate) {
|
|
556
|
+
// Single node selected - use node context menu handler
|
|
557
|
+
e.preventDefault();
|
|
558
|
+
const modKeyLabel = isMac ? "⌘" : "Ctrl";
|
|
559
|
+
showToast(`Duplicate (${modKeyLabel} + E)`);
|
|
560
|
+
nodeContextMenuHandlers.onDuplicate();
|
|
561
|
+
}
|
|
562
|
+
else if (selection.nodes.length > 1 && selectionContextMenuHandlers.onDuplicate) {
|
|
563
|
+
// Multiple nodes selected - use selection context menu handler
|
|
564
|
+
e.preventDefault();
|
|
565
|
+
const modKeyLabel = isMac ? "⌘" : "Ctrl";
|
|
566
|
+
showToast(`Duplicate (${modKeyLabel} + E)`);
|
|
567
|
+
selectionContextMenuHandlers.onDuplicate();
|
|
568
|
+
}
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
// Paste: Cmd/Ctrl + V
|
|
572
|
+
if (modKey && key === "v" && !e.shiftKey && !e.altKey) {
|
|
573
|
+
if (!isShortcutEnabled("paste"))
|
|
574
|
+
return;
|
|
575
|
+
e.preventDefault();
|
|
576
|
+
if ("hasPasteData" in defaultContextMenuHandlers &&
|
|
577
|
+
defaultContextMenuHandlers.hasPasteData &&
|
|
578
|
+
defaultContextMenuHandlers.hasPasteData() &&
|
|
579
|
+
"onPaste" in defaultContextMenuHandlers &&
|
|
580
|
+
defaultContextMenuHandlers.onPaste) {
|
|
581
|
+
const modKeyLabel = isMac ? "⌘" : "Ctrl";
|
|
582
|
+
showToast(`Paste (${modKeyLabel} + V)`);
|
|
583
|
+
const center = rfInstanceRef.current?.screenToFlowPosition({
|
|
584
|
+
x: window.innerWidth / 2,
|
|
585
|
+
y: window.innerHeight / 2,
|
|
586
|
+
}) || { x: 0, y: 0 };
|
|
587
|
+
defaultContextMenuHandlers.onPaste(center);
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
// Select All: Cmd/Ctrl + A
|
|
592
|
+
if (modKey && key === "a" && !e.shiftKey && !e.altKey) {
|
|
593
|
+
if (!isShortcutEnabled("selectAll"))
|
|
594
|
+
return;
|
|
595
|
+
e.preventDefault();
|
|
596
|
+
if (defaultContextMenuHandlers.onSelectAll) {
|
|
597
|
+
const modKeyLabel = isMac ? "⌘" : "Ctrl";
|
|
598
|
+
showToast(`Select All (${modKeyLabel} + A)`);
|
|
599
|
+
defaultContextMenuHandlers.onSelectAll();
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
// Note: Delete/Backspace is handled by ReactFlow's deleteKeyCode prop
|
|
604
|
+
// which triggers onNodesDelete/onEdgesDelete, so we don't need to handle it here
|
|
605
|
+
};
|
|
606
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
607
|
+
return () => {
|
|
608
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
609
|
+
};
|
|
610
|
+
}, [
|
|
611
|
+
hasEnabledKeyboardShortcuts,
|
|
612
|
+
wb,
|
|
613
|
+
runner,
|
|
614
|
+
defaultContextMenuHandlers,
|
|
615
|
+
selectionContextMenuHandlers,
|
|
616
|
+
nodeContextMenuHandlers,
|
|
617
|
+
rfInstanceRef,
|
|
618
|
+
showToast,
|
|
619
|
+
isShortcutEnabled,
|
|
620
|
+
]);
|
|
621
|
+
// Get custom renderers from UI extension registry (reactive to uiVersion changes)
|
|
622
|
+
const { BackgroundRenderer, MinimapRenderer, ControlsRenderer, DefaultContextMenuRenderer, NodeContextMenuRenderer, SelectionContextMenuRenderer, connectionLineRenderer, } = useMemo(() => {
|
|
623
|
+
return {
|
|
624
|
+
BackgroundRenderer: ui.getBackgroundRenderer(),
|
|
625
|
+
MinimapRenderer: ui.getMinimapRenderer(),
|
|
626
|
+
ControlsRenderer: ui.getControlsRenderer(),
|
|
627
|
+
DefaultContextMenuRenderer: ui.getDefaultContextMenuRenderer(),
|
|
628
|
+
NodeContextMenuRenderer: ui.getNodeContextMenuRenderer(),
|
|
629
|
+
SelectionContextMenuRenderer: ui.getSelectionContextMenuRenderer(),
|
|
630
|
+
connectionLineRenderer: ui.getConnectionLineRenderer(),
|
|
631
|
+
};
|
|
632
|
+
}, [ui, uiVersion]);
|
|
633
|
+
const onMoveEnd = useCallback(() => {
|
|
634
|
+
if (rfInstanceRef.current) {
|
|
635
|
+
const viewport = rfInstanceRef.current.getViewport();
|
|
636
|
+
const viewportData = lod.clone(viewport);
|
|
637
|
+
wb.setViewport(viewportData);
|
|
638
|
+
}
|
|
639
|
+
}, [wb]);
|
|
640
|
+
// Sync viewport when workbench fires graphUiChanged with viewport event
|
|
641
|
+
useEffect(() => {
|
|
642
|
+
const off = wb.on("graphUiChanged", (event) => {
|
|
643
|
+
if (event.change?.type === "viewport" && rfInstanceRef.current && event.init) {
|
|
644
|
+
const viewport = wb.getViewport();
|
|
645
|
+
if (viewport) {
|
|
646
|
+
rfInstanceRef.current.setViewport(lod.clone(viewport));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
return () => off();
|
|
651
|
+
}, [wb]);
|
|
652
|
+
const { onInit: userOnInit, ...restReactFlowProps } = reactFlowProps || {};
|
|
653
|
+
return (_jsxs("div", { ref: containerRef, className: "w-full h-full relative overflow-hidden", tabIndex: 0, onContextMenu: onContextMenu, children: [_jsxs(ReactFlowProvider, { children: [_jsxs(ReactFlow, { ...restReactFlowProps, nodes: rfData.nodes, edges: rfData.edges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, connectionLineComponent: connectionLineRenderer, selectionOnDrag: true, onInit: (inst) => {
|
|
654
|
+
rfInstanceRef.current = inst;
|
|
655
|
+
const savedViewport = wb.getViewport();
|
|
656
|
+
if (savedViewport) {
|
|
657
|
+
inst.setViewport(lod.clone(savedViewport));
|
|
658
|
+
}
|
|
659
|
+
if (userOnInit) {
|
|
660
|
+
userOnInit(inst);
|
|
661
|
+
}
|
|
662
|
+
}, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onMoveEnd: onMoveEnd, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", children: [children, BackgroundRenderer ? (_jsx(BackgroundRenderer, {})) : (_jsx(Background, { id: "workbench-canvas-background", variant: BackgroundVariant.Dots, gap: 12, size: 1 })), MinimapRenderer ? _jsx(MinimapRenderer, {}) : _jsx(MiniMap, {}), ControlsRenderer ? _jsx(ControlsRenderer, {}) : _jsx(Controls, {}), menuState?.type === "default" &&
|
|
663
|
+
(DefaultContextMenuRenderer ? (_jsx(DefaultContextMenuRenderer, { open: true, clientPos: menuState.menuPos, handlers: defaultContextMenuHandlers, registry: wb.registry, nodeIds: nodeIds, keyboardShortcuts: keyboardShortcuts })) : (_jsx(DefaultContextMenu, { open: true, clientPos: menuState.menuPos, handlers: defaultContextMenuHandlers, registry: wb.registry, nodeIds: nodeIds, keyboardShortcuts: keyboardShortcuts }))), menuState?.type === "node" &&
|
|
664
|
+
nodeContextMenuHandlers &&
|
|
665
|
+
(NodeContextMenuRenderer ? (_jsx(NodeContextMenuRenderer, { open: true, clientPos: menuState.menuPos, nodeId: menuState.nodeId, handlers: nodeContextMenuHandlers, bakeableOutputs: bakeableOutputs, runMode: runMode, wb: wb, keyboardShortcuts: keyboardShortcuts })) : (_jsx(NodeContextMenu, { open: true, clientPos: menuState.menuPos, nodeId: menuState.nodeId, handlers: nodeContextMenuHandlers, bakeableOutputs: bakeableOutputs, runMode: runMode }))), menuState?.type === "selection" &&
|
|
666
|
+
(SelectionContextMenuRenderer ? (_jsx(SelectionContextMenuRenderer, { open: true, clientPos: menuState.menuPos, handlers: selectionContextMenuHandlers, keyboardShortcuts: keyboardShortcuts })) : (_jsx(SelectionContextMenu, { open: true, clientPos: menuState.menuPos, handlers: selectionContextMenuHandlers, keyboardShortcuts: keyboardShortcuts })))] }), _jsx(SelectionActiveSync, { selection: selection })] }), toast && _jsx(KeyboardShortcutToast, { message: toast.message, onClose: hideToast }, toast.id)] }));
|
|
667
|
+
});
|
|
668
|
+
export const WorkbenchCanvas = WorkbenchCanvasComponent;
|
|
669
|
+
//# sourceMappingURL=WorkbenchCanvas.js.map
|