@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/cjs/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var sparkGraph = require('@bian-womp/spark-graph');
|
|
4
|
+
var lod = require('lodash');
|
|
4
5
|
var sparkRemote = require('@bian-womp/spark-remote');
|
|
5
6
|
var react = require('@xyflow/react');
|
|
6
7
|
var React = require('react');
|
|
@@ -210,18 +211,19 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
210
211
|
inputs: options?.inputs,
|
|
211
212
|
copyOutputsFrom: options?.copyOutputsFrom,
|
|
212
213
|
},
|
|
213
|
-
|
|
214
|
+
...lod.pick(options, ["dry", "commit", "reason"]),
|
|
214
215
|
});
|
|
215
216
|
this.refreshValidation();
|
|
216
217
|
return id;
|
|
217
218
|
}
|
|
218
|
-
removeNode(nodeId) {
|
|
219
|
+
removeNode(nodeId, options) {
|
|
219
220
|
this.def.nodes = this.def.nodes.filter((n) => n.nodeId !== nodeId);
|
|
220
221
|
this.def.edges = this.def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
|
|
221
222
|
delete this.positions[nodeId];
|
|
222
223
|
this.emit("graphChanged", {
|
|
223
224
|
def: this.def,
|
|
224
225
|
change: { type: "removeNode", nodeId },
|
|
226
|
+
...options,
|
|
225
227
|
});
|
|
226
228
|
this.refreshValidation();
|
|
227
229
|
}
|
|
@@ -236,16 +238,17 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
236
238
|
this.emit("graphChanged", {
|
|
237
239
|
def: this.def,
|
|
238
240
|
change: { type: "connect", edgeId: id },
|
|
239
|
-
|
|
241
|
+
...options,
|
|
240
242
|
});
|
|
241
243
|
this.refreshValidation();
|
|
242
244
|
return id;
|
|
243
245
|
}
|
|
244
|
-
disconnect(edgeId) {
|
|
246
|
+
disconnect(edgeId, options) {
|
|
245
247
|
this.def.edges = this.def.edges.filter((e) => e.id !== edgeId);
|
|
246
248
|
this.emit("graphChanged", {
|
|
247
249
|
def: this.def,
|
|
248
250
|
change: { type: "disconnect", edgeId },
|
|
251
|
+
...options,
|
|
249
252
|
});
|
|
250
253
|
this.emit("validationChanged", this.validate());
|
|
251
254
|
}
|
|
@@ -274,32 +277,32 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
274
277
|
});
|
|
275
278
|
}
|
|
276
279
|
// Position and selection APIs for React Flow bridge
|
|
277
|
-
setPosition(nodeId, pos,
|
|
280
|
+
setPosition(nodeId, pos, options) {
|
|
278
281
|
this.positions[nodeId] = pos;
|
|
279
282
|
this.emit("graphUiChanged", {
|
|
280
283
|
def: this.def,
|
|
281
284
|
change: { type: "moveNode", nodeId, pos },
|
|
282
|
-
|
|
285
|
+
...options,
|
|
283
286
|
});
|
|
284
287
|
}
|
|
285
|
-
setPositions(map,
|
|
288
|
+
setPositions(map, options) {
|
|
286
289
|
this.positions = { ...map };
|
|
287
290
|
this.emit("graphUiChanged", {
|
|
288
291
|
def: this.def,
|
|
289
292
|
change: { type: "moveNodes" },
|
|
290
|
-
|
|
293
|
+
...options,
|
|
291
294
|
});
|
|
292
295
|
}
|
|
293
296
|
getPositions() {
|
|
294
297
|
return { ...this.positions };
|
|
295
298
|
}
|
|
296
|
-
setSelection(sel,
|
|
299
|
+
setSelection(sel, options) {
|
|
297
300
|
this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
|
|
298
301
|
this.emit("selectionChanged", this.selection);
|
|
299
302
|
this.emit("graphUiChanged", {
|
|
300
303
|
def: this.def,
|
|
301
304
|
change: { type: "selection" },
|
|
302
|
-
|
|
305
|
+
...options,
|
|
303
306
|
});
|
|
304
307
|
}
|
|
305
308
|
getSelection() {
|
|
@@ -311,7 +314,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
311
314
|
/**
|
|
312
315
|
* Delete all selected nodes and edges.
|
|
313
316
|
*/
|
|
314
|
-
deleteSelection() {
|
|
317
|
+
deleteSelection(options) {
|
|
315
318
|
const selection = this.getSelection();
|
|
316
319
|
// Delete all selected nodes (this will also remove connected edges)
|
|
317
320
|
for (const nodeId of selection.nodes) {
|
|
@@ -322,14 +325,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
322
325
|
this.disconnect(edgeId);
|
|
323
326
|
}
|
|
324
327
|
// Clear selection
|
|
325
|
-
this.setSelection({ nodes: [], edges: [] });
|
|
328
|
+
this.setSelection({ nodes: [], edges: [] }, options);
|
|
326
329
|
}
|
|
327
|
-
setViewport(viewport,
|
|
330
|
+
setViewport(viewport, options) {
|
|
328
331
|
this.viewport = { ...viewport };
|
|
329
332
|
this.emit("graphUiChanged", {
|
|
330
333
|
def: this.def,
|
|
331
334
|
change: { type: "viewport" },
|
|
332
|
-
|
|
335
|
+
...options,
|
|
333
336
|
});
|
|
334
337
|
}
|
|
335
338
|
getViewport() {
|
|
@@ -474,7 +477,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
474
477
|
* Returns the mapping from original node IDs to new node IDs.
|
|
475
478
|
* Uses copyOutputsFrom to copy outputs from original nodes (like duplicate does).
|
|
476
479
|
*/
|
|
477
|
-
pasteCopiedData(data, center) {
|
|
480
|
+
pasteCopiedData(data, center, options) {
|
|
478
481
|
const nodeIdMap = new Map();
|
|
479
482
|
const edgeIds = [];
|
|
480
483
|
// Add nodes
|
|
@@ -514,10 +517,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
514
517
|
}
|
|
515
518
|
}
|
|
516
519
|
// Select the newly pasted nodes
|
|
517
|
-
this.setSelection({
|
|
518
|
-
nodes: Array.from(nodeIdMap.values()),
|
|
519
|
-
edges: edgeIds,
|
|
520
|
-
});
|
|
520
|
+
this.setSelection({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
|
|
521
521
|
return { nodeIdMap, edgeIds };
|
|
522
522
|
}
|
|
523
523
|
/**
|
|
@@ -686,7 +686,7 @@ class AbstractGraphRunner {
|
|
|
686
686
|
return false;
|
|
687
687
|
}
|
|
688
688
|
// Optional commit support
|
|
689
|
-
async commit() { }
|
|
689
|
+
async commit(_reason) { }
|
|
690
690
|
}
|
|
691
691
|
|
|
692
692
|
// Counter for generating readable runner IDs
|
|
@@ -1433,10 +1433,10 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1433
1433
|
const client = await this.ensureClient();
|
|
1434
1434
|
await client.setExtData(data);
|
|
1435
1435
|
}
|
|
1436
|
-
async commit() {
|
|
1436
|
+
async commit(reason) {
|
|
1437
1437
|
const client = await this.ensureClient();
|
|
1438
1438
|
try {
|
|
1439
|
-
await client.commit();
|
|
1439
|
+
await client.commit(reason);
|
|
1440
1440
|
}
|
|
1441
1441
|
catch (err) {
|
|
1442
1442
|
console.error("[RemoteGraphRunner] Error committing:", err);
|
|
@@ -1916,7 +1916,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1916
1916
|
wb.connect({
|
|
1917
1917
|
source: { nodeId: params.source, handle: params.sourceHandle },
|
|
1918
1918
|
target: { nodeId: params.target, handle: params.targetHandle },
|
|
1919
|
-
});
|
|
1919
|
+
}, { commit: true });
|
|
1920
1920
|
}, [wb]);
|
|
1921
1921
|
const onNodesChange = React.useCallback((changes) => {
|
|
1922
1922
|
// Apply position updates continuously, but mark commit only on drag end
|
|
@@ -1959,7 +1959,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1959
1959
|
});
|
|
1960
1960
|
}
|
|
1961
1961
|
}, [wb]);
|
|
1962
|
-
const onEdgesDelete = React.useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
|
|
1962
|
+
const onEdgesDelete = React.useCallback((edges) => edges.forEach((e, idx) => wb.disconnect(e.id, { commit: idx === edges.length - 1 })), [wb]);
|
|
1963
1963
|
const onEdgesChange = React.useCallback((changes) => {
|
|
1964
1964
|
const current = wb.getSelection();
|
|
1965
1965
|
const nextEdgeIds = new Set(current.edges);
|
|
@@ -1995,8 +1995,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1995
1995
|
}
|
|
1996
1996
|
}, [wb]);
|
|
1997
1997
|
const onNodesDelete = React.useCallback((nodes) => {
|
|
1998
|
-
|
|
1999
|
-
wb.removeNode(n.id);
|
|
1998
|
+
nodes.forEach((n, idx) => wb.removeNode(n.id, { commit: idx === nodes.length - 1 }));
|
|
2000
1999
|
}, [wb]);
|
|
2001
2000
|
return {
|
|
2002
2001
|
onConnect,
|
|
@@ -2689,7 +2688,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2689
2688
|
}
|
|
2690
2689
|
curX += maxWidth + H_GAP;
|
|
2691
2690
|
}
|
|
2692
|
-
wb.setPositions(pos, { commit: true });
|
|
2691
|
+
wb.setPositions(pos, { commit: true, reason: "auto-layout" });
|
|
2693
2692
|
}, [wb, registry, overrides?.getDefaultNodeSize]);
|
|
2694
2693
|
const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
|
|
2695
2694
|
const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
|
|
@@ -2945,11 +2944,36 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2945
2944
|
}
|
|
2946
2945
|
});
|
|
2947
2946
|
const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
|
|
2947
|
+
// Build detailed reason from change type
|
|
2948
|
+
let reason = "graph-changed";
|
|
2949
|
+
if (event.change) {
|
|
2950
|
+
const changeType = event.change.type;
|
|
2951
|
+
if (changeType === "addNode") {
|
|
2952
|
+
reason = "add-node";
|
|
2953
|
+
}
|
|
2954
|
+
else if (changeType === "removeNode") {
|
|
2955
|
+
reason = "remove-node";
|
|
2956
|
+
}
|
|
2957
|
+
else if (changeType === "connect") {
|
|
2958
|
+
reason = "connect-edge";
|
|
2959
|
+
}
|
|
2960
|
+
else if (changeType === "disconnect") {
|
|
2961
|
+
reason = "disconnect-edge";
|
|
2962
|
+
}
|
|
2963
|
+
else if (changeType === "updateParams") {
|
|
2964
|
+
reason = "update-node-params";
|
|
2965
|
+
}
|
|
2966
|
+
else if (changeType === "updateEdgeType") {
|
|
2967
|
+
reason = "update-edge-type";
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2948
2970
|
if (!runner.isRunning()) {
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2971
|
+
if (event.commit) {
|
|
2972
|
+
// If runner not running, commit immediately (no update needed)
|
|
2973
|
+
await runner.commit(reason).catch((err) => {
|
|
2974
|
+
console.error("[WorkbenchContext] Error committing:", err);
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2953
2977
|
return;
|
|
2954
2978
|
}
|
|
2955
2979
|
try {
|
|
@@ -2974,10 +2998,12 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2974
2998
|
else {
|
|
2975
2999
|
await runner.update(event.def, { dry: event.dry });
|
|
2976
3000
|
}
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3001
|
+
if (event.commit) {
|
|
3002
|
+
// Wait for update to complete, then commit
|
|
3003
|
+
await runner.commit(event.reason ?? reason).catch((err) => {
|
|
3004
|
+
console.error("[WorkbenchContext] Error committing after update:", err);
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
2981
3007
|
}
|
|
2982
3008
|
catch (err) {
|
|
2983
3009
|
console.error("[WorkbenchContext] Error updating graph:", err);
|
|
@@ -2987,15 +3013,22 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2987
3013
|
const offWbSelectionChanged = wb.on("selectionChanged", async (sel) => {
|
|
2988
3014
|
setSelectedNodeId(sel.nodes?.[0]);
|
|
2989
3015
|
setSelectedEdgeId(sel.edges?.[0]);
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3016
|
+
if (sel.commit) {
|
|
3017
|
+
// Commit on selection change
|
|
3018
|
+
await runner.commit(sel.reason ?? "selection").catch((err) => {
|
|
3019
|
+
console.error("[WorkbenchContext] Error committing selection change:", err);
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
2994
3022
|
});
|
|
2995
3023
|
const offWbGraphUiChanged = wb.on("graphUiChanged", async (event) => {
|
|
2996
3024
|
// Only commit if commit flag is true (e.g., drag end, not during dragging)
|
|
2997
3025
|
if (event.commit) {
|
|
2998
|
-
|
|
3026
|
+
if (event.change) {
|
|
3027
|
+
event.change.type;
|
|
3028
|
+
}
|
|
3029
|
+
await runner
|
|
3030
|
+
.commit(event.reason ?? "ui-changed")
|
|
3031
|
+
.catch((err) => {
|
|
2999
3032
|
console.error("[WorkbenchContext] Error committing UI changes:", err);
|
|
3000
3033
|
});
|
|
3001
3034
|
}
|
|
@@ -3211,6 +3244,7 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3211
3244
|
try {
|
|
3212
3245
|
const typeId = outputTypesMap?.[nodeId]?.[handleId];
|
|
3213
3246
|
const raw = outputsMap?.[nodeId]?.[handleId];
|
|
3247
|
+
let newNodeId;
|
|
3214
3248
|
if (!typeId || raw === undefined)
|
|
3215
3249
|
return;
|
|
3216
3250
|
const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
|
|
@@ -3236,23 +3270,21 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3236
3270
|
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
3237
3271
|
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
3238
3272
|
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3239
|
-
wb.addNode({
|
|
3273
|
+
newNodeId = wb.addNode({
|
|
3240
3274
|
typeId: singleTarget.nodeTypeId,
|
|
3241
3275
|
position: { x: pos.x + 180, y: pos.y },
|
|
3242
3276
|
}, { inputs: { [singleTarget.inputHandle]: coerced } });
|
|
3243
|
-
return;
|
|
3244
3277
|
}
|
|
3245
|
-
if (isArray && arrTarget) {
|
|
3278
|
+
else if (isArray && arrTarget) {
|
|
3246
3279
|
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
3247
3280
|
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
3248
3281
|
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3249
|
-
wb.addNode({
|
|
3282
|
+
newNodeId = wb.addNode({
|
|
3250
3283
|
typeId: arrTarget.nodeTypeId,
|
|
3251
3284
|
position: { x: pos.x + 180, y: pos.y },
|
|
3252
3285
|
}, { inputs: { [arrTarget.inputHandle]: coerced } });
|
|
3253
|
-
return;
|
|
3254
3286
|
}
|
|
3255
|
-
if (isArray && elemTarget) {
|
|
3287
|
+
else if (isArray && elemTarget) {
|
|
3256
3288
|
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
3257
3289
|
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
3258
3290
|
const src = unwrap(raw);
|
|
@@ -3264,19 +3296,21 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3264
3296
|
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
3265
3297
|
const col = idx % COLS;
|
|
3266
3298
|
const row = Math.floor(idx / COLS);
|
|
3267
|
-
wb.addNode({
|
|
3299
|
+
newNodeId = wb.addNode({
|
|
3268
3300
|
typeId: elemTarget.nodeTypeId,
|
|
3269
3301
|
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
3270
3302
|
}, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
|
|
3271
3303
|
}
|
|
3272
|
-
|
|
3304
|
+
}
|
|
3305
|
+
if (newNodeId) {
|
|
3306
|
+
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "bake" });
|
|
3273
3307
|
}
|
|
3274
3308
|
}
|
|
3275
3309
|
catch { }
|
|
3276
3310
|
};
|
|
3277
3311
|
return {
|
|
3278
3312
|
onDelete: () => {
|
|
3279
|
-
wb.removeNode(nodeId);
|
|
3313
|
+
wb.removeNode(nodeId, { commit: true });
|
|
3280
3314
|
onClose();
|
|
3281
3315
|
},
|
|
3282
3316
|
onDuplicate: async () => {
|
|
@@ -3301,10 +3335,7 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3301
3335
|
dry: true,
|
|
3302
3336
|
});
|
|
3303
3337
|
// Select the newly duplicated node
|
|
3304
|
-
wb.setSelection({
|
|
3305
|
-
nodes: [newNodeId],
|
|
3306
|
-
edges: [],
|
|
3307
|
-
});
|
|
3338
|
+
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
|
|
3308
3339
|
onClose();
|
|
3309
3340
|
},
|
|
3310
3341
|
onDuplicateWithEdges: async () => {
|
|
@@ -3337,10 +3368,9 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3337
3368
|
}, { dry: true });
|
|
3338
3369
|
}
|
|
3339
3370
|
// Select the newly duplicated node and edges
|
|
3340
|
-
|
|
3341
|
-
nodes: [newNodeId],
|
|
3342
|
-
|
|
3343
|
-
});
|
|
3371
|
+
if (newNodeId) {
|
|
3372
|
+
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate-with-edges" });
|
|
3373
|
+
}
|
|
3344
3374
|
onClose();
|
|
3345
3375
|
},
|
|
3346
3376
|
onRunPull: async () => {
|
|
@@ -3414,7 +3444,7 @@ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onC
|
|
|
3414
3444
|
onClose();
|
|
3415
3445
|
},
|
|
3416
3446
|
onDelete: () => {
|
|
3417
|
-
wb.deleteSelection();
|
|
3447
|
+
wb.deleteSelection({ commit: true, reason: "delete-selection" });
|
|
3418
3448
|
onClose();
|
|
3419
3449
|
},
|
|
3420
3450
|
onClose,
|
|
@@ -4096,10 +4126,14 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
|
|
|
4096
4126
|
}, children: [hasPasteData && handlers.onPaste && (jsxRuntime.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: [jsxRuntime.jsx("span", { children: "Paste" }), enableKeyboardShortcuts && keyboardShortcuts.paste && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.paste) }))] })), (handlers.onUndo || handlers.onRedo) && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [hasPasteData && handlers.onPaste && (jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" })), handlers.onUndo && (jsxRuntime.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: [jsxRuntime.jsx("span", { children: "Undo" }), enableKeyboardShortcuts && keyboardShortcuts.undo && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.undo) }))] })), handlers.onRedo && (jsxRuntime.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: [jsxRuntime.jsx("span", { children: "Redo" }), enableKeyboardShortcuts && keyboardShortcuts.redo && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.redo) }))] }))] })), hasPasteData &&
|
|
4097
4127
|
handlers.onPaste &&
|
|
4098
4128
|
!handlers.onUndo &&
|
|
4099
|
-
!handlers.onRedo && jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxRuntime.jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsxRuntime.jsx("div", { className: "px-2 pb-1", children: jsxRuntime.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() }) }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsxRuntime.jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
|
|
4129
|
+
!handlers.onRedo && jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxRuntime.jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsxRuntime.jsx("div", { className: "px-2 pb-1", children: jsxRuntime.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() }) }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsxRuntime.jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
|
|
4100
4130
|
}
|
|
4101
4131
|
|
|
4102
|
-
function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs,
|
|
4132
|
+
function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
4133
|
+
copy: "⌘/Ctrl + C",
|
|
4134
|
+
duplicate: "⌘/Ctrl + D",
|
|
4135
|
+
delete: "Delete",
|
|
4136
|
+
}, }) {
|
|
4103
4137
|
const ref = React.useRef(null);
|
|
4104
4138
|
// outside click + ESC
|
|
4105
4139
|
React.useEffect(() => {
|
|
@@ -4126,6 +4160,12 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
4126
4160
|
if (open)
|
|
4127
4161
|
ref.current?.focus();
|
|
4128
4162
|
}, [open]);
|
|
4163
|
+
// Helper to format shortcut for current platform
|
|
4164
|
+
const formatShortcut = (shortcut) => {
|
|
4165
|
+
const isMac = typeof navigator !== "undefined" &&
|
|
4166
|
+
navigator.userAgent.toLowerCase().includes("mac");
|
|
4167
|
+
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4168
|
+
};
|
|
4129
4169
|
if (!open || !clientPos || !nodeId)
|
|
4130
4170
|
return null;
|
|
4131
4171
|
// clamp
|
|
@@ -4137,7 +4177,7 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
4137
4177
|
return (jsxRuntime.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) => {
|
|
4138
4178
|
e.preventDefault();
|
|
4139
4179
|
e.stopPropagation();
|
|
4140
|
-
}, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.
|
|
4180
|
+
}, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.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: [jsxRuntime.jsx("span", { children: "Delete" }), enableKeyboardShortcuts && keyboardShortcuts.delete && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.delete) }))] }), jsxRuntime.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: [jsxRuntime.jsx("span", { children: "Duplicate" }), enableKeyboardShortcuts && keyboardShortcuts.duplicate && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.duplicate) }))] }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), canRunPull && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.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: [jsxRuntime.jsx("span", { children: "Copy" }), enableKeyboardShortcuts && keyboardShortcuts.copy && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.copy) }))] }), jsxRuntime.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 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h)))] }))] }));
|
|
4141
4181
|
}
|
|
4142
4182
|
|
|
4143
4183
|
function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
@@ -4525,7 +4565,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4525
4565
|
setNodeMenuOpen(false);
|
|
4526
4566
|
setSelectionMenuOpen(false);
|
|
4527
4567
|
};
|
|
4528
|
-
const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
|
|
4568
|
+
const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs, commit: true }), [wb]);
|
|
4529
4569
|
const onCloseMenu = React.useCallback(() => {
|
|
4530
4570
|
setMenuOpen(false);
|
|
4531
4571
|
}, []);
|
|
@@ -4557,7 +4597,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4557
4597
|
const data = storage.get();
|
|
4558
4598
|
if (!data)
|
|
4559
4599
|
return;
|
|
4560
|
-
wb.pasteCopiedData(data, position);
|
|
4600
|
+
wb.pasteCopiedData(data, position, { commit: true, reason: "paste" });
|
|
4561
4601
|
onCloseMenu();
|
|
4562
4602
|
}, runner, () => storage.get(), () => storage.set(null));
|
|
4563
4603
|
if (overrides?.getDefaultContextMenuHandlers) {
|
|
@@ -4578,9 +4618,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4578
4618
|
}, runner);
|
|
4579
4619
|
if (overrides?.getSelectionContextMenuHandlers) {
|
|
4580
4620
|
const selection = wb.getSelection();
|
|
4581
|
-
return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, {
|
|
4582
|
-
getDefaultNodeSize: overrides.getDefaultNodeSize,
|
|
4583
|
-
});
|
|
4621
|
+
return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, { getDefaultNodeSize: overrides.getDefaultNodeSize });
|
|
4584
4622
|
}
|
|
4585
4623
|
return baseHandlers;
|
|
4586
4624
|
}, [wb, runner, overrides, onCloseSelectionMenu]);
|
|
@@ -4626,6 +4664,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4626
4664
|
redo: "⌘/Ctrl + Shift + Z",
|
|
4627
4665
|
copy: "⌘/Ctrl + C",
|
|
4628
4666
|
paste: "⌘/Ctrl + V",
|
|
4667
|
+
duplicate: "⌘/Ctrl + D",
|
|
4629
4668
|
delete: "Delete",
|
|
4630
4669
|
};
|
|
4631
4670
|
// Keyboard shortcut handler
|
|
@@ -4676,12 +4715,26 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4676
4715
|
const selection = wb.getSelection();
|
|
4677
4716
|
if (selection.nodes.length > 0 || selection.edges.length > 0) {
|
|
4678
4717
|
e.preventDefault();
|
|
4679
|
-
|
|
4718
|
+
// If single node selected, use node context menu handler; otherwise use selection handler
|
|
4719
|
+
if (selection.nodes.length === 1 && nodeContextMenuHandlers?.onCopy) {
|
|
4720
|
+
nodeContextMenuHandlers.onCopy();
|
|
4721
|
+
}
|
|
4722
|
+
else if (selectionContextMenuHandlers.onCopy) {
|
|
4680
4723
|
selectionContextMenuHandlers.onCopy();
|
|
4681
4724
|
}
|
|
4682
4725
|
}
|
|
4683
4726
|
return;
|
|
4684
4727
|
}
|
|
4728
|
+
// Duplicate: Cmd/Ctrl + D
|
|
4729
|
+
if (modKey && key === "d" && !e.shiftKey && !e.altKey) {
|
|
4730
|
+
const selection = wb.getSelection();
|
|
4731
|
+
if (selection.nodes.length === 1 &&
|
|
4732
|
+
nodeContextMenuHandlers?.onDuplicate) {
|
|
4733
|
+
e.preventDefault();
|
|
4734
|
+
nodeContextMenuHandlers.onDuplicate();
|
|
4735
|
+
}
|
|
4736
|
+
return;
|
|
4737
|
+
}
|
|
4685
4738
|
// Paste: Cmd/Ctrl + V
|
|
4686
4739
|
if (modKey && key === "v" && !e.shiftKey && !e.altKey) {
|
|
4687
4740
|
e.preventDefault();
|
|
@@ -4711,6 +4764,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4711
4764
|
runner,
|
|
4712
4765
|
defaultContextMenuHandlers,
|
|
4713
4766
|
selectionContextMenuHandlers,
|
|
4767
|
+
nodeContextMenuHandlers,
|
|
4714
4768
|
rfInstanceRef,
|
|
4715
4769
|
]);
|
|
4716
4770
|
// Get custom renderers from UI extension registry (reactive to uiVersion changes)
|
|
@@ -4727,7 +4781,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4727
4781
|
const onMoveEnd = React.useCallback(() => {
|
|
4728
4782
|
if (rfInstanceRef.current) {
|
|
4729
4783
|
const viewport = rfInstanceRef.current.getViewport();
|
|
4730
|
-
wb.setViewport({ x: viewport.x, y: viewport.y, zoom: viewport.zoom }
|
|
4784
|
+
wb.setViewport({ x: viewport.x, y: viewport.y, zoom: viewport.zoom });
|
|
4731
4785
|
}
|
|
4732
4786
|
}, [wb]);
|
|
4733
4787
|
const viewportRef = React.useRef(null);
|
|
@@ -4763,7 +4817,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4763
4817
|
? { enableKeyboardShortcuts, keyboardShortcuts }
|
|
4764
4818
|
: {}) })) : (jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts })), !!nodeAtMenu &&
|
|
4765
4819
|
nodeContextMenuHandlers &&
|
|
4766
|
-
(NodeContextMenuRenderer ? (jsxRuntime.jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs
|
|
4820
|
+
(NodeContextMenuRenderer ? (jsxRuntime.jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs, ...(enableKeyboardShortcuts !== false
|
|
4821
|
+
? { enableKeyboardShortcuts, keyboardShortcuts }
|
|
4822
|
+
: {}) })) : (jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))), selectionMenuOpen && selectionMenuPos && (jsxRuntime.jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))] }) }) }));
|
|
4767
4823
|
});
|
|
4768
4824
|
|
|
4769
4825
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
|