@bian-womp/spark-workbench 0.2.69 → 0.2.70
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 +125 -69
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts +27 -7
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/core/contracts.d.ts +5 -0
- package/lib/cjs/src/core/contracts.d.ts.map +1 -1
- package/lib/cjs/src/misc/NodeContextMenu.d.ts +1 -1
- package/lib/cjs/src/misc/NodeContextMenu.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts +6 -0
- package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +1 -0
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/cjs/src/misc/hooks.d.ts.map +1 -1
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +1 -1
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/IGraphRunner.d.ts +1 -1
- package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +1 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/lib/esm/index.js +125 -69
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/InMemoryWorkbench.d.ts +27 -7
- package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/esm/src/core/contracts.d.ts +5 -0
- package/lib/esm/src/core/contracts.d.ts.map +1 -1
- package/lib/esm/src/misc/NodeContextMenu.d.ts +1 -1
- package/lib/esm/src/misc/NodeContextMenu.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts +6 -0
- package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
- package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts +1 -0
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
- package/lib/esm/src/misc/hooks.d.ts.map +1 -1
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +1 -1
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/IGraphRunner.d.ts +1 -1
- package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +1 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/package.json +4 -4
package/lib/esm/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generateId, GraphBuilder, createEngine, StepEngine, PullEngine, BatchedEngine, isTypedOutput, getTypedOutputValue, getTypedOutputTypeId, isInputPrivate, getInputTypeId, createSimpleGraphRegistry, createSimpleGraphDef, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createValidationGraphDef, createValidationGraphRegistry } from '@bian-womp/spark-graph';
|
|
2
|
+
import lod from 'lodash';
|
|
2
3
|
import { RuntimeApiClient } from '@bian-womp/spark-remote';
|
|
3
4
|
import { Position, Handle, useUpdateNodeInternals, useReactFlow, ReactFlowProvider, ReactFlow, Background, BackgroundVariant, MiniMap, Controls } from '@xyflow/react';
|
|
4
5
|
import React, { useCallback, useState, useRef, useEffect, useMemo, createContext, useContext, useImperativeHandle } from 'react';
|
|
@@ -208,18 +209,19 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
208
209
|
inputs: options?.inputs,
|
|
209
210
|
copyOutputsFrom: options?.copyOutputsFrom,
|
|
210
211
|
},
|
|
211
|
-
|
|
212
|
+
...lod.pick(options, ["dry", "commit", "reason"]),
|
|
212
213
|
});
|
|
213
214
|
this.refreshValidation();
|
|
214
215
|
return id;
|
|
215
216
|
}
|
|
216
|
-
removeNode(nodeId) {
|
|
217
|
+
removeNode(nodeId, options) {
|
|
217
218
|
this.def.nodes = this.def.nodes.filter((n) => n.nodeId !== nodeId);
|
|
218
219
|
this.def.edges = this.def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
|
|
219
220
|
delete this.positions[nodeId];
|
|
220
221
|
this.emit("graphChanged", {
|
|
221
222
|
def: this.def,
|
|
222
223
|
change: { type: "removeNode", nodeId },
|
|
224
|
+
...options,
|
|
223
225
|
});
|
|
224
226
|
this.refreshValidation();
|
|
225
227
|
}
|
|
@@ -234,16 +236,17 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
234
236
|
this.emit("graphChanged", {
|
|
235
237
|
def: this.def,
|
|
236
238
|
change: { type: "connect", edgeId: id },
|
|
237
|
-
|
|
239
|
+
...options,
|
|
238
240
|
});
|
|
239
241
|
this.refreshValidation();
|
|
240
242
|
return id;
|
|
241
243
|
}
|
|
242
|
-
disconnect(edgeId) {
|
|
244
|
+
disconnect(edgeId, options) {
|
|
243
245
|
this.def.edges = this.def.edges.filter((e) => e.id !== edgeId);
|
|
244
246
|
this.emit("graphChanged", {
|
|
245
247
|
def: this.def,
|
|
246
248
|
change: { type: "disconnect", edgeId },
|
|
249
|
+
...options,
|
|
247
250
|
});
|
|
248
251
|
this.emit("validationChanged", this.validate());
|
|
249
252
|
}
|
|
@@ -272,32 +275,32 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
272
275
|
});
|
|
273
276
|
}
|
|
274
277
|
// Position and selection APIs for React Flow bridge
|
|
275
|
-
setPosition(nodeId, pos,
|
|
278
|
+
setPosition(nodeId, pos, options) {
|
|
276
279
|
this.positions[nodeId] = pos;
|
|
277
280
|
this.emit("graphUiChanged", {
|
|
278
281
|
def: this.def,
|
|
279
282
|
change: { type: "moveNode", nodeId, pos },
|
|
280
|
-
|
|
283
|
+
...options,
|
|
281
284
|
});
|
|
282
285
|
}
|
|
283
|
-
setPositions(map,
|
|
286
|
+
setPositions(map, options) {
|
|
284
287
|
this.positions = { ...map };
|
|
285
288
|
this.emit("graphUiChanged", {
|
|
286
289
|
def: this.def,
|
|
287
290
|
change: { type: "moveNodes" },
|
|
288
|
-
|
|
291
|
+
...options,
|
|
289
292
|
});
|
|
290
293
|
}
|
|
291
294
|
getPositions() {
|
|
292
295
|
return { ...this.positions };
|
|
293
296
|
}
|
|
294
|
-
setSelection(sel,
|
|
297
|
+
setSelection(sel, options) {
|
|
295
298
|
this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
|
|
296
299
|
this.emit("selectionChanged", this.selection);
|
|
297
300
|
this.emit("graphUiChanged", {
|
|
298
301
|
def: this.def,
|
|
299
302
|
change: { type: "selection" },
|
|
300
|
-
|
|
303
|
+
...options,
|
|
301
304
|
});
|
|
302
305
|
}
|
|
303
306
|
getSelection() {
|
|
@@ -309,7 +312,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
309
312
|
/**
|
|
310
313
|
* Delete all selected nodes and edges.
|
|
311
314
|
*/
|
|
312
|
-
deleteSelection() {
|
|
315
|
+
deleteSelection(options) {
|
|
313
316
|
const selection = this.getSelection();
|
|
314
317
|
// Delete all selected nodes (this will also remove connected edges)
|
|
315
318
|
for (const nodeId of selection.nodes) {
|
|
@@ -320,14 +323,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
320
323
|
this.disconnect(edgeId);
|
|
321
324
|
}
|
|
322
325
|
// Clear selection
|
|
323
|
-
this.setSelection({ nodes: [], edges: [] });
|
|
326
|
+
this.setSelection({ nodes: [], edges: [] }, options);
|
|
324
327
|
}
|
|
325
|
-
setViewport(viewport,
|
|
328
|
+
setViewport(viewport, options) {
|
|
326
329
|
this.viewport = { ...viewport };
|
|
327
330
|
this.emit("graphUiChanged", {
|
|
328
331
|
def: this.def,
|
|
329
332
|
change: { type: "viewport" },
|
|
330
|
-
|
|
333
|
+
...options,
|
|
331
334
|
});
|
|
332
335
|
}
|
|
333
336
|
getViewport() {
|
|
@@ -472,7 +475,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
472
475
|
* Returns the mapping from original node IDs to new node IDs.
|
|
473
476
|
* Uses copyOutputsFrom to copy outputs from original nodes (like duplicate does).
|
|
474
477
|
*/
|
|
475
|
-
pasteCopiedData(data, center) {
|
|
478
|
+
pasteCopiedData(data, center, options) {
|
|
476
479
|
const nodeIdMap = new Map();
|
|
477
480
|
const edgeIds = [];
|
|
478
481
|
// Add nodes
|
|
@@ -512,10 +515,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
512
515
|
}
|
|
513
516
|
}
|
|
514
517
|
// Select the newly pasted nodes
|
|
515
|
-
this.setSelection({
|
|
516
|
-
nodes: Array.from(nodeIdMap.values()),
|
|
517
|
-
edges: edgeIds,
|
|
518
|
-
});
|
|
518
|
+
this.setSelection({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
|
|
519
519
|
return { nodeIdMap, edgeIds };
|
|
520
520
|
}
|
|
521
521
|
/**
|
|
@@ -684,7 +684,7 @@ class AbstractGraphRunner {
|
|
|
684
684
|
return false;
|
|
685
685
|
}
|
|
686
686
|
// Optional commit support
|
|
687
|
-
async commit() { }
|
|
687
|
+
async commit(_reason) { }
|
|
688
688
|
}
|
|
689
689
|
|
|
690
690
|
// Counter for generating readable runner IDs
|
|
@@ -1431,10 +1431,10 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1431
1431
|
const client = await this.ensureClient();
|
|
1432
1432
|
await client.setExtData(data);
|
|
1433
1433
|
}
|
|
1434
|
-
async commit() {
|
|
1434
|
+
async commit(reason) {
|
|
1435
1435
|
const client = await this.ensureClient();
|
|
1436
1436
|
try {
|
|
1437
|
-
await client.commit();
|
|
1437
|
+
await client.commit(reason);
|
|
1438
1438
|
}
|
|
1439
1439
|
catch (err) {
|
|
1440
1440
|
console.error("[RemoteGraphRunner] Error committing:", err);
|
|
@@ -1914,7 +1914,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1914
1914
|
wb.connect({
|
|
1915
1915
|
source: { nodeId: params.source, handle: params.sourceHandle },
|
|
1916
1916
|
target: { nodeId: params.target, handle: params.targetHandle },
|
|
1917
|
-
});
|
|
1917
|
+
}, { commit: true });
|
|
1918
1918
|
}, [wb]);
|
|
1919
1919
|
const onNodesChange = useCallback((changes) => {
|
|
1920
1920
|
// Apply position updates continuously, but mark commit only on drag end
|
|
@@ -1957,7 +1957,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1957
1957
|
});
|
|
1958
1958
|
}
|
|
1959
1959
|
}, [wb]);
|
|
1960
|
-
const onEdgesDelete = useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
|
|
1960
|
+
const onEdgesDelete = useCallback((edges) => edges.forEach((e, idx) => wb.disconnect(e.id, { commit: idx === edges.length - 1 })), [wb]);
|
|
1961
1961
|
const onEdgesChange = useCallback((changes) => {
|
|
1962
1962
|
const current = wb.getSelection();
|
|
1963
1963
|
const nextEdgeIds = new Set(current.edges);
|
|
@@ -1993,8 +1993,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1993
1993
|
}
|
|
1994
1994
|
}, [wb]);
|
|
1995
1995
|
const onNodesDelete = useCallback((nodes) => {
|
|
1996
|
-
|
|
1997
|
-
wb.removeNode(n.id);
|
|
1996
|
+
nodes.forEach((n, idx) => wb.removeNode(n.id, { commit: idx === nodes.length - 1 }));
|
|
1998
1997
|
}, [wb]);
|
|
1999
1998
|
return {
|
|
2000
1999
|
onConnect,
|
|
@@ -2687,7 +2686,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2687
2686
|
}
|
|
2688
2687
|
curX += maxWidth + H_GAP;
|
|
2689
2688
|
}
|
|
2690
|
-
wb.setPositions(pos, { commit: true });
|
|
2689
|
+
wb.setPositions(pos, { commit: true, reason: "auto-layout" });
|
|
2691
2690
|
}, [wb, registry, overrides?.getDefaultNodeSize]);
|
|
2692
2691
|
const updateEdgeType = useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
|
|
2693
2692
|
const triggerExternal = useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
|
|
@@ -2943,11 +2942,36 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2943
2942
|
}
|
|
2944
2943
|
});
|
|
2945
2944
|
const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
|
|
2945
|
+
// Build detailed reason from change type
|
|
2946
|
+
let reason = "graph-changed";
|
|
2947
|
+
if (event.change) {
|
|
2948
|
+
const changeType = event.change.type;
|
|
2949
|
+
if (changeType === "addNode") {
|
|
2950
|
+
reason = "add-node";
|
|
2951
|
+
}
|
|
2952
|
+
else if (changeType === "removeNode") {
|
|
2953
|
+
reason = "remove-node";
|
|
2954
|
+
}
|
|
2955
|
+
else if (changeType === "connect") {
|
|
2956
|
+
reason = "connect-edge";
|
|
2957
|
+
}
|
|
2958
|
+
else if (changeType === "disconnect") {
|
|
2959
|
+
reason = "disconnect-edge";
|
|
2960
|
+
}
|
|
2961
|
+
else if (changeType === "updateParams") {
|
|
2962
|
+
reason = "update-node-params";
|
|
2963
|
+
}
|
|
2964
|
+
else if (changeType === "updateEdgeType") {
|
|
2965
|
+
reason = "update-edge-type";
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2946
2968
|
if (!runner.isRunning()) {
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2969
|
+
if (event.commit) {
|
|
2970
|
+
// If runner not running, commit immediately (no update needed)
|
|
2971
|
+
await runner.commit(reason).catch((err) => {
|
|
2972
|
+
console.error("[WorkbenchContext] Error committing:", err);
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2951
2975
|
return;
|
|
2952
2976
|
}
|
|
2953
2977
|
try {
|
|
@@ -2972,10 +2996,12 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2972
2996
|
else {
|
|
2973
2997
|
await runner.update(event.def, { dry: event.dry });
|
|
2974
2998
|
}
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2999
|
+
if (event.commit) {
|
|
3000
|
+
// Wait for update to complete, then commit
|
|
3001
|
+
await runner.commit(event.reason ?? reason).catch((err) => {
|
|
3002
|
+
console.error("[WorkbenchContext] Error committing after update:", err);
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
2979
3005
|
}
|
|
2980
3006
|
catch (err) {
|
|
2981
3007
|
console.error("[WorkbenchContext] Error updating graph:", err);
|
|
@@ -2985,15 +3011,22 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2985
3011
|
const offWbSelectionChanged = wb.on("selectionChanged", async (sel) => {
|
|
2986
3012
|
setSelectedNodeId(sel.nodes?.[0]);
|
|
2987
3013
|
setSelectedEdgeId(sel.edges?.[0]);
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
3014
|
+
if (sel.commit) {
|
|
3015
|
+
// Commit on selection change
|
|
3016
|
+
await runner.commit(sel.reason ?? "selection").catch((err) => {
|
|
3017
|
+
console.error("[WorkbenchContext] Error committing selection change:", err);
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
2992
3020
|
});
|
|
2993
3021
|
const offWbGraphUiChanged = wb.on("graphUiChanged", async (event) => {
|
|
2994
3022
|
// Only commit if commit flag is true (e.g., drag end, not during dragging)
|
|
2995
3023
|
if (event.commit) {
|
|
2996
|
-
|
|
3024
|
+
if (event.change) {
|
|
3025
|
+
event.change.type;
|
|
3026
|
+
}
|
|
3027
|
+
await runner
|
|
3028
|
+
.commit(event.reason ?? "ui-changed")
|
|
3029
|
+
.catch((err) => {
|
|
2997
3030
|
console.error("[WorkbenchContext] Error committing UI changes:", err);
|
|
2998
3031
|
});
|
|
2999
3032
|
}
|
|
@@ -3209,6 +3242,7 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3209
3242
|
try {
|
|
3210
3243
|
const typeId = outputTypesMap?.[nodeId]?.[handleId];
|
|
3211
3244
|
const raw = outputsMap?.[nodeId]?.[handleId];
|
|
3245
|
+
let newNodeId;
|
|
3212
3246
|
if (!typeId || raw === undefined)
|
|
3213
3247
|
return;
|
|
3214
3248
|
const unwrap = (v) => isTypedOutput(v) ? getTypedOutputValue(v) : v;
|
|
@@ -3234,23 +3268,21 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3234
3268
|
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
3235
3269
|
const inType = getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
3236
3270
|
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3237
|
-
wb.addNode({
|
|
3271
|
+
newNodeId = wb.addNode({
|
|
3238
3272
|
typeId: singleTarget.nodeTypeId,
|
|
3239
3273
|
position: { x: pos.x + 180, y: pos.y },
|
|
3240
3274
|
}, { inputs: { [singleTarget.inputHandle]: coerced } });
|
|
3241
|
-
return;
|
|
3242
3275
|
}
|
|
3243
|
-
if (isArray && arrTarget) {
|
|
3276
|
+
else if (isArray && arrTarget) {
|
|
3244
3277
|
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
3245
3278
|
const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
3246
3279
|
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3247
|
-
wb.addNode({
|
|
3280
|
+
newNodeId = wb.addNode({
|
|
3248
3281
|
typeId: arrTarget.nodeTypeId,
|
|
3249
3282
|
position: { x: pos.x + 180, y: pos.y },
|
|
3250
3283
|
}, { inputs: { [arrTarget.inputHandle]: coerced } });
|
|
3251
|
-
return;
|
|
3252
3284
|
}
|
|
3253
|
-
if (isArray && elemTarget) {
|
|
3285
|
+
else if (isArray && elemTarget) {
|
|
3254
3286
|
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
3255
3287
|
const inType = getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
3256
3288
|
const src = unwrap(raw);
|
|
@@ -3262,19 +3294,21 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3262
3294
|
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
3263
3295
|
const col = idx % COLS;
|
|
3264
3296
|
const row = Math.floor(idx / COLS);
|
|
3265
|
-
wb.addNode({
|
|
3297
|
+
newNodeId = wb.addNode({
|
|
3266
3298
|
typeId: elemTarget.nodeTypeId,
|
|
3267
3299
|
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
3268
3300
|
}, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
|
|
3269
3301
|
}
|
|
3270
|
-
|
|
3302
|
+
}
|
|
3303
|
+
if (newNodeId) {
|
|
3304
|
+
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "bake" });
|
|
3271
3305
|
}
|
|
3272
3306
|
}
|
|
3273
3307
|
catch { }
|
|
3274
3308
|
};
|
|
3275
3309
|
return {
|
|
3276
3310
|
onDelete: () => {
|
|
3277
|
-
wb.removeNode(nodeId);
|
|
3311
|
+
wb.removeNode(nodeId, { commit: true });
|
|
3278
3312
|
onClose();
|
|
3279
3313
|
},
|
|
3280
3314
|
onDuplicate: async () => {
|
|
@@ -3299,10 +3333,7 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3299
3333
|
dry: true,
|
|
3300
3334
|
});
|
|
3301
3335
|
// Select the newly duplicated node
|
|
3302
|
-
wb.setSelection({
|
|
3303
|
-
nodes: [newNodeId],
|
|
3304
|
-
edges: [],
|
|
3305
|
-
});
|
|
3336
|
+
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
|
|
3306
3337
|
onClose();
|
|
3307
3338
|
},
|
|
3308
3339
|
onDuplicateWithEdges: async () => {
|
|
@@ -3335,10 +3366,9 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3335
3366
|
}, { dry: true });
|
|
3336
3367
|
}
|
|
3337
3368
|
// Select the newly duplicated node and edges
|
|
3338
|
-
|
|
3339
|
-
nodes: [newNodeId],
|
|
3340
|
-
|
|
3341
|
-
});
|
|
3369
|
+
if (newNodeId) {
|
|
3370
|
+
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate-with-edges" });
|
|
3371
|
+
}
|
|
3342
3372
|
onClose();
|
|
3343
3373
|
},
|
|
3344
3374
|
onRunPull: async () => {
|
|
@@ -3412,7 +3442,7 @@ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onC
|
|
|
3412
3442
|
onClose();
|
|
3413
3443
|
},
|
|
3414
3444
|
onDelete: () => {
|
|
3415
|
-
wb.deleteSelection();
|
|
3445
|
+
wb.deleteSelection({ commit: true, reason: "delete-selection" });
|
|
3416
3446
|
onClose();
|
|
3417
3447
|
},
|
|
3418
3448
|
onClose,
|
|
@@ -4094,10 +4124,14 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
|
|
|
4094
4124
|
}, children: [hasPasteData && handlers.onPaste && (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-between", onClick: handlePaste, children: [jsx("span", { children: "Paste" }), enableKeyboardShortcuts && keyboardShortcuts.paste && (jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.paste) }))] })), (handlers.onUndo || handlers.onRedo) && (jsxs(Fragment, { children: [hasPasteData && handlers.onPaste && (jsx("div", { className: "h-px bg-gray-200 my-1" })), handlers.onUndo && (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-between", onClick: handlers.onUndo, disabled: !canUndo, children: [jsx("span", { children: "Undo" }), enableKeyboardShortcuts && keyboardShortcuts.undo && (jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.undo) }))] })), handlers.onRedo && (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-between", onClick: handlers.onRedo, disabled: !canRedo, children: [jsx("span", { children: "Redo" }), enableKeyboardShortcuts && keyboardShortcuts.redo && (jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.redo) }))] }))] })), hasPasteData &&
|
|
4095
4125
|
handlers.onPaste &&
|
|
4096
4126
|
!handlers.onUndo &&
|
|
4097
|
-
!handlers.onRedo && jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsx("div", { className: "px-2 pb-1", children: jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 px-2 py-1 text-sm outline-none focus:border-gray-400 select-text", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
|
|
4127
|
+
!handlers.onRedo && jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsx("div", { className: "px-2 pb-1", children: jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 rounded px-2 py-1 text-sm outline-none focus:border-gray-400 select-text", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
|
|
4098
4128
|
}
|
|
4099
4129
|
|
|
4100
|
-
function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs,
|
|
4130
|
+
function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
4131
|
+
copy: "⌘/Ctrl + C",
|
|
4132
|
+
duplicate: "⌘/Ctrl + D",
|
|
4133
|
+
delete: "Delete",
|
|
4134
|
+
}, }) {
|
|
4101
4135
|
const ref = useRef(null);
|
|
4102
4136
|
// outside click + ESC
|
|
4103
4137
|
useEffect(() => {
|
|
@@ -4124,6 +4158,12 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
4124
4158
|
if (open)
|
|
4125
4159
|
ref.current?.focus();
|
|
4126
4160
|
}, [open]);
|
|
4161
|
+
// Helper to format shortcut for current platform
|
|
4162
|
+
const formatShortcut = (shortcut) => {
|
|
4163
|
+
const isMac = typeof navigator !== "undefined" &&
|
|
4164
|
+
navigator.userAgent.toLowerCase().includes("mac");
|
|
4165
|
+
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4166
|
+
};
|
|
4127
4167
|
if (!open || !clientPos || !nodeId)
|
|
4128
4168
|
return null;
|
|
4129
4169
|
// clamp
|
|
@@ -4135,7 +4175,7 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
4135
4175
|
return (jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
|
|
4136
4176
|
e.preventDefault();
|
|
4137
4177
|
e.stopPropagation();
|
|
4138
|
-
}, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }),
|
|
4178
|
+
}, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 flex items-center justify-between", onClick: handlers.onDelete, children: [jsx("span", { children: "Delete" }), enableKeyboardShortcuts && keyboardShortcuts.delete && (jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.delete) }))] }), jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 flex items-center justify-between", onClick: handlers.onDuplicate, children: [jsx("span", { children: "Duplicate" }), enableKeyboardShortcuts && keyboardShortcuts.duplicate && (jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.duplicate) }))] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 flex items-center justify-between", onClick: handlers.onCopy, children: [jsx("span", { children: "Copy" }), enableKeyboardShortcuts && keyboardShortcuts.copy && (jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.copy) }))] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" }), bakeableOutputs.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "h-px bg-gray-200 my-1" }), jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h)))] }))] }));
|
|
4139
4179
|
}
|
|
4140
4180
|
|
|
4141
4181
|
function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
@@ -4523,7 +4563,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4523
4563
|
setNodeMenuOpen(false);
|
|
4524
4564
|
setSelectionMenuOpen(false);
|
|
4525
4565
|
};
|
|
4526
|
-
const addNodeAt = useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
|
|
4566
|
+
const addNodeAt = useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs, commit: true }), [wb]);
|
|
4527
4567
|
const onCloseMenu = useCallback(() => {
|
|
4528
4568
|
setMenuOpen(false);
|
|
4529
4569
|
}, []);
|
|
@@ -4555,7 +4595,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4555
4595
|
const data = storage.get();
|
|
4556
4596
|
if (!data)
|
|
4557
4597
|
return;
|
|
4558
|
-
wb.pasteCopiedData(data, position);
|
|
4598
|
+
wb.pasteCopiedData(data, position, { commit: true, reason: "paste" });
|
|
4559
4599
|
onCloseMenu();
|
|
4560
4600
|
}, runner, () => storage.get(), () => storage.set(null));
|
|
4561
4601
|
if (overrides?.getDefaultContextMenuHandlers) {
|
|
@@ -4576,9 +4616,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4576
4616
|
}, runner);
|
|
4577
4617
|
if (overrides?.getSelectionContextMenuHandlers) {
|
|
4578
4618
|
const selection = wb.getSelection();
|
|
4579
|
-
return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, {
|
|
4580
|
-
getDefaultNodeSize: overrides.getDefaultNodeSize,
|
|
4581
|
-
});
|
|
4619
|
+
return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, { getDefaultNodeSize: overrides.getDefaultNodeSize });
|
|
4582
4620
|
}
|
|
4583
4621
|
return baseHandlers;
|
|
4584
4622
|
}, [wb, runner, overrides, onCloseSelectionMenu]);
|
|
@@ -4624,6 +4662,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4624
4662
|
redo: "⌘/Ctrl + Shift + Z",
|
|
4625
4663
|
copy: "⌘/Ctrl + C",
|
|
4626
4664
|
paste: "⌘/Ctrl + V",
|
|
4665
|
+
duplicate: "⌘/Ctrl + D",
|
|
4627
4666
|
delete: "Delete",
|
|
4628
4667
|
};
|
|
4629
4668
|
// Keyboard shortcut handler
|
|
@@ -4674,12 +4713,26 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4674
4713
|
const selection = wb.getSelection();
|
|
4675
4714
|
if (selection.nodes.length > 0 || selection.edges.length > 0) {
|
|
4676
4715
|
e.preventDefault();
|
|
4677
|
-
|
|
4716
|
+
// If single node selected, use node context menu handler; otherwise use selection handler
|
|
4717
|
+
if (selection.nodes.length === 1 && nodeContextMenuHandlers?.onCopy) {
|
|
4718
|
+
nodeContextMenuHandlers.onCopy();
|
|
4719
|
+
}
|
|
4720
|
+
else if (selectionContextMenuHandlers.onCopy) {
|
|
4678
4721
|
selectionContextMenuHandlers.onCopy();
|
|
4679
4722
|
}
|
|
4680
4723
|
}
|
|
4681
4724
|
return;
|
|
4682
4725
|
}
|
|
4726
|
+
// Duplicate: Cmd/Ctrl + D
|
|
4727
|
+
if (modKey && key === "d" && !e.shiftKey && !e.altKey) {
|
|
4728
|
+
const selection = wb.getSelection();
|
|
4729
|
+
if (selection.nodes.length === 1 &&
|
|
4730
|
+
nodeContextMenuHandlers?.onDuplicate) {
|
|
4731
|
+
e.preventDefault();
|
|
4732
|
+
nodeContextMenuHandlers.onDuplicate();
|
|
4733
|
+
}
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4683
4736
|
// Paste: Cmd/Ctrl + V
|
|
4684
4737
|
if (modKey && key === "v" && !e.shiftKey && !e.altKey) {
|
|
4685
4738
|
e.preventDefault();
|
|
@@ -4709,6 +4762,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4709
4762
|
runner,
|
|
4710
4763
|
defaultContextMenuHandlers,
|
|
4711
4764
|
selectionContextMenuHandlers,
|
|
4765
|
+
nodeContextMenuHandlers,
|
|
4712
4766
|
rfInstanceRef,
|
|
4713
4767
|
]);
|
|
4714
4768
|
// Get custom renderers from UI extension registry (reactive to uiVersion changes)
|
|
@@ -4725,7 +4779,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4725
4779
|
const onMoveEnd = useCallback(() => {
|
|
4726
4780
|
if (rfInstanceRef.current) {
|
|
4727
4781
|
const viewport = rfInstanceRef.current.getViewport();
|
|
4728
|
-
wb.setViewport({ x: viewport.x, y: viewport.y, zoom: viewport.zoom }
|
|
4782
|
+
wb.setViewport({ x: viewport.x, y: viewport.y, zoom: viewport.zoom });
|
|
4729
4783
|
}
|
|
4730
4784
|
}, [wb]);
|
|
4731
4785
|
const viewportRef = useRef(null);
|
|
@@ -4761,7 +4815,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4761
4815
|
? { enableKeyboardShortcuts, keyboardShortcuts }
|
|
4762
4816
|
: {}) })) : (jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts })), !!nodeAtMenu &&
|
|
4763
4817
|
nodeContextMenuHandlers &&
|
|
4764
|
-
(NodeContextMenuRenderer ? (jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs
|
|
4818
|
+
(NodeContextMenuRenderer ? (jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs, ...(enableKeyboardShortcuts !== false
|
|
4819
|
+
? { enableKeyboardShortcuts, keyboardShortcuts }
|
|
4820
|
+
: {}) })) : (jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))), selectionMenuOpen && selectionMenuPos && (jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))] }) }) }));
|
|
4765
4821
|
});
|
|
4766
4822
|
|
|
4767
4823
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
|