@bian-womp/spark-workbench 0.2.68 → 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 +390 -88
- 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/DefaultContextMenu.d.ts +1 -1
- package/lib/cjs/src/misc/DefaultContextMenu.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/SelectionContextMenu.d.ts +1 -1
- package/lib/cjs/src/misc/SelectionContextMenu.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 +22 -0
- package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts +1 -1
- package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +11 -22
- 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 +6 -1
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/IGraphRunner.d.ts +6 -1
- package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +6 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/lib/esm/index.js +390 -88
- 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/DefaultContextMenu.d.ts +1 -1
- package/lib/esm/src/misc/DefaultContextMenu.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/SelectionContextMenu.d.ts +1 -1
- package/lib/esm/src/misc/SelectionContextMenu.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 +22 -0
- package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
- package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts +1 -1
- package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts +11 -22
- 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 +6 -1
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/IGraphRunner.d.ts +6 -1
- package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +6 -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
|
/**
|
|
@@ -627,11 +627,10 @@ class AbstractGraphRunner {
|
|
|
627
627
|
this.runtime.resume();
|
|
628
628
|
// Create and launch new engine (to be implemented by subclasses)
|
|
629
629
|
await this.createAndLaunchEngine(opts);
|
|
630
|
-
// Re-apply staged inputs to new engine
|
|
630
|
+
// Re-apply staged inputs to new engine using runner's setInputs method
|
|
631
|
+
// This ensures consistency and proper handling of staged inputs
|
|
631
632
|
for (const [nodeId, map] of Object.entries(currentInputs)) {
|
|
632
|
-
|
|
633
|
-
this.engine.setInputs(nodeId, map);
|
|
634
|
-
}
|
|
633
|
+
await this.setInputs(nodeId, map);
|
|
635
634
|
}
|
|
636
635
|
}
|
|
637
636
|
getInputDefaults(def) {
|
|
@@ -673,6 +672,21 @@ class AbstractGraphRunner {
|
|
|
673
672
|
getRunningEngine() {
|
|
674
673
|
return this.runningKind;
|
|
675
674
|
}
|
|
675
|
+
// Optional undo/redo support
|
|
676
|
+
async undo() {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
async redo() {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
async canUndo() {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
async canRedo() {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
// Optional commit support
|
|
689
|
+
async commit(_reason) { }
|
|
676
690
|
}
|
|
677
691
|
|
|
678
692
|
// Counter for generating readable runner IDs
|
|
@@ -1297,8 +1311,11 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1297
1311
|
this.engine = eng;
|
|
1298
1312
|
this.runningKind = opts?.engine ?? "push";
|
|
1299
1313
|
this.emit("status", { running: true, engine: this.runningKind });
|
|
1314
|
+
// Re-apply staged inputs using client.setInputs for consistency
|
|
1300
1315
|
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
1301
|
-
|
|
1316
|
+
await client.setInputs(nodeId, map).catch(() => {
|
|
1317
|
+
// Ignore errors during launch - inputs will be set when user calls setInputs
|
|
1318
|
+
});
|
|
1302
1319
|
}
|
|
1303
1320
|
}
|
|
1304
1321
|
/**
|
|
@@ -1340,9 +1357,11 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1340
1357
|
this.engine = eng;
|
|
1341
1358
|
this.runningKind = opts?.engine ?? "push";
|
|
1342
1359
|
this.emit("status", { running: true, engine: this.runningKind });
|
|
1343
|
-
// Re-apply staged inputs
|
|
1360
|
+
// Re-apply staged inputs using client.setInputs for consistency
|
|
1344
1361
|
for (const [nodeId, map] of Object.entries(currentInputs)) {
|
|
1345
|
-
|
|
1362
|
+
await client.setInputs(nodeId, map).catch(() => {
|
|
1363
|
+
// Ignore errors during engine switch - inputs will be set when user calls setInputs
|
|
1364
|
+
});
|
|
1346
1365
|
}
|
|
1347
1366
|
}
|
|
1348
1367
|
async step() {
|
|
@@ -1357,7 +1376,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1357
1376
|
const client = await this.ensureClient();
|
|
1358
1377
|
await client.flush();
|
|
1359
1378
|
}
|
|
1360
|
-
setInputs(nodeId, inputs, options) {
|
|
1379
|
+
async setInputs(nodeId, inputs, options) {
|
|
1361
1380
|
// Update staged inputs (for getInputs to work correctly)
|
|
1362
1381
|
if (!this.stagedInputs[nodeId])
|
|
1363
1382
|
this.stagedInputs[nodeId] = {};
|
|
@@ -1369,21 +1388,17 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1369
1388
|
this.stagedInputs[nodeId][handle] = value;
|
|
1370
1389
|
}
|
|
1371
1390
|
}
|
|
1372
|
-
//
|
|
1373
|
-
|
|
1374
|
-
|
|
1391
|
+
// Use transport.request instead of transport.send for consistency
|
|
1392
|
+
const client = await this.ensureClient();
|
|
1393
|
+
try {
|
|
1394
|
+
await client.setInputs(nodeId, inputs, options);
|
|
1375
1395
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// Emit synthetic events if connection fails
|
|
1383
|
-
for (const [handle, value] of Object.entries(inputs)) {
|
|
1384
|
-
this.emit("value", { nodeId, handle, value, io: "input" });
|
|
1385
|
-
}
|
|
1386
|
-
});
|
|
1396
|
+
catch (err) {
|
|
1397
|
+
// Emit synthetic events if connection fails
|
|
1398
|
+
for (const [handle, value] of Object.entries(inputs)) {
|
|
1399
|
+
this.emit("value", { nodeId, handle, value, io: "input" });
|
|
1400
|
+
}
|
|
1401
|
+
throw err;
|
|
1387
1402
|
}
|
|
1388
1403
|
}
|
|
1389
1404
|
async copyOutputs(fromNodeId, toNodeId, options) {
|
|
@@ -1418,6 +1433,52 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1418
1433
|
const client = await this.ensureClient();
|
|
1419
1434
|
await client.setExtData(data);
|
|
1420
1435
|
}
|
|
1436
|
+
async commit(reason) {
|
|
1437
|
+
const client = await this.ensureClient();
|
|
1438
|
+
try {
|
|
1439
|
+
await client.commit(reason);
|
|
1440
|
+
}
|
|
1441
|
+
catch (err) {
|
|
1442
|
+
console.error("[RemoteGraphRunner] Error committing:", err);
|
|
1443
|
+
throw err;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
async undo() {
|
|
1447
|
+
const client = await this.ensureClient();
|
|
1448
|
+
try {
|
|
1449
|
+
return await client.undo();
|
|
1450
|
+
}
|
|
1451
|
+
catch {
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async redo() {
|
|
1456
|
+
const client = await this.ensureClient();
|
|
1457
|
+
try {
|
|
1458
|
+
return await client.redo();
|
|
1459
|
+
}
|
|
1460
|
+
catch {
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
async canUndo() {
|
|
1465
|
+
const client = await this.ensureClient();
|
|
1466
|
+
try {
|
|
1467
|
+
return await client.canUndo();
|
|
1468
|
+
}
|
|
1469
|
+
catch {
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
async canRedo() {
|
|
1474
|
+
const client = await this.ensureClient();
|
|
1475
|
+
try {
|
|
1476
|
+
return await client.canRedo();
|
|
1477
|
+
}
|
|
1478
|
+
catch {
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1421
1482
|
async snapshotFull() {
|
|
1422
1483
|
const client = await this.ensureClient();
|
|
1423
1484
|
try {
|
|
@@ -1855,7 +1916,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1855
1916
|
wb.connect({
|
|
1856
1917
|
source: { nodeId: params.source, handle: params.sourceHandle },
|
|
1857
1918
|
target: { nodeId: params.target, handle: params.targetHandle },
|
|
1858
|
-
});
|
|
1919
|
+
}, { commit: true });
|
|
1859
1920
|
}, [wb]);
|
|
1860
1921
|
const onNodesChange = React.useCallback((changes) => {
|
|
1861
1922
|
// Apply position updates continuously, but mark commit only on drag end
|
|
@@ -1898,7 +1959,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1898
1959
|
});
|
|
1899
1960
|
}
|
|
1900
1961
|
}, [wb]);
|
|
1901
|
-
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]);
|
|
1902
1963
|
const onEdgesChange = React.useCallback((changes) => {
|
|
1903
1964
|
const current = wb.getSelection();
|
|
1904
1965
|
const nextEdgeIds = new Set(current.edges);
|
|
@@ -1934,8 +1995,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1934
1995
|
}
|
|
1935
1996
|
}, [wb]);
|
|
1936
1997
|
const onNodesDelete = React.useCallback((nodes) => {
|
|
1937
|
-
|
|
1938
|
-
wb.removeNode(n.id);
|
|
1998
|
+
nodes.forEach((n, idx) => wb.removeNode(n.id, { commit: idx === nodes.length - 1 }));
|
|
1939
1999
|
}, [wb]);
|
|
1940
2000
|
return {
|
|
1941
2001
|
onConnect,
|
|
@@ -2628,7 +2688,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2628
2688
|
}
|
|
2629
2689
|
curX += maxWidth + H_GAP;
|
|
2630
2690
|
}
|
|
2631
|
-
wb.setPositions(pos, { commit: true });
|
|
2691
|
+
wb.setPositions(pos, { commit: true, reason: "auto-layout" });
|
|
2632
2692
|
}, [wb, registry, overrides?.getDefaultNodeSize]);
|
|
2633
2693
|
const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
|
|
2634
2694
|
const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
|
|
@@ -2870,7 +2930,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2870
2930
|
}
|
|
2871
2931
|
return add("workbench", "graphChanged")(event);
|
|
2872
2932
|
});
|
|
2873
|
-
const
|
|
2933
|
+
const offWbGraphUiChangedForLog = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
2874
2934
|
const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
2875
2935
|
// Ensure newly added nodes start as invalidated until first evaluation
|
|
2876
2936
|
const offWbAddNode = wb.on("graphChanged", (e) => {
|
|
@@ -2884,39 +2944,94 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2884
2944
|
}
|
|
2885
2945
|
});
|
|
2886
2946
|
const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
|
|
2887
|
-
|
|
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
|
+
}
|
|
2970
|
+
if (!runner.isRunning()) {
|
|
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
|
+
}
|
|
2888
2977
|
return;
|
|
2978
|
+
}
|
|
2889
2979
|
try {
|
|
2890
2980
|
if (event.change?.type === "addNode") {
|
|
2891
2981
|
const { nodeId, inputs, copyOutputsFrom } = event.change;
|
|
2892
2982
|
if (event.dry) {
|
|
2893
2983
|
await runner.update(event.def, { dry: true });
|
|
2894
2984
|
if (inputs) {
|
|
2895
|
-
runner.setInputs(nodeId, inputs, { dry: true });
|
|
2985
|
+
await runner.setInputs(nodeId, inputs, { dry: true });
|
|
2896
2986
|
}
|
|
2897
2987
|
if (copyOutputsFrom) {
|
|
2898
|
-
runner.copyOutputs(copyOutputsFrom, nodeId, { dry: true });
|
|
2988
|
+
await runner.copyOutputs(copyOutputsFrom, nodeId, { dry: true });
|
|
2899
2989
|
}
|
|
2900
2990
|
}
|
|
2901
2991
|
else {
|
|
2902
2992
|
await runner.update(event.def, { dry: !!inputs });
|
|
2903
2993
|
if (inputs) {
|
|
2904
|
-
runner.setInputs(nodeId, inputs, { dry: false });
|
|
2994
|
+
await runner.setInputs(nodeId, inputs, { dry: false });
|
|
2905
2995
|
}
|
|
2906
2996
|
}
|
|
2907
2997
|
}
|
|
2908
2998
|
else {
|
|
2909
2999
|
await runner.update(event.def, { dry: event.dry });
|
|
2910
3000
|
}
|
|
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
|
+
}
|
|
2911
3007
|
}
|
|
2912
3008
|
catch (err) {
|
|
2913
3009
|
console.error("[WorkbenchContext] Error updating graph:", err);
|
|
2914
3010
|
}
|
|
2915
3011
|
});
|
|
2916
3012
|
const offWbdSetValidation = wb.on("validationChanged", (r) => setValidation(r));
|
|
2917
|
-
const offWbSelectionChanged = wb.on("selectionChanged", (sel) => {
|
|
3013
|
+
const offWbSelectionChanged = wb.on("selectionChanged", async (sel) => {
|
|
2918
3014
|
setSelectedNodeId(sel.nodes?.[0]);
|
|
2919
3015
|
setSelectedEdgeId(sel.edges?.[0]);
|
|
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
|
+
}
|
|
3022
|
+
});
|
|
3023
|
+
const offWbGraphUiChanged = wb.on("graphUiChanged", async (event) => {
|
|
3024
|
+
// Only commit if commit flag is true (e.g., drag end, not during dragging)
|
|
3025
|
+
if (event.commit) {
|
|
3026
|
+
if (event.change) {
|
|
3027
|
+
event.change.type;
|
|
3028
|
+
}
|
|
3029
|
+
await runner
|
|
3030
|
+
.commit(event.reason ?? "ui-changed")
|
|
3031
|
+
.catch((err) => {
|
|
3032
|
+
console.error("[WorkbenchContext] Error committing UI changes:", err);
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
2920
3035
|
});
|
|
2921
3036
|
const offWbError = wb.on("error", add("workbench", "error"));
|
|
2922
3037
|
// Registry updates: swap registry and refresh graph validation/UI
|
|
@@ -2953,6 +3068,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2953
3068
|
offRunnerInvalidate();
|
|
2954
3069
|
offRunnerStats();
|
|
2955
3070
|
offWbGraphChanged();
|
|
3071
|
+
offWbGraphUiChangedForLog();
|
|
2956
3072
|
offWbGraphUiChanged();
|
|
2957
3073
|
offWbValidationChanged();
|
|
2958
3074
|
offWbError();
|
|
@@ -3128,6 +3244,7 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3128
3244
|
try {
|
|
3129
3245
|
const typeId = outputTypesMap?.[nodeId]?.[handleId];
|
|
3130
3246
|
const raw = outputsMap?.[nodeId]?.[handleId];
|
|
3247
|
+
let newNodeId;
|
|
3131
3248
|
if (!typeId || raw === undefined)
|
|
3132
3249
|
return;
|
|
3133
3250
|
const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
|
|
@@ -3153,23 +3270,21 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3153
3270
|
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
3154
3271
|
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
3155
3272
|
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3156
|
-
wb.addNode({
|
|
3273
|
+
newNodeId = wb.addNode({
|
|
3157
3274
|
typeId: singleTarget.nodeTypeId,
|
|
3158
3275
|
position: { x: pos.x + 180, y: pos.y },
|
|
3159
3276
|
}, { inputs: { [singleTarget.inputHandle]: coerced } });
|
|
3160
|
-
return;
|
|
3161
3277
|
}
|
|
3162
|
-
if (isArray && arrTarget) {
|
|
3278
|
+
else if (isArray && arrTarget) {
|
|
3163
3279
|
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
3164
3280
|
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
3165
3281
|
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3166
|
-
wb.addNode({
|
|
3282
|
+
newNodeId = wb.addNode({
|
|
3167
3283
|
typeId: arrTarget.nodeTypeId,
|
|
3168
3284
|
position: { x: pos.x + 180, y: pos.y },
|
|
3169
3285
|
}, { inputs: { [arrTarget.inputHandle]: coerced } });
|
|
3170
|
-
return;
|
|
3171
3286
|
}
|
|
3172
|
-
if (isArray && elemTarget) {
|
|
3287
|
+
else if (isArray && elemTarget) {
|
|
3173
3288
|
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
3174
3289
|
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
3175
3290
|
const src = unwrap(raw);
|
|
@@ -3181,19 +3296,21 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3181
3296
|
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
3182
3297
|
const col = idx % COLS;
|
|
3183
3298
|
const row = Math.floor(idx / COLS);
|
|
3184
|
-
wb.addNode({
|
|
3299
|
+
newNodeId = wb.addNode({
|
|
3185
3300
|
typeId: elemTarget.nodeTypeId,
|
|
3186
3301
|
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
3187
3302
|
}, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
|
|
3188
3303
|
}
|
|
3189
|
-
|
|
3304
|
+
}
|
|
3305
|
+
if (newNodeId) {
|
|
3306
|
+
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "bake" });
|
|
3190
3307
|
}
|
|
3191
3308
|
}
|
|
3192
3309
|
catch { }
|
|
3193
3310
|
};
|
|
3194
3311
|
return {
|
|
3195
3312
|
onDelete: () => {
|
|
3196
|
-
wb.removeNode(nodeId);
|
|
3313
|
+
wb.removeNode(nodeId, { commit: true });
|
|
3197
3314
|
onClose();
|
|
3198
3315
|
},
|
|
3199
3316
|
onDuplicate: async () => {
|
|
@@ -3218,10 +3335,7 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3218
3335
|
dry: true,
|
|
3219
3336
|
});
|
|
3220
3337
|
// Select the newly duplicated node
|
|
3221
|
-
wb.setSelection({
|
|
3222
|
-
nodes: [newNodeId],
|
|
3223
|
-
edges: [],
|
|
3224
|
-
});
|
|
3338
|
+
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
|
|
3225
3339
|
onClose();
|
|
3226
3340
|
},
|
|
3227
3341
|
onDuplicateWithEdges: async () => {
|
|
@@ -3254,10 +3368,9 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3254
3368
|
}, { dry: true });
|
|
3255
3369
|
}
|
|
3256
3370
|
// Select the newly duplicated node and edges
|
|
3257
|
-
|
|
3258
|
-
nodes: [newNodeId],
|
|
3259
|
-
|
|
3260
|
-
});
|
|
3371
|
+
if (newNodeId) {
|
|
3372
|
+
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate-with-edges" });
|
|
3373
|
+
}
|
|
3261
3374
|
onClose();
|
|
3262
3375
|
},
|
|
3263
3376
|
onRunPull: async () => {
|
|
@@ -3331,7 +3444,7 @@ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onC
|
|
|
3331
3444
|
onClose();
|
|
3332
3445
|
},
|
|
3333
3446
|
onDelete: () => {
|
|
3334
|
-
wb.deleteSelection();
|
|
3447
|
+
wb.deleteSelection({ commit: true, reason: "delete-selection" });
|
|
3335
3448
|
onClose();
|
|
3336
3449
|
},
|
|
3337
3450
|
onClose,
|
|
@@ -3340,10 +3453,24 @@ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onC
|
|
|
3340
3453
|
/**
|
|
3341
3454
|
* Creates base default context menu handlers.
|
|
3342
3455
|
*/
|
|
3343
|
-
function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste) {
|
|
3456
|
+
function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, getCopiedData, clearCopiedData) {
|
|
3457
|
+
// Wrap paste handler to clear storage after paste
|
|
3458
|
+
const wrappedOnPaste = onPaste && getCopiedData && clearCopiedData
|
|
3459
|
+
? (position) => {
|
|
3460
|
+
onPaste(position);
|
|
3461
|
+
clearCopiedData();
|
|
3462
|
+
}
|
|
3463
|
+
: onPaste;
|
|
3464
|
+
// Function to check if paste data exists (called dynamically when menu opens)
|
|
3465
|
+
const hasPasteData = getCopiedData ? () => !!getCopiedData() : undefined;
|
|
3344
3466
|
return {
|
|
3345
3467
|
onAddNode,
|
|
3346
|
-
onPaste,
|
|
3468
|
+
onPaste: wrappedOnPaste,
|
|
3469
|
+
hasPasteData,
|
|
3470
|
+
onUndo: runner ? () => runner.undo().then(() => onClose()) : undefined,
|
|
3471
|
+
onRedo: runner ? () => runner.redo().then(() => onClose()) : undefined,
|
|
3472
|
+
canUndo: runner ? () => runner.canUndo().catch(() => false) : undefined,
|
|
3473
|
+
canRedo: runner ? () => runner.canRedo().catch(() => false) : undefined,
|
|
3347
3474
|
onClose,
|
|
3348
3475
|
};
|
|
3349
3476
|
}
|
|
@@ -3867,13 +3994,48 @@ function DefaultNodeContent({ data, isConnectable, }) {
|
|
|
3867
3994
|
} })] }));
|
|
3868
3995
|
}
|
|
3869
3996
|
|
|
3870
|
-
function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds,
|
|
3997
|
+
function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
3998
|
+
undo: "⌘/Ctrl + Z",
|
|
3999
|
+
redo: "⌘/Ctrl + Shift + Z",
|
|
4000
|
+
paste: "⌘/Ctrl + V",
|
|
4001
|
+
}, }) {
|
|
3871
4002
|
const rf = react.useReactFlow();
|
|
3872
4003
|
const [query, setQuery] = React.useState("");
|
|
4004
|
+
const [canUndo, setCanUndo] = React.useState(false);
|
|
4005
|
+
const [canRedo, setCanRedo] = React.useState(false);
|
|
4006
|
+
const [hasPasteData, setHasPasteData] = React.useState(false);
|
|
3873
4007
|
const q = query.trim().toLowerCase();
|
|
3874
4008
|
const filteredIds = q
|
|
3875
4009
|
? nodeIds.filter((id) => id.toLowerCase().includes(q))
|
|
3876
4010
|
: nodeIds;
|
|
4011
|
+
// Check undo/redo availability and paste data when menu opens
|
|
4012
|
+
React.useEffect(() => {
|
|
4013
|
+
if (!open)
|
|
4014
|
+
return;
|
|
4015
|
+
let cancelled = false;
|
|
4016
|
+
const checkAvailability = async () => {
|
|
4017
|
+
if (handlers.canUndo) {
|
|
4018
|
+
const result = await handlers.canUndo();
|
|
4019
|
+
if (!cancelled)
|
|
4020
|
+
setCanUndo(result);
|
|
4021
|
+
}
|
|
4022
|
+
if (handlers.canRedo) {
|
|
4023
|
+
const result = await handlers.canRedo();
|
|
4024
|
+
if (!cancelled)
|
|
4025
|
+
setCanRedo(result);
|
|
4026
|
+
}
|
|
4027
|
+
// Check paste data dynamically
|
|
4028
|
+
if (handlers.hasPasteData) {
|
|
4029
|
+
const result = handlers.hasPasteData();
|
|
4030
|
+
if (!cancelled)
|
|
4031
|
+
setHasPasteData(result);
|
|
4032
|
+
}
|
|
4033
|
+
};
|
|
4034
|
+
checkAvailability();
|
|
4035
|
+
return () => {
|
|
4036
|
+
cancelled = true;
|
|
4037
|
+
};
|
|
4038
|
+
}, [open, handlers.canUndo, handlers.canRedo, handlers.hasPasteData]);
|
|
3877
4039
|
const root = { __children: {} };
|
|
3878
4040
|
for (const id of filteredIds) {
|
|
3879
4041
|
const parts = id.split(".");
|
|
@@ -3938,6 +4100,12 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
|
|
|
3938
4100
|
handlers.onPaste(p);
|
|
3939
4101
|
handlers.onClose();
|
|
3940
4102
|
};
|
|
4103
|
+
// Helper to format shortcut for current platform
|
|
4104
|
+
const formatShortcut = (shortcut) => {
|
|
4105
|
+
const isMac = typeof navigator !== "undefined" &&
|
|
4106
|
+
navigator.userAgent.toLowerCase().includes("mac");
|
|
4107
|
+
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4108
|
+
};
|
|
3941
4109
|
const renderTree = (tree, path = []) => {
|
|
3942
4110
|
const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
|
|
3943
4111
|
return (jsxRuntime.jsx("div", { children: entries.map(([key, child]) => {
|
|
@@ -3955,10 +4123,17 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
|
|
|
3955
4123
|
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) => {
|
|
3956
4124
|
e.preventDefault();
|
|
3957
4125
|
e.stopPropagation();
|
|
3958
|
-
}, children: [handlers.onPaste && (jsxRuntime.
|
|
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 &&
|
|
4127
|
+
handlers.onPaste &&
|
|
4128
|
+
!handlers.onUndo &&
|
|
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" })) })] }));
|
|
3959
4130
|
}
|
|
3960
4131
|
|
|
3961
|
-
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
|
+
}, }) {
|
|
3962
4137
|
const ref = React.useRef(null);
|
|
3963
4138
|
// outside click + ESC
|
|
3964
4139
|
React.useEffect(() => {
|
|
@@ -3985,6 +4160,12 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
3985
4160
|
if (open)
|
|
3986
4161
|
ref.current?.focus();
|
|
3987
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
|
+
};
|
|
3988
4169
|
if (!open || !clientPos || !nodeId)
|
|
3989
4170
|
return null;
|
|
3990
4171
|
// clamp
|
|
@@ -3996,10 +4177,13 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
3996
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) => {
|
|
3997
4178
|
e.preventDefault();
|
|
3998
4179
|
e.stopPropagation();
|
|
3999
|
-
}, 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)))] }))] }));
|
|
4000
4181
|
}
|
|
4001
4182
|
|
|
4002
|
-
function SelectionContextMenu({ open, clientPos, handlers,
|
|
4183
|
+
function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
4184
|
+
copy: "⌘/Ctrl + C",
|
|
4185
|
+
delete: "Delete",
|
|
4186
|
+
}, }) {
|
|
4003
4187
|
const ref = React.useRef(null);
|
|
4004
4188
|
// Close on outside click and on ESC
|
|
4005
4189
|
React.useEffect(() => {
|
|
@@ -4026,6 +4210,12 @@ function SelectionContextMenu({ open, clientPos, handlers, }) {
|
|
|
4026
4210
|
if (open)
|
|
4027
4211
|
ref.current?.focus();
|
|
4028
4212
|
}, [open]);
|
|
4213
|
+
// Helper to format shortcut for current platform
|
|
4214
|
+
const formatShortcut = (shortcut) => {
|
|
4215
|
+
const isMac = typeof navigator !== "undefined" &&
|
|
4216
|
+
navigator.userAgent.toLowerCase().includes("mac");
|
|
4217
|
+
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4218
|
+
};
|
|
4029
4219
|
if (!open || !clientPos)
|
|
4030
4220
|
return null;
|
|
4031
4221
|
// Clamp menu position to viewport
|
|
@@ -4037,7 +4227,7 @@ function SelectionContextMenu({ open, clientPos, handlers, }) {
|
|
|
4037
4227
|
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) => {
|
|
4038
4228
|
e.preventDefault();
|
|
4039
4229
|
e.stopPropagation();
|
|
4040
|
-
}, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.
|
|
4230
|
+
}, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), 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.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) }))] })] }));
|
|
4041
4231
|
}
|
|
4042
4232
|
|
|
4043
4233
|
const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
|
|
@@ -4375,7 +4565,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4375
4565
|
setNodeMenuOpen(false);
|
|
4376
4566
|
setSelectionMenuOpen(false);
|
|
4377
4567
|
};
|
|
4378
|
-
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]);
|
|
4379
4569
|
const onCloseMenu = React.useCallback(() => {
|
|
4380
4570
|
setMenuOpen(false);
|
|
4381
4571
|
}, []);
|
|
@@ -4407,14 +4597,14 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4407
4597
|
const data = storage.get();
|
|
4408
4598
|
if (!data)
|
|
4409
4599
|
return;
|
|
4410
|
-
wb.pasteCopiedData(data, position);
|
|
4600
|
+
wb.pasteCopiedData(data, position, { commit: true, reason: "paste" });
|
|
4411
4601
|
onCloseMenu();
|
|
4412
|
-
});
|
|
4602
|
+
}, runner, () => storage.get(), () => storage.set(null));
|
|
4413
4603
|
if (overrides?.getDefaultContextMenuHandlers) {
|
|
4414
4604
|
return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
|
|
4415
4605
|
}
|
|
4416
4606
|
return baseHandlers;
|
|
4417
|
-
}, [addNodeAt, onCloseMenu, overrides, wb]);
|
|
4607
|
+
}, [addNodeAt, onCloseMenu, overrides, wb, runner]);
|
|
4418
4608
|
const selectionContextMenuHandlers = React.useMemo(() => {
|
|
4419
4609
|
// Get storage from override or use workbench's internal storage
|
|
4420
4610
|
const storage = overrides?.getCopiedDataStorage
|
|
@@ -4428,9 +4618,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4428
4618
|
}, runner);
|
|
4429
4619
|
if (overrides?.getSelectionContextMenuHandlers) {
|
|
4430
4620
|
const selection = wb.getSelection();
|
|
4431
|
-
return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, {
|
|
4432
|
-
getDefaultNodeSize: overrides.getDefaultNodeSize,
|
|
4433
|
-
});
|
|
4621
|
+
return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, { getDefaultNodeSize: overrides.getDefaultNodeSize });
|
|
4434
4622
|
}
|
|
4435
4623
|
return baseHandlers;
|
|
4436
4624
|
}, [wb, runner, overrides, onCloseSelectionMenu]);
|
|
@@ -4469,6 +4657,116 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4469
4657
|
return [];
|
|
4470
4658
|
return getBakeableOutputs(nodeAtMenu, wb, registry, outputTypesMap);
|
|
4471
4659
|
}, [nodeAtMenu, wb, registry, outputTypesMap]);
|
|
4660
|
+
// Keyboard shortcuts configuration
|
|
4661
|
+
const enableKeyboardShortcuts = overrides?.enableKeyboardShortcuts !== false; // Default to true
|
|
4662
|
+
const keyboardShortcuts = overrides?.keyboardShortcuts || {
|
|
4663
|
+
undo: "⌘/Ctrl + Z",
|
|
4664
|
+
redo: "⌘/Ctrl + Shift + Z",
|
|
4665
|
+
copy: "⌘/Ctrl + C",
|
|
4666
|
+
paste: "⌘/Ctrl + V",
|
|
4667
|
+
duplicate: "⌘/Ctrl + D",
|
|
4668
|
+
delete: "Delete",
|
|
4669
|
+
};
|
|
4670
|
+
// Keyboard shortcut handler
|
|
4671
|
+
React.useEffect(() => {
|
|
4672
|
+
if (!enableKeyboardShortcuts)
|
|
4673
|
+
return;
|
|
4674
|
+
const handleKeyDown = async (e) => {
|
|
4675
|
+
// Ignore if typing in input/textarea
|
|
4676
|
+
const target = e.target;
|
|
4677
|
+
if (target.tagName === "INPUT" ||
|
|
4678
|
+
target.tagName === "TEXTAREA" ||
|
|
4679
|
+
target.isContentEditable) {
|
|
4680
|
+
return;
|
|
4681
|
+
}
|
|
4682
|
+
// Detect Mac platform using userAgent (navigator.platform is deprecated)
|
|
4683
|
+
const isMac = typeof navigator !== "undefined" &&
|
|
4684
|
+
navigator.userAgent.toLowerCase().includes("mac");
|
|
4685
|
+
const modKey = isMac ? e.metaKey : e.ctrlKey;
|
|
4686
|
+
const key = e.key.toLowerCase();
|
|
4687
|
+
// Undo: Cmd/Ctrl + Z
|
|
4688
|
+
if (modKey && key === "z" && !e.shiftKey && !e.altKey) {
|
|
4689
|
+
e.preventDefault();
|
|
4690
|
+
if (runner &&
|
|
4691
|
+
"onUndo" in defaultContextMenuHandlers &&
|
|
4692
|
+
defaultContextMenuHandlers.onUndo) {
|
|
4693
|
+
const canUndo = await runner.canUndo().catch(() => false);
|
|
4694
|
+
if (canUndo) {
|
|
4695
|
+
defaultContextMenuHandlers.onUndo();
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
return;
|
|
4699
|
+
}
|
|
4700
|
+
// Redo: Cmd/Ctrl + Shift + Z
|
|
4701
|
+
if (modKey && e.shiftKey && key === "z" && !e.altKey) {
|
|
4702
|
+
e.preventDefault();
|
|
4703
|
+
if (runner &&
|
|
4704
|
+
"onRedo" in defaultContextMenuHandlers &&
|
|
4705
|
+
defaultContextMenuHandlers.onRedo) {
|
|
4706
|
+
const canRedo = await runner.canRedo().catch(() => false);
|
|
4707
|
+
if (canRedo) {
|
|
4708
|
+
defaultContextMenuHandlers.onRedo();
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
return;
|
|
4712
|
+
}
|
|
4713
|
+
// Copy: Cmd/Ctrl + C
|
|
4714
|
+
if (modKey && key === "c" && !e.shiftKey && !e.altKey) {
|
|
4715
|
+
const selection = wb.getSelection();
|
|
4716
|
+
if (selection.nodes.length > 0 || selection.edges.length > 0) {
|
|
4717
|
+
e.preventDefault();
|
|
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) {
|
|
4723
|
+
selectionContextMenuHandlers.onCopy();
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
return;
|
|
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
|
+
}
|
|
4738
|
+
// Paste: Cmd/Ctrl + V
|
|
4739
|
+
if (modKey && key === "v" && !e.shiftKey && !e.altKey) {
|
|
4740
|
+
e.preventDefault();
|
|
4741
|
+
if ("hasPasteData" in defaultContextMenuHandlers &&
|
|
4742
|
+
defaultContextMenuHandlers.hasPasteData &&
|
|
4743
|
+
defaultContextMenuHandlers.hasPasteData() &&
|
|
4744
|
+
"onPaste" in defaultContextMenuHandlers &&
|
|
4745
|
+
defaultContextMenuHandlers.onPaste) {
|
|
4746
|
+
const center = rfInstanceRef.current?.screenToFlowPosition({
|
|
4747
|
+
x: window.innerWidth / 2,
|
|
4748
|
+
y: window.innerHeight / 2,
|
|
4749
|
+
}) || { x: 0, y: 0 };
|
|
4750
|
+
defaultContextMenuHandlers.onPaste(center);
|
|
4751
|
+
}
|
|
4752
|
+
return;
|
|
4753
|
+
}
|
|
4754
|
+
// Note: Delete/Backspace is handled by ReactFlow's deleteKeyCode prop
|
|
4755
|
+
// which triggers onNodesDelete/onEdgesDelete, so we don't need to handle it here
|
|
4756
|
+
};
|
|
4757
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
4758
|
+
return () => {
|
|
4759
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
4760
|
+
};
|
|
4761
|
+
}, [
|
|
4762
|
+
enableKeyboardShortcuts,
|
|
4763
|
+
wb,
|
|
4764
|
+
runner,
|
|
4765
|
+
defaultContextMenuHandlers,
|
|
4766
|
+
selectionContextMenuHandlers,
|
|
4767
|
+
nodeContextMenuHandlers,
|
|
4768
|
+
rfInstanceRef,
|
|
4769
|
+
]);
|
|
4472
4770
|
// Get custom renderers from UI extension registry (reactive to uiVersion changes)
|
|
4473
4771
|
const { BackgroundRenderer, MinimapRenderer, ControlsRenderer, DefaultContextMenuRenderer, NodeContextMenuRenderer, connectionLineRenderer, } = React.useMemo(() => {
|
|
4474
4772
|
return {
|
|
@@ -4483,7 +4781,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4483
4781
|
const onMoveEnd = React.useCallback(() => {
|
|
4484
4782
|
if (rfInstanceRef.current) {
|
|
4485
4783
|
const viewport = rfInstanceRef.current.getViewport();
|
|
4486
|
-
wb.setViewport({ x: viewport.x, y: viewport.y, zoom: viewport.zoom }
|
|
4784
|
+
wb.setViewport({ x: viewport.x, y: viewport.y, zoom: viewport.zoom });
|
|
4487
4785
|
}
|
|
4488
4786
|
}, [wb]);
|
|
4489
4787
|
const viewportRef = React.useRef(null);
|
|
@@ -4515,9 +4813,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4515
4813
|
zoom: savedViewport.zoom,
|
|
4516
4814
|
});
|
|
4517
4815
|
}
|
|
4518
|
-
}, 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", fitView: true, children: [BackgroundRenderer ? (jsxRuntime.jsx(BackgroundRenderer, {})) : (jsxRuntime.jsx(react.Background, { id: "workbench-canvas-background", variant: react.BackgroundVariant.Dots, gap: 12, size: 1 })), MinimapRenderer ? jsxRuntime.jsx(MinimapRenderer, {}) : jsxRuntime.jsx(react.MiniMap, {}), ControlsRenderer ? jsxRuntime.jsx(ControlsRenderer, {}) : jsxRuntime.jsx(react.Controls, {}), DefaultContextMenuRenderer ? (jsxRuntime.jsx(DefaultContextMenuRenderer, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds
|
|
4816
|
+
}, 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", fitView: true, children: [BackgroundRenderer ? (jsxRuntime.jsx(BackgroundRenderer, {})) : (jsxRuntime.jsx(react.Background, { id: "workbench-canvas-background", variant: react.BackgroundVariant.Dots, gap: 12, size: 1 })), MinimapRenderer ? jsxRuntime.jsx(MinimapRenderer, {}) : jsxRuntime.jsx(react.MiniMap, {}), ControlsRenderer ? jsxRuntime.jsx(ControlsRenderer, {}) : jsxRuntime.jsx(react.Controls, {}), DefaultContextMenuRenderer ? (jsxRuntime.jsx(DefaultContextMenuRenderer, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds, ...(enableKeyboardShortcuts !== false
|
|
4817
|
+
? { enableKeyboardShortcuts, keyboardShortcuts }
|
|
4818
|
+
: {}) })) : (jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts })), !!nodeAtMenu &&
|
|
4519
4819
|
nodeContextMenuHandlers &&
|
|
4520
|
-
(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 }))] }) }) }));
|
|
4521
4823
|
});
|
|
4522
4824
|
|
|
4523
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, }) {
|