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