@bian-womp/spark-workbench 0.2.69 → 0.2.71
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 +377 -177
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts +35 -14
- 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/index.d.ts +1 -1
- package/lib/cjs/src/index.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +2 -1
- 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/context-menu/ContextMenuButton.d.ts +9 -0
- package/lib/cjs/src/misc/context-menu/ContextMenuButton.d.ts.map +1 -0
- package/lib/cjs/src/misc/{context → context-menu}/ContextMenuHandlers.d.ts +8 -2
- package/lib/cjs/src/misc/context-menu/ContextMenuHandlers.d.ts.map +1 -0
- package/lib/cjs/src/misc/{context → context-menu}/ContextMenuHelpers.d.ts +2 -1
- package/lib/cjs/src/misc/context-menu/ContextMenuHelpers.d.ts.map +1 -0
- package/lib/cjs/src/misc/{DefaultContextMenu.d.ts → context-menu/DefaultContextMenu.d.ts} +1 -1
- package/lib/cjs/src/misc/context-menu/DefaultContextMenu.d.ts.map +1 -0
- package/lib/cjs/src/misc/context-menu/NodeContextMenu.d.ts +3 -0
- package/lib/cjs/src/misc/context-menu/NodeContextMenu.d.ts.map +1 -0
- package/lib/cjs/src/misc/{SelectionContextMenu.d.ts → context-menu/SelectionContextMenu.d.ts} +1 -1
- package/lib/cjs/src/misc/context-menu/SelectionContextMenu.d.ts.map +1 -0
- package/lib/cjs/src/misc/hooks.d.ts.map +1 -1
- package/lib/cjs/src/misc/load.d.ts.map +1 -1
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +2 -4
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/IGraphRunner.d.ts +3 -4
- package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +2 -4
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/lib/esm/index.js +377 -177
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/InMemoryWorkbench.d.ts +35 -14
- 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/index.d.ts +1 -1
- package/lib/esm/src/index.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts +2 -1
- 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/context-menu/ContextMenuButton.d.ts +9 -0
- package/lib/esm/src/misc/context-menu/ContextMenuButton.d.ts.map +1 -0
- package/lib/esm/src/misc/{context → context-menu}/ContextMenuHandlers.d.ts +8 -2
- package/lib/esm/src/misc/context-menu/ContextMenuHandlers.d.ts.map +1 -0
- package/lib/esm/src/misc/{context → context-menu}/ContextMenuHelpers.d.ts +2 -1
- package/lib/esm/src/misc/context-menu/ContextMenuHelpers.d.ts.map +1 -0
- package/lib/esm/src/misc/{DefaultContextMenu.d.ts → context-menu/DefaultContextMenu.d.ts} +1 -1
- package/lib/esm/src/misc/context-menu/DefaultContextMenu.d.ts.map +1 -0
- package/lib/esm/src/misc/context-menu/NodeContextMenu.d.ts +3 -0
- package/lib/esm/src/misc/context-menu/NodeContextMenu.d.ts.map +1 -0
- package/lib/esm/src/misc/{SelectionContextMenu.d.ts → context-menu/SelectionContextMenu.d.ts} +1 -1
- package/lib/esm/src/misc/context-menu/SelectionContextMenu.d.ts.map +1 -0
- package/lib/esm/src/misc/hooks.d.ts.map +1 -1
- package/lib/esm/src/misc/load.d.ts.map +1 -1
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +2 -4
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/IGraphRunner.d.ts +3 -4
- package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +2 -4
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/package.json +4 -4
- package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +0 -1
- package/lib/cjs/src/misc/NodeContextMenu.d.ts +0 -3
- package/lib/cjs/src/misc/NodeContextMenu.d.ts.map +0 -1
- package/lib/cjs/src/misc/SelectionContextMenu.d.ts.map +0 -1
- package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +0 -1
- package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +0 -1
- package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +0 -1
- package/lib/esm/src/misc/NodeContextMenu.d.ts +0 -3
- package/lib/esm/src/misc/NodeContextMenu.d.ts.map +0 -1
- package/lib/esm/src/misc/SelectionContextMenu.d.ts.map +0 -1
- package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +0 -1
- package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +0 -1
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');
|
|
@@ -124,13 +125,15 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
124
125
|
constructor() {
|
|
125
126
|
super(...arguments);
|
|
126
127
|
this.def = { nodes: [], edges: [] };
|
|
127
|
-
this.positions = {};
|
|
128
128
|
this.listeners = new Map();
|
|
129
|
+
this.positions = {};
|
|
129
130
|
this.selection = {
|
|
130
131
|
nodes: [],
|
|
131
132
|
edges: [],
|
|
132
133
|
};
|
|
133
134
|
this.viewport = null;
|
|
135
|
+
this.runtimeState = null;
|
|
136
|
+
this.historyState = undefined;
|
|
134
137
|
this.copiedData = null;
|
|
135
138
|
}
|
|
136
139
|
setRegistry(registry) {
|
|
@@ -210,18 +213,19 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
210
213
|
inputs: options?.inputs,
|
|
211
214
|
copyOutputsFrom: options?.copyOutputsFrom,
|
|
212
215
|
},
|
|
213
|
-
|
|
216
|
+
...lod.pick(options, ["dry", "commit", "reason"]),
|
|
214
217
|
});
|
|
215
218
|
this.refreshValidation();
|
|
216
219
|
return id;
|
|
217
220
|
}
|
|
218
|
-
removeNode(nodeId) {
|
|
221
|
+
removeNode(nodeId, options) {
|
|
219
222
|
this.def.nodes = this.def.nodes.filter((n) => n.nodeId !== nodeId);
|
|
220
223
|
this.def.edges = this.def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
|
|
221
224
|
delete this.positions[nodeId];
|
|
222
225
|
this.emit("graphChanged", {
|
|
223
226
|
def: this.def,
|
|
224
227
|
change: { type: "removeNode", nodeId },
|
|
228
|
+
...options,
|
|
225
229
|
});
|
|
226
230
|
this.refreshValidation();
|
|
227
231
|
}
|
|
@@ -236,16 +240,17 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
236
240
|
this.emit("graphChanged", {
|
|
237
241
|
def: this.def,
|
|
238
242
|
change: { type: "connect", edgeId: id },
|
|
239
|
-
|
|
243
|
+
...options,
|
|
240
244
|
});
|
|
241
245
|
this.refreshValidation();
|
|
242
246
|
return id;
|
|
243
247
|
}
|
|
244
|
-
disconnect(edgeId) {
|
|
248
|
+
disconnect(edgeId, options) {
|
|
245
249
|
this.def.edges = this.def.edges.filter((e) => e.id !== edgeId);
|
|
246
250
|
this.emit("graphChanged", {
|
|
247
251
|
def: this.def,
|
|
248
252
|
change: { type: "disconnect", edgeId },
|
|
253
|
+
...options,
|
|
249
254
|
});
|
|
250
255
|
this.emit("validationChanged", this.validate());
|
|
251
256
|
}
|
|
@@ -274,32 +279,26 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
274
279
|
});
|
|
275
280
|
}
|
|
276
281
|
// Position and selection APIs for React Flow bridge
|
|
277
|
-
|
|
278
|
-
this.positions[nodeId] = pos;
|
|
279
|
-
this.emit("graphUiChanged", {
|
|
280
|
-
def: this.def,
|
|
281
|
-
change: { type: "moveNode", nodeId, pos },
|
|
282
|
-
commit: !!opts?.commit === true,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
setPositions(map, opts) {
|
|
282
|
+
setPositions(map, options) {
|
|
286
283
|
this.positions = { ...map };
|
|
287
284
|
this.emit("graphUiChanged", {
|
|
288
285
|
def: this.def,
|
|
289
286
|
change: { type: "moveNodes" },
|
|
290
|
-
|
|
287
|
+
...options,
|
|
291
288
|
});
|
|
292
289
|
}
|
|
293
290
|
getPositions() {
|
|
294
291
|
return { ...this.positions };
|
|
295
292
|
}
|
|
296
|
-
setSelection(sel,
|
|
293
|
+
setSelection(sel, options) {
|
|
294
|
+
if (lod.isEqual(this.selection, sel))
|
|
295
|
+
return;
|
|
297
296
|
this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
|
|
298
297
|
this.emit("selectionChanged", this.selection);
|
|
299
298
|
this.emit("graphUiChanged", {
|
|
300
299
|
def: this.def,
|
|
301
300
|
change: { type: "selection" },
|
|
302
|
-
|
|
301
|
+
...options,
|
|
303
302
|
});
|
|
304
303
|
}
|
|
305
304
|
getSelection() {
|
|
@@ -311,7 +310,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
311
310
|
/**
|
|
312
311
|
* Delete all selected nodes and edges.
|
|
313
312
|
*/
|
|
314
|
-
deleteSelection() {
|
|
313
|
+
deleteSelection(options) {
|
|
315
314
|
const selection = this.getSelection();
|
|
316
315
|
// Delete all selected nodes (this will also remove connected edges)
|
|
317
316
|
for (const nodeId of selection.nodes) {
|
|
@@ -322,14 +321,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
322
321
|
this.disconnect(edgeId);
|
|
323
322
|
}
|
|
324
323
|
// Clear selection
|
|
325
|
-
this.setSelection({ nodes: [], edges: [] });
|
|
324
|
+
this.setSelection({ nodes: [], edges: [] }, options);
|
|
326
325
|
}
|
|
327
|
-
setViewport(viewport,
|
|
326
|
+
setViewport(viewport, options) {
|
|
328
327
|
this.viewport = { ...viewport };
|
|
329
328
|
this.emit("graphUiChanged", {
|
|
330
329
|
def: this.def,
|
|
331
330
|
change: { type: "viewport" },
|
|
332
|
-
|
|
331
|
+
...options,
|
|
333
332
|
});
|
|
334
333
|
}
|
|
335
334
|
getViewport() {
|
|
@@ -371,6 +370,27 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
371
370
|
this.viewport = { ...ui.viewport };
|
|
372
371
|
}
|
|
373
372
|
}
|
|
373
|
+
getRuntimeState() {
|
|
374
|
+
return this.runtimeState ? { ...this.runtimeState } : null;
|
|
375
|
+
}
|
|
376
|
+
setRuntimeState(runtime) {
|
|
377
|
+
this.runtimeState = runtime ? { ...runtime } : null;
|
|
378
|
+
}
|
|
379
|
+
getHistory() {
|
|
380
|
+
return this.historyState;
|
|
381
|
+
}
|
|
382
|
+
setHistory(history) {
|
|
383
|
+
this.historyState = history;
|
|
384
|
+
}
|
|
385
|
+
getNodeRuntimeMetadata(nodeId) {
|
|
386
|
+
return this.runtimeState?.nodes[nodeId];
|
|
387
|
+
}
|
|
388
|
+
updateNodeRuntimeMetadata(nodeId, updater) {
|
|
389
|
+
const current = this.runtimeState ?? { nodes: {} };
|
|
390
|
+
const nodeMeta = current.nodes[nodeId] ?? {};
|
|
391
|
+
const updated = updater({ ...nodeMeta });
|
|
392
|
+
this.runtimeState = { nodes: { ...current.nodes, [nodeId]: updated } };
|
|
393
|
+
}
|
|
374
394
|
on(event, handler) {
|
|
375
395
|
if (!this.listeners.has(event))
|
|
376
396
|
this.listeners.set(event, new Set());
|
|
@@ -474,7 +494,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
474
494
|
* Returns the mapping from original node IDs to new node IDs.
|
|
475
495
|
* Uses copyOutputsFrom to copy outputs from original nodes (like duplicate does).
|
|
476
496
|
*/
|
|
477
|
-
pasteCopiedData(data, center) {
|
|
497
|
+
pasteCopiedData(data, center, options) {
|
|
478
498
|
const nodeIdMap = new Map();
|
|
479
499
|
const edgeIds = [];
|
|
480
500
|
// Add nodes
|
|
@@ -514,10 +534,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
514
534
|
}
|
|
515
535
|
}
|
|
516
536
|
// Select the newly pasted nodes
|
|
517
|
-
this.setSelection({
|
|
518
|
-
nodes: Array.from(nodeIdMap.values()),
|
|
519
|
-
edges: edgeIds,
|
|
520
|
-
});
|
|
537
|
+
this.setSelection({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
|
|
521
538
|
return { nodeIdMap, edgeIds };
|
|
522
539
|
}
|
|
523
540
|
/**
|
|
@@ -679,14 +696,10 @@ class AbstractGraphRunner {
|
|
|
679
696
|
async redo() {
|
|
680
697
|
return false;
|
|
681
698
|
}
|
|
682
|
-
async canUndo() {
|
|
683
|
-
return false;
|
|
684
|
-
}
|
|
685
|
-
async canRedo() {
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
699
|
// Optional commit support
|
|
689
|
-
async commit() {
|
|
700
|
+
async commit(_reason) {
|
|
701
|
+
return undefined;
|
|
702
|
+
}
|
|
690
703
|
}
|
|
691
704
|
|
|
692
705
|
// Counter for generating readable runner IDs
|
|
@@ -1433,10 +1446,11 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1433
1446
|
const client = await this.ensureClient();
|
|
1434
1447
|
await client.setExtData(data);
|
|
1435
1448
|
}
|
|
1436
|
-
async commit() {
|
|
1449
|
+
async commit(reason) {
|
|
1437
1450
|
const client = await this.ensureClient();
|
|
1438
1451
|
try {
|
|
1439
|
-
await client.commit();
|
|
1452
|
+
const history = await client.commit(reason);
|
|
1453
|
+
return history;
|
|
1440
1454
|
}
|
|
1441
1455
|
catch (err) {
|
|
1442
1456
|
console.error("[RemoteGraphRunner] Error committing:", err);
|
|
@@ -1461,24 +1475,6 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1461
1475
|
return false;
|
|
1462
1476
|
}
|
|
1463
1477
|
}
|
|
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
|
-
}
|
|
1482
1478
|
async snapshotFull() {
|
|
1483
1479
|
const client = await this.ensureClient();
|
|
1484
1480
|
try {
|
|
@@ -1916,13 +1912,17 @@ function useWorkbenchBridge(wb) {
|
|
|
1916
1912
|
wb.connect({
|
|
1917
1913
|
source: { nodeId: params.source, handle: params.sourceHandle },
|
|
1918
1914
|
target: { nodeId: params.target, handle: params.targetHandle },
|
|
1919
|
-
});
|
|
1915
|
+
}, { commit: true });
|
|
1920
1916
|
}, [wb]);
|
|
1921
1917
|
const onNodesChange = React.useCallback((changes) => {
|
|
1922
1918
|
// Apply position updates continuously, but mark commit only on drag end
|
|
1919
|
+
const positions = {};
|
|
1920
|
+
let commit = false;
|
|
1923
1921
|
changes.forEach((c) => {
|
|
1924
1922
|
if (c.type === "position" && c.position) {
|
|
1925
|
-
|
|
1923
|
+
positions[c.id] = c.position;
|
|
1924
|
+
if (!c.dragging)
|
|
1925
|
+
commit = true;
|
|
1926
1926
|
}
|
|
1927
1927
|
});
|
|
1928
1928
|
// Derive next node selection from change set
|
|
@@ -1953,13 +1953,13 @@ function useWorkbenchBridge(wb) {
|
|
|
1953
1953
|
}
|
|
1954
1954
|
}
|
|
1955
1955
|
if (selectionChanged) {
|
|
1956
|
-
wb.setSelection({
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
});
|
|
1956
|
+
wb.setSelection({ nodes: Array.from(nextNodeIds), edges: current.edges }, { commit: !(Object.keys(positions).length && commit) });
|
|
1957
|
+
}
|
|
1958
|
+
if (Object.keys(positions).length > 0) {
|
|
1959
|
+
wb.setPositions(positions, { commit });
|
|
1960
1960
|
}
|
|
1961
1961
|
}, [wb]);
|
|
1962
|
-
const onEdgesDelete = React.useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
|
|
1962
|
+
const onEdgesDelete = React.useCallback((edges) => edges.forEach((e, idx) => wb.disconnect(e.id, { commit: idx === edges.length - 1 })), [wb]);
|
|
1963
1963
|
const onEdgesChange = React.useCallback((changes) => {
|
|
1964
1964
|
const current = wb.getSelection();
|
|
1965
1965
|
const nextEdgeIds = new Set(current.edges);
|
|
@@ -1988,15 +1988,11 @@ function useWorkbenchBridge(wb) {
|
|
|
1988
1988
|
}
|
|
1989
1989
|
}
|
|
1990
1990
|
if (selectionChanged) {
|
|
1991
|
-
wb.setSelection({
|
|
1992
|
-
nodes: current.nodes,
|
|
1993
|
-
edges: Array.from(nextEdgeIds),
|
|
1994
|
-
});
|
|
1991
|
+
wb.setSelection({ nodes: current.nodes, edges: Array.from(nextEdgeIds) }, { commit: true });
|
|
1995
1992
|
}
|
|
1996
1993
|
}, [wb]);
|
|
1997
1994
|
const onNodesDelete = React.useCallback((nodes) => {
|
|
1998
|
-
|
|
1999
|
-
wb.removeNode(n.id);
|
|
1995
|
+
nodes.forEach((n, idx) => wb.removeNode(n.id, { commit: idx === nodes.length - 1 }));
|
|
2000
1996
|
}, [wb]);
|
|
2001
1997
|
return {
|
|
2002
1998
|
onConnect,
|
|
@@ -2440,6 +2436,7 @@ async function download(wb, runner) {
|
|
|
2440
2436
|
try {
|
|
2441
2437
|
const def = wb.export();
|
|
2442
2438
|
const uiState = wb.getUIState();
|
|
2439
|
+
const runtimeState = wb.getRuntimeState();
|
|
2443
2440
|
let snapshot;
|
|
2444
2441
|
if (runner.isRunning()) {
|
|
2445
2442
|
const fullSnapshot = await runner.snapshotFull();
|
|
@@ -2449,6 +2446,7 @@ async function download(wb, runner) {
|
|
|
2449
2446
|
extData: {
|
|
2450
2447
|
...(fullSnapshot.extData || {}),
|
|
2451
2448
|
ui: uiState,
|
|
2449
|
+
runtime: runtimeState || undefined,
|
|
2452
2450
|
},
|
|
2453
2451
|
};
|
|
2454
2452
|
}
|
|
@@ -2459,7 +2457,7 @@ async function download(wb, runner) {
|
|
|
2459
2457
|
inputs,
|
|
2460
2458
|
outputs: {},
|
|
2461
2459
|
environment: {},
|
|
2462
|
-
extData: { ui: uiState },
|
|
2460
|
+
extData: { ui: uiState, runtime: runtimeState || undefined },
|
|
2463
2461
|
};
|
|
2464
2462
|
}
|
|
2465
2463
|
downloadJSON(snapshot, `spark-snapshot-${generateTimestamp()}.json`);
|
|
@@ -2485,6 +2483,9 @@ async function upload(parsed, wb, runner) {
|
|
|
2485
2483
|
if (extData.ui && typeof extData.ui === "object") {
|
|
2486
2484
|
wb.setUIState(extData.ui);
|
|
2487
2485
|
}
|
|
2486
|
+
if (extData.runtime && typeof extData.runtime === "object") {
|
|
2487
|
+
wb.setRuntimeState(extData.runtime);
|
|
2488
|
+
}
|
|
2488
2489
|
if (runner.isRunning()) {
|
|
2489
2490
|
await runner.applySnapshotFull({
|
|
2490
2491
|
def,
|
|
@@ -2512,6 +2513,18 @@ function useWorkbenchContext() {
|
|
|
2512
2513
|
return ctx;
|
|
2513
2514
|
}
|
|
2514
2515
|
|
|
2516
|
+
// Helper to compute invalidated status from runtime metadata
|
|
2517
|
+
function computeInvalidatedFromMetadata(metadata) {
|
|
2518
|
+
if (!metadata)
|
|
2519
|
+
return true;
|
|
2520
|
+
const { lastSuccessAt, lastInputAt, lastRunAt } = metadata;
|
|
2521
|
+
if (!lastSuccessAt && !lastRunAt)
|
|
2522
|
+
return true;
|
|
2523
|
+
if (!lastInputAt || Object.keys(lastInputAt).length === 0)
|
|
2524
|
+
return false;
|
|
2525
|
+
const maxInputTime = Math.max(...Object.values(lastInputAt));
|
|
2526
|
+
return maxInputTime > (lastSuccessAt ?? lastRunAt ?? 0);
|
|
2527
|
+
}
|
|
2515
2528
|
function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVersion, children, }) {
|
|
2516
2529
|
const [nodeStatus, setNodeStatus] = React.useState({});
|
|
2517
2530
|
const [edgeStatus, setEdgeStatus] = React.useState({});
|
|
@@ -2602,27 +2615,35 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2602
2615
|
}
|
|
2603
2616
|
return out;
|
|
2604
2617
|
}, [def, outputsMap, registry]);
|
|
2605
|
-
// Initialize nodes
|
|
2618
|
+
// Initialize nodes and derive invalidated status from persisted metadata
|
|
2606
2619
|
React.useEffect(() => {
|
|
2620
|
+
const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
|
|
2607
2621
|
setNodeStatus((prev) => {
|
|
2608
2622
|
const next = { ...prev };
|
|
2623
|
+
const metadata = workbenchRuntimeState;
|
|
2609
2624
|
for (const n of def.nodes) {
|
|
2610
2625
|
const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
|
|
2626
|
+
const nodeMeta = metadata.nodes[n.nodeId];
|
|
2611
2627
|
const updates = {};
|
|
2612
2628
|
if (cur.invalidated === undefined) {
|
|
2613
|
-
updates.invalidated =
|
|
2629
|
+
updates.invalidated = computeInvalidatedFromMetadata(nodeMeta);
|
|
2614
2630
|
}
|
|
2615
|
-
// Ensure activeRunIds is always initialized as an array
|
|
2616
2631
|
if (cur.activeRunIds === undefined) {
|
|
2617
2632
|
updates.activeRunIds = [];
|
|
2618
2633
|
}
|
|
2634
|
+
if (cur.activeRuns === undefined) {
|
|
2635
|
+
updates.activeRuns = 0;
|
|
2636
|
+
}
|
|
2637
|
+
if (nodeMeta?.lastErrorSummary && cur.lastError === undefined) {
|
|
2638
|
+
updates.lastError = nodeMeta.lastErrorSummary;
|
|
2639
|
+
}
|
|
2619
2640
|
if (Object.keys(updates).length > 0) {
|
|
2620
2641
|
next[n.nodeId] = { ...cur, ...updates };
|
|
2621
2642
|
}
|
|
2622
2643
|
}
|
|
2623
2644
|
return next;
|
|
2624
2645
|
});
|
|
2625
|
-
}, [def]);
|
|
2646
|
+
}, [def, wb]);
|
|
2626
2647
|
// Auto layout (simple layered layout)
|
|
2627
2648
|
const runAutoLayout = React.useCallback(() => {
|
|
2628
2649
|
const cur = wb.export();
|
|
@@ -2689,10 +2710,31 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2689
2710
|
}
|
|
2690
2711
|
curX += maxWidth + H_GAP;
|
|
2691
2712
|
}
|
|
2692
|
-
wb.setPositions(pos, { commit: true });
|
|
2713
|
+
wb.setPositions(pos, { commit: true, reason: "auto-layout" });
|
|
2693
2714
|
}, [wb, registry, overrides?.getDefaultNodeSize]);
|
|
2694
2715
|
const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
|
|
2695
2716
|
const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
|
|
2717
|
+
// Helper to save runtime metadata to extData.runtime and workbench state
|
|
2718
|
+
const saveRuntimeMetadata = React.useCallback(async () => {
|
|
2719
|
+
try {
|
|
2720
|
+
const current = wb.getRuntimeState() ?? { nodes: {} };
|
|
2721
|
+
const metadata = { nodes: { ...current.nodes } };
|
|
2722
|
+
// Clean up metadata for nodes that no longer exist
|
|
2723
|
+
const nodeIds = new Set(def.nodes.map((n) => n.nodeId));
|
|
2724
|
+
for (const nodeId of Object.keys(metadata.nodes)) {
|
|
2725
|
+
if (!nodeIds.has(nodeId)) {
|
|
2726
|
+
delete metadata.nodes[nodeId];
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
// Save cleaned metadata to workbench state
|
|
2730
|
+
wb.setRuntimeState(metadata);
|
|
2731
|
+
// Save to extData.runtime via runner (no snapshotFull)
|
|
2732
|
+
await runner.setExtData?.({ runtime: metadata });
|
|
2733
|
+
}
|
|
2734
|
+
catch (err) {
|
|
2735
|
+
console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
|
|
2736
|
+
}
|
|
2737
|
+
}, [wb, def, runner]);
|
|
2696
2738
|
// Subscribe to runner/workbench events
|
|
2697
2739
|
React.useEffect(() => {
|
|
2698
2740
|
const add = (source, type) => (payload) => setEvents((prev) => {
|
|
@@ -2735,9 +2777,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2735
2777
|
wb.refreshValidation();
|
|
2736
2778
|
};
|
|
2737
2779
|
const offRunnerValue = runner.on("value", (e) => {
|
|
2780
|
+
const now = Date.now();
|
|
2738
2781
|
if (e?.io === "input") {
|
|
2739
|
-
const nodeId = e
|
|
2740
|
-
const handle = e
|
|
2782
|
+
const nodeId = e.nodeId;
|
|
2783
|
+
const handle = e.handle;
|
|
2784
|
+
// Track input timestamp in workbench runtime state
|
|
2785
|
+
wb.updateNodeRuntimeMetadata(nodeId, (nodeMeta) => ({
|
|
2786
|
+
...nodeMeta,
|
|
2787
|
+
lastInputAt: {
|
|
2788
|
+
...(nodeMeta.lastInputAt ?? {}),
|
|
2789
|
+
[handle]: now,
|
|
2790
|
+
},
|
|
2791
|
+
}));
|
|
2741
2792
|
setNodeStatus((s) => ({
|
|
2742
2793
|
...s,
|
|
2743
2794
|
[nodeId]: { ...s[nodeId], invalidated: true },
|
|
@@ -2745,6 +2796,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2745
2796
|
// Clear validation errors for this input when a valid value is set
|
|
2746
2797
|
setInputValidationErrors((prev) => prev.filter((err) => !(err.nodeId === nodeId && err.handle === handle)));
|
|
2747
2798
|
}
|
|
2799
|
+
else if (e?.io === "output") {
|
|
2800
|
+
const nodeId = e.nodeId;
|
|
2801
|
+
const handle = e.handle;
|
|
2802
|
+
// Track output timestamp in workbench runtime state
|
|
2803
|
+
wb.updateNodeRuntimeMetadata(nodeId, (nodeMeta) => ({
|
|
2804
|
+
...nodeMeta,
|
|
2805
|
+
lastOutputAt: {
|
|
2806
|
+
...(nodeMeta.lastOutputAt ?? {}),
|
|
2807
|
+
[handle]: now,
|
|
2808
|
+
},
|
|
2809
|
+
}));
|
|
2810
|
+
}
|
|
2748
2811
|
return add("runner", "value")(e);
|
|
2749
2812
|
});
|
|
2750
2813
|
const offRunnerError = runner.on("error", (e) => {
|
|
@@ -2763,6 +2826,24 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2763
2826
|
else if (nodeError.kind === "node-run" && nodeError.nodeId) {
|
|
2764
2827
|
const nodeId = nodeError.nodeId;
|
|
2765
2828
|
const runId = nodeError.runId;
|
|
2829
|
+
const now = Date.now();
|
|
2830
|
+
// Track error timestamp and summary in workbench runtime state
|
|
2831
|
+
const err = nodeError.err;
|
|
2832
|
+
let errorSummary;
|
|
2833
|
+
if (err && typeof err === "object") {
|
|
2834
|
+
const message = err.message || String(err);
|
|
2835
|
+
const code = err.code || err.statusCode;
|
|
2836
|
+
errorSummary = {
|
|
2837
|
+
message: typeof message === "string" ? message : String(message),
|
|
2838
|
+
code: typeof code === "number" ? code : undefined,
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
wb.updateNodeRuntimeMetadata(nodeId, (nodeMeta) => ({
|
|
2842
|
+
...nodeMeta,
|
|
2843
|
+
lastErrorAt: now,
|
|
2844
|
+
lastRunAt: now,
|
|
2845
|
+
...(errorSummary ? { lastErrorSummary: errorSummary } : {}),
|
|
2846
|
+
}));
|
|
2766
2847
|
setNodeStatus((s) => ({
|
|
2767
2848
|
...s,
|
|
2768
2849
|
[nodeId]: {
|
|
@@ -2817,6 +2898,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2817
2898
|
// If resolvedHandles are included in the event, use them directly (more efficient)
|
|
2818
2899
|
if (e?.resolvedHandles && Object.keys(e.resolvedHandles).length > 0) {
|
|
2819
2900
|
applyResolvedHandles(e.resolvedHandles);
|
|
2901
|
+
// Mark nodes whose handles changed as invalid
|
|
2902
|
+
const affectedNodeIds = Object.keys(e.resolvedHandles);
|
|
2903
|
+
if (affectedNodeIds.length > 0) {
|
|
2904
|
+
setNodeStatus((prev) => {
|
|
2905
|
+
const next = { ...prev };
|
|
2906
|
+
for (const id of affectedNodeIds) {
|
|
2907
|
+
const cur = next[id] ?? (next[id] = { activeRuns: 0, activeRunIds: [] });
|
|
2908
|
+
next[id] = { ...cur, invalidated: true };
|
|
2909
|
+
}
|
|
2910
|
+
return next;
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
// For broader invalidations (e.g. registry-changed, graph-updated), mark all nodes invalid
|
|
2915
|
+
if (e?.reason === "registry-changed" || e?.reason === "graph-updated") {
|
|
2916
|
+
setNodeStatus((prev) => {
|
|
2917
|
+
const next = { ...prev };
|
|
2918
|
+
for (const n of def.nodes) {
|
|
2919
|
+
const cur = next[n.nodeId] ??
|
|
2920
|
+
(next[n.nodeId] = { activeRuns: 0, activeRunIds: [] });
|
|
2921
|
+
next[n.nodeId] = { ...cur, invalidated: true };
|
|
2922
|
+
}
|
|
2923
|
+
return next;
|
|
2924
|
+
});
|
|
2820
2925
|
}
|
|
2821
2926
|
return add("runner", "invalidate")(e);
|
|
2822
2927
|
});
|
|
@@ -2826,6 +2931,12 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2826
2931
|
if (s.kind === "node-start") {
|
|
2827
2932
|
const id = s.nodeId;
|
|
2828
2933
|
const runId = s.runId;
|
|
2934
|
+
const now = Date.now();
|
|
2935
|
+
// Track run timestamp in workbench runtime state
|
|
2936
|
+
wb.updateNodeRuntimeMetadata(id, (nodeMeta) => ({
|
|
2937
|
+
...nodeMeta,
|
|
2938
|
+
lastRunAt: now,
|
|
2939
|
+
}));
|
|
2829
2940
|
// Validate runId is a non-empty string
|
|
2830
2941
|
const isValidRunId = runId && typeof runId === "string" && runId.length > 0;
|
|
2831
2942
|
if (!isValidRunId) {
|
|
@@ -2848,7 +2959,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2848
2959
|
};
|
|
2849
2960
|
});
|
|
2850
2961
|
// Start fallback animation window
|
|
2851
|
-
setFallbackStarts((prev) => ({ ...prev, [id]:
|
|
2962
|
+
setFallbackStarts((prev) => ({ ...prev, [id]: now }));
|
|
2852
2963
|
}
|
|
2853
2964
|
else if (s.kind === "node-progress") {
|
|
2854
2965
|
const id = s.nodeId;
|
|
@@ -2863,8 +2974,21 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2863
2974
|
else if (s.kind === "node-done") {
|
|
2864
2975
|
const id = s.nodeId;
|
|
2865
2976
|
const runId = s.runId;
|
|
2977
|
+
const now = Date.now();
|
|
2866
2978
|
// Validate runId is a non-empty string
|
|
2867
2979
|
const isValidRunId = runId && typeof runId === "string" && runId.length > 0;
|
|
2980
|
+
const hadError = !!(isValidRunId && errorRunsRef.current[id]?.[runId]);
|
|
2981
|
+
// Track success timestamp if no error in workbench runtime state
|
|
2982
|
+
if (!hadError) {
|
|
2983
|
+
wb.updateNodeRuntimeMetadata(id, (nodeMeta) => {
|
|
2984
|
+
const updated = { ...nodeMeta, lastSuccessAt: now };
|
|
2985
|
+
// Clear error summary on success
|
|
2986
|
+
if (updated.lastErrorSummary) {
|
|
2987
|
+
delete updated.lastErrorSummary;
|
|
2988
|
+
}
|
|
2989
|
+
return updated;
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2868
2992
|
setNodeStatus((prev) => {
|
|
2869
2993
|
const current = prev[id]?.activeRuns ?? 0;
|
|
2870
2994
|
const currentRunIds = prev[id]?.activeRunIds ?? [];
|
|
@@ -2876,7 +3000,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2876
3000
|
const nextRunIds = isValidRunId
|
|
2877
3001
|
? currentRunIds.filter((rid) => rid !== runId)
|
|
2878
3002
|
: currentRunIds;
|
|
2879
|
-
const hadError = !!(isValidRunId && errorRunsRef.current[id]?.[runId]);
|
|
2880
3003
|
const keepProgress = hadError || nextActive > 0;
|
|
2881
3004
|
// Clear error flag for this runId
|
|
2882
3005
|
if (isValidRunId && errorRunsRef.current[id]?.[runId]) {
|
|
@@ -2945,11 +3068,40 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2945
3068
|
}
|
|
2946
3069
|
});
|
|
2947
3070
|
const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
|
|
3071
|
+
// Build detailed reason from change type
|
|
3072
|
+
let reason = "graph-changed";
|
|
3073
|
+
if (event.change) {
|
|
3074
|
+
const changeType = event.change.type;
|
|
3075
|
+
if (changeType === "addNode") {
|
|
3076
|
+
reason = "add-node";
|
|
3077
|
+
}
|
|
3078
|
+
else if (changeType === "removeNode") {
|
|
3079
|
+
reason = "remove-node";
|
|
3080
|
+
}
|
|
3081
|
+
else if (changeType === "connect") {
|
|
3082
|
+
reason = "connect-edge";
|
|
3083
|
+
}
|
|
3084
|
+
else if (changeType === "disconnect") {
|
|
3085
|
+
reason = "disconnect-edge";
|
|
3086
|
+
}
|
|
3087
|
+
else if (changeType === "updateParams") {
|
|
3088
|
+
reason = "update-node-params";
|
|
3089
|
+
}
|
|
3090
|
+
else if (changeType === "updateEdgeType") {
|
|
3091
|
+
reason = "update-edge-type";
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
2948
3094
|
if (!runner.isRunning()) {
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
3095
|
+
if (event.commit) {
|
|
3096
|
+
await saveRuntimeMetadata();
|
|
3097
|
+
const history = await runner.commit(reason).catch((err) => {
|
|
3098
|
+
console.error("[WorkbenchContext] Error committing:", err);
|
|
3099
|
+
return undefined;
|
|
3100
|
+
});
|
|
3101
|
+
if (history) {
|
|
3102
|
+
wb.setHistory(history);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
2953
3105
|
return;
|
|
2954
3106
|
}
|
|
2955
3107
|
try {
|
|
@@ -2974,10 +3126,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2974
3126
|
else {
|
|
2975
3127
|
await runner.update(event.def, { dry: event.dry });
|
|
2976
3128
|
}
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3129
|
+
if (event.commit) {
|
|
3130
|
+
await saveRuntimeMetadata();
|
|
3131
|
+
const history = await runner
|
|
3132
|
+
.commit(event.reason ?? reason)
|
|
3133
|
+
.catch((err) => {
|
|
3134
|
+
console.error("[WorkbenchContext] Error committing after update:", err);
|
|
3135
|
+
return undefined;
|
|
3136
|
+
});
|
|
3137
|
+
if (history) {
|
|
3138
|
+
wb.setHistory(history);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
2981
3141
|
}
|
|
2982
3142
|
catch (err) {
|
|
2983
3143
|
console.error("[WorkbenchContext] Error updating graph:", err);
|
|
@@ -2987,17 +3147,49 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2987
3147
|
const offWbSelectionChanged = wb.on("selectionChanged", async (sel) => {
|
|
2988
3148
|
setSelectedNodeId(sel.nodes?.[0]);
|
|
2989
3149
|
setSelectedEdgeId(sel.edges?.[0]);
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3150
|
+
if (sel.commit) {
|
|
3151
|
+
await saveRuntimeMetadata();
|
|
3152
|
+
const history = await runner
|
|
3153
|
+
.commit(sel.reason ?? "selection")
|
|
3154
|
+
.catch((err) => {
|
|
3155
|
+
console.error("[WorkbenchContext] Error committing selection change:", err);
|
|
3156
|
+
return undefined;
|
|
3157
|
+
});
|
|
3158
|
+
if (history) {
|
|
3159
|
+
wb.setHistory(history);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
2994
3162
|
});
|
|
2995
3163
|
const offWbGraphUiChanged = wb.on("graphUiChanged", async (event) => {
|
|
2996
3164
|
// Only commit if commit flag is true (e.g., drag end, not during dragging)
|
|
2997
3165
|
if (event.commit) {
|
|
2998
|
-
|
|
3166
|
+
// Build detailed reason from change type
|
|
3167
|
+
let reason = "ui-changed";
|
|
3168
|
+
if (event.change) {
|
|
3169
|
+
const changeType = event.change.type;
|
|
3170
|
+
if (changeType === "moveNode") {
|
|
3171
|
+
reason = "move-node";
|
|
3172
|
+
}
|
|
3173
|
+
else if (changeType === "moveNodes") {
|
|
3174
|
+
reason = "move-nodes";
|
|
3175
|
+
}
|
|
3176
|
+
else if (changeType === "selection") {
|
|
3177
|
+
reason = "selection";
|
|
3178
|
+
}
|
|
3179
|
+
else if (changeType === "viewport") {
|
|
3180
|
+
reason = "viewport";
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
await saveRuntimeMetadata();
|
|
3184
|
+
const history = await runner
|
|
3185
|
+
.commit(event.reason ?? reason)
|
|
3186
|
+
.catch((err) => {
|
|
2999
3187
|
console.error("[WorkbenchContext] Error committing UI changes:", err);
|
|
3188
|
+
return undefined;
|
|
3000
3189
|
});
|
|
3190
|
+
if (history) {
|
|
3191
|
+
wb.setHistory(history);
|
|
3192
|
+
}
|
|
3001
3193
|
}
|
|
3002
3194
|
});
|
|
3003
3195
|
const offWbError = wb.on("error", add("workbench", "error"));
|
|
@@ -3018,11 +3210,24 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3018
3210
|
console.error("Failed to handle registry changed event");
|
|
3019
3211
|
}
|
|
3020
3212
|
});
|
|
3021
|
-
// Handle transport
|
|
3213
|
+
// Handle transport changes: reset runtime status when connection is lost
|
|
3022
3214
|
const offRunnerTransport = runner.on("transport", (t) => {
|
|
3023
3215
|
if (t.state === "disconnected") {
|
|
3024
3216
|
console.info("[WorkbenchContext] Transport disconnected, resetting node status");
|
|
3025
|
-
|
|
3217
|
+
// Reinitialize node status with invalidated=true for all nodes
|
|
3218
|
+
setNodeStatus(() => {
|
|
3219
|
+
const next = {};
|
|
3220
|
+
const metadata = wb.getRuntimeState() ?? { nodes: {} };
|
|
3221
|
+
for (const n of def.nodes) {
|
|
3222
|
+
const nodeMeta = metadata.nodes[n.nodeId];
|
|
3223
|
+
next[n.nodeId] = {
|
|
3224
|
+
activeRuns: 0,
|
|
3225
|
+
activeRunIds: [],
|
|
3226
|
+
invalidated: computeInvalidatedFromMetadata(nodeMeta),
|
|
3227
|
+
};
|
|
3228
|
+
}
|
|
3229
|
+
return next;
|
|
3230
|
+
});
|
|
3026
3231
|
setEdgeStatus({});
|
|
3027
3232
|
setFallbackStarts({});
|
|
3028
3233
|
errorRunsRef.current = {};
|
|
@@ -3211,6 +3416,7 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3211
3416
|
try {
|
|
3212
3417
|
const typeId = outputTypesMap?.[nodeId]?.[handleId];
|
|
3213
3418
|
const raw = outputsMap?.[nodeId]?.[handleId];
|
|
3419
|
+
let newNodeId;
|
|
3214
3420
|
if (!typeId || raw === undefined)
|
|
3215
3421
|
return;
|
|
3216
3422
|
const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
|
|
@@ -3236,23 +3442,21 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3236
3442
|
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
3237
3443
|
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
3238
3444
|
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3239
|
-
wb.addNode({
|
|
3445
|
+
newNodeId = wb.addNode({
|
|
3240
3446
|
typeId: singleTarget.nodeTypeId,
|
|
3241
3447
|
position: { x: pos.x + 180, y: pos.y },
|
|
3242
3448
|
}, { inputs: { [singleTarget.inputHandle]: coerced } });
|
|
3243
|
-
return;
|
|
3244
3449
|
}
|
|
3245
|
-
if (isArray && arrTarget) {
|
|
3450
|
+
else if (isArray && arrTarget) {
|
|
3246
3451
|
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
3247
3452
|
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
3248
3453
|
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3249
|
-
wb.addNode({
|
|
3454
|
+
newNodeId = wb.addNode({
|
|
3250
3455
|
typeId: arrTarget.nodeTypeId,
|
|
3251
3456
|
position: { x: pos.x + 180, y: pos.y },
|
|
3252
3457
|
}, { inputs: { [arrTarget.inputHandle]: coerced } });
|
|
3253
|
-
return;
|
|
3254
3458
|
}
|
|
3255
|
-
if (isArray && elemTarget) {
|
|
3459
|
+
else if (isArray && elemTarget) {
|
|
3256
3460
|
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
3257
3461
|
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
3258
3462
|
const src = unwrap(raw);
|
|
@@ -3264,19 +3468,21 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3264
3468
|
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
3265
3469
|
const col = idx % COLS;
|
|
3266
3470
|
const row = Math.floor(idx / COLS);
|
|
3267
|
-
wb.addNode({
|
|
3471
|
+
newNodeId = wb.addNode({
|
|
3268
3472
|
typeId: elemTarget.nodeTypeId,
|
|
3269
3473
|
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
3270
3474
|
}, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
|
|
3271
3475
|
}
|
|
3272
|
-
|
|
3476
|
+
}
|
|
3477
|
+
if (newNodeId) {
|
|
3478
|
+
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "bake" });
|
|
3273
3479
|
}
|
|
3274
3480
|
}
|
|
3275
3481
|
catch { }
|
|
3276
3482
|
};
|
|
3277
3483
|
return {
|
|
3278
3484
|
onDelete: () => {
|
|
3279
|
-
wb.removeNode(nodeId);
|
|
3485
|
+
wb.removeNode(nodeId, { commit: true });
|
|
3280
3486
|
onClose();
|
|
3281
3487
|
},
|
|
3282
3488
|
onDuplicate: async () => {
|
|
@@ -3301,10 +3507,7 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3301
3507
|
dry: true,
|
|
3302
3508
|
});
|
|
3303
3509
|
// Select the newly duplicated node
|
|
3304
|
-
wb.setSelection({
|
|
3305
|
-
nodes: [newNodeId],
|
|
3306
|
-
edges: [],
|
|
3307
|
-
});
|
|
3510
|
+
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
|
|
3308
3511
|
onClose();
|
|
3309
3512
|
},
|
|
3310
3513
|
onDuplicateWithEdges: async () => {
|
|
@@ -3337,10 +3540,9 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
|
|
|
3337
3540
|
}, { dry: true });
|
|
3338
3541
|
}
|
|
3339
3542
|
// Select the newly duplicated node and edges
|
|
3340
|
-
|
|
3341
|
-
nodes: [newNodeId],
|
|
3342
|
-
|
|
3343
|
-
});
|
|
3543
|
+
if (newNodeId) {
|
|
3544
|
+
wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate-with-edges" });
|
|
3545
|
+
}
|
|
3344
3546
|
onClose();
|
|
3345
3547
|
},
|
|
3346
3548
|
onRunPull: async () => {
|
|
@@ -3414,7 +3616,7 @@ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onC
|
|
|
3414
3616
|
onClose();
|
|
3415
3617
|
},
|
|
3416
3618
|
onDelete: () => {
|
|
3417
|
-
wb.deleteSelection();
|
|
3619
|
+
wb.deleteSelection({ commit: true, reason: "delete-selection" });
|
|
3418
3620
|
onClose();
|
|
3419
3621
|
},
|
|
3420
3622
|
onClose,
|
|
@@ -3423,7 +3625,7 @@ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onC
|
|
|
3423
3625
|
/**
|
|
3424
3626
|
* Creates base default context menu handlers.
|
|
3425
3627
|
*/
|
|
3426
|
-
function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, getCopiedData, clearCopiedData) {
|
|
3628
|
+
function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, getCopiedData, clearCopiedData, history) {
|
|
3427
3629
|
// Wrap paste handler to clear storage after paste
|
|
3428
3630
|
const wrappedOnPaste = onPaste && getCopiedData && clearCopiedData
|
|
3429
3631
|
? (position) => {
|
|
@@ -3431,16 +3633,17 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
|
|
|
3431
3633
|
clearCopiedData();
|
|
3432
3634
|
}
|
|
3433
3635
|
: onPaste;
|
|
3434
|
-
// Function to check if paste data exists (called dynamically when menu opens)
|
|
3435
3636
|
const hasPasteData = getCopiedData ? () => !!getCopiedData() : undefined;
|
|
3637
|
+
const canUndo = history ? history.undoCount > 0 : undefined;
|
|
3638
|
+
const canRedo = history ? history.redoCount > 0 : undefined;
|
|
3436
3639
|
return {
|
|
3437
3640
|
onAddNode,
|
|
3438
3641
|
onPaste: wrappedOnPaste,
|
|
3439
3642
|
hasPasteData,
|
|
3440
3643
|
onUndo: runner ? () => runner.undo().then(() => onClose()) : undefined,
|
|
3441
3644
|
onRedo: runner ? () => runner.redo().then(() => onClose()) : undefined,
|
|
3442
|
-
canUndo
|
|
3443
|
-
canRedo
|
|
3645
|
+
canUndo,
|
|
3646
|
+
canRedo,
|
|
3444
3647
|
onClose,
|
|
3445
3648
|
};
|
|
3446
3649
|
}
|
|
@@ -3964,6 +4167,16 @@ function DefaultNodeContent({ data, isConnectable, }) {
|
|
|
3964
4167
|
} })] }));
|
|
3965
4168
|
}
|
|
3966
4169
|
|
|
4170
|
+
// Helper to format shortcut for current platform
|
|
4171
|
+
function formatShortcut(shortcut) {
|
|
4172
|
+
const isMac = typeof navigator !== "undefined" &&
|
|
4173
|
+
navigator.userAgent.toLowerCase().includes("mac");
|
|
4174
|
+
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4175
|
+
}
|
|
4176
|
+
function ContextMenuButton({ label, onClick, disabled = false, shortcut, enableKeyboardShortcuts = true, }) {
|
|
4177
|
+
return (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: onClick, disabled: disabled, children: [jsxRuntime.jsx("span", { children: label }), enableKeyboardShortcuts && shortcut && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(shortcut) }))] }));
|
|
4178
|
+
}
|
|
4179
|
+
|
|
3967
4180
|
function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
3968
4181
|
undo: "⌘/Ctrl + Z",
|
|
3969
4182
|
redo: "⌘/Ctrl + Shift + Z",
|
|
@@ -3971,41 +4184,24 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
|
|
|
3971
4184
|
}, }) {
|
|
3972
4185
|
const rf = react.useReactFlow();
|
|
3973
4186
|
const [query, setQuery] = React.useState("");
|
|
3974
|
-
const [canUndo, setCanUndo] = React.useState(false);
|
|
3975
|
-
const [canRedo, setCanRedo] = React.useState(false);
|
|
3976
4187
|
const [hasPasteData, setHasPasteData] = React.useState(false);
|
|
3977
4188
|
const q = query.trim().toLowerCase();
|
|
3978
4189
|
const filteredIds = q
|
|
3979
4190
|
? nodeIds.filter((id) => id.toLowerCase().includes(q))
|
|
3980
4191
|
: nodeIds;
|
|
3981
|
-
|
|
4192
|
+
const canUndo = handlers.canUndo ?? false;
|
|
4193
|
+
const canRedo = handlers.canRedo ?? false;
|
|
3982
4194
|
React.useEffect(() => {
|
|
3983
4195
|
if (!open)
|
|
3984
4196
|
return;
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
const result = await handlers.canRedo();
|
|
3994
|
-
if (!cancelled)
|
|
3995
|
-
setCanRedo(result);
|
|
3996
|
-
}
|
|
3997
|
-
// Check paste data dynamically
|
|
3998
|
-
if (handlers.hasPasteData) {
|
|
3999
|
-
const result = handlers.hasPasteData();
|
|
4000
|
-
if (!cancelled)
|
|
4001
|
-
setHasPasteData(result);
|
|
4002
|
-
}
|
|
4003
|
-
};
|
|
4004
|
-
checkAvailability();
|
|
4005
|
-
return () => {
|
|
4006
|
-
cancelled = true;
|
|
4007
|
-
};
|
|
4008
|
-
}, [open, handlers.canUndo, handlers.canRedo, handlers.hasPasteData]);
|
|
4197
|
+
if (handlers.hasPasteData) {
|
|
4198
|
+
const result = handlers.hasPasteData();
|
|
4199
|
+
setHasPasteData(result);
|
|
4200
|
+
}
|
|
4201
|
+
else {
|
|
4202
|
+
setHasPasteData(false);
|
|
4203
|
+
}
|
|
4204
|
+
}, [open, handlers.hasPasteData]);
|
|
4009
4205
|
const root = { __children: {} };
|
|
4010
4206
|
for (const id of filteredIds) {
|
|
4011
4207
|
const parts = id.split(".");
|
|
@@ -4070,12 +4266,6 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
|
|
|
4070
4266
|
handlers.onPaste(p);
|
|
4071
4267
|
handlers.onClose();
|
|
4072
4268
|
};
|
|
4073
|
-
// Helper to format shortcut for current platform
|
|
4074
|
-
const formatShortcut = (shortcut) => {
|
|
4075
|
-
const isMac = typeof navigator !== "undefined" &&
|
|
4076
|
-
navigator.userAgent.toLowerCase().includes("mac");
|
|
4077
|
-
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4078
|
-
};
|
|
4079
4269
|
const renderTree = (tree, path = []) => {
|
|
4080
4270
|
const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
|
|
4081
4271
|
return (jsxRuntime.jsx("div", { children: entries.map(([key, child]) => {
|
|
@@ -4093,13 +4283,17 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
|
|
|
4093
4283
|
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) => {
|
|
4094
4284
|
e.preventDefault();
|
|
4095
4285
|
e.stopPropagation();
|
|
4096
|
-
}, children: [hasPasteData && handlers.onPaste && (jsxRuntime.
|
|
4286
|
+
}, children: [hasPasteData && handlers.onPaste && (jsxRuntime.jsx(ContextMenuButton, { label: "Paste", onClick: handlePaste, shortcut: keyboardShortcuts.paste, enableKeyboardShortcuts: enableKeyboardShortcuts })), (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.jsx(ContextMenuButton, { label: "Undo", onClick: handlers.onUndo, disabled: !canUndo, shortcut: keyboardShortcuts.undo, enableKeyboardShortcuts: enableKeyboardShortcuts })), handlers.onRedo && (jsxRuntime.jsx(ContextMenuButton, { label: "Redo", onClick: handlers.onRedo, disabled: !canRedo, shortcut: keyboardShortcuts.redo, enableKeyboardShortcuts: enableKeyboardShortcuts }))] })), hasPasteData &&
|
|
4097
4287
|
handlers.onPaste &&
|
|
4098
4288
|
!handlers.onUndo &&
|
|
4099
|
-
!handlers.onRedo && jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxRuntime.jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsxRuntime.jsx("div", { className: "px-2 pb-1", children: jsxRuntime.jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 px-2 py-1 text-sm outline-none focus:border-gray-400 select-text", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsxRuntime.jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
|
|
4289
|
+
!handlers.onRedo && jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxRuntime.jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsxRuntime.jsx("div", { className: "px-2 pb-1", children: jsxRuntime.jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 rounded px-2 py-1 text-sm outline-none focus:border-gray-400 select-text", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsxRuntime.jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
|
|
4100
4290
|
}
|
|
4101
4291
|
|
|
4102
|
-
function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs,
|
|
4292
|
+
function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
4293
|
+
copy: "⌘/Ctrl + C",
|
|
4294
|
+
duplicate: "⌘/Ctrl + D",
|
|
4295
|
+
delete: "Delete",
|
|
4296
|
+
}, }) {
|
|
4103
4297
|
const ref = React.useRef(null);
|
|
4104
4298
|
// outside click + ESC
|
|
4105
4299
|
React.useEffect(() => {
|
|
@@ -4137,7 +4331,7 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
4137
4331
|
return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
|
|
4138
4332
|
e.preventDefault();
|
|
4139
4333
|
e.stopPropagation();
|
|
4140
|
-
}, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx(
|
|
4334
|
+
}, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx(ContextMenuButton, { label: "Delete", onClick: handlers.onDelete, shortcut: keyboardShortcuts.delete, enableKeyboardShortcuts: enableKeyboardShortcuts }), jsxRuntime.jsx(ContextMenuButton, { label: "Duplicate", onClick: handlers.onDuplicate, shortcut: keyboardShortcuts.duplicate, enableKeyboardShortcuts: enableKeyboardShortcuts }), 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.jsx(ContextMenuButton, { label: "Copy", onClick: handlers.onCopy, shortcut: keyboardShortcuts.copy, enableKeyboardShortcuts: enableKeyboardShortcuts }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" }), bakeableOutputs.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h)))] }))] }));
|
|
4141
4335
|
}
|
|
4142
4336
|
|
|
4143
4337
|
function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
@@ -4170,12 +4364,6 @@ function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcu
|
|
|
4170
4364
|
if (open)
|
|
4171
4365
|
ref.current?.focus();
|
|
4172
4366
|
}, [open]);
|
|
4173
|
-
// Helper to format shortcut for current platform
|
|
4174
|
-
const formatShortcut = (shortcut) => {
|
|
4175
|
-
const isMac = typeof navigator !== "undefined" &&
|
|
4176
|
-
navigator.userAgent.toLowerCase().includes("mac");
|
|
4177
|
-
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4178
|
-
};
|
|
4179
4367
|
if (!open || !clientPos)
|
|
4180
4368
|
return null;
|
|
4181
4369
|
// Clamp menu position to viewport
|
|
@@ -4187,7 +4375,7 @@ function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcu
|
|
|
4187
4375
|
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) => {
|
|
4188
4376
|
e.preventDefault();
|
|
4189
4377
|
e.stopPropagation();
|
|
4190
|
-
}, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.
|
|
4378
|
+
}, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.jsx(ContextMenuButton, { label: "Copy", onClick: handlers.onCopy, shortcut: keyboardShortcuts.copy, enableKeyboardShortcuts: enableKeyboardShortcuts }), jsxRuntime.jsx(ContextMenuButton, { label: "Delete", onClick: handlers.onDelete, shortcut: keyboardShortcuts.delete, enableKeyboardShortcuts: enableKeyboardShortcuts })] }));
|
|
4191
4379
|
}
|
|
4192
4380
|
|
|
4193
4381
|
const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
|
|
@@ -4195,7 +4383,6 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4195
4383
|
const nodeValidation = validationByNode;
|
|
4196
4384
|
const edgeValidation = validationByEdge.errors;
|
|
4197
4385
|
const [registryVersion, setRegistryVersion] = React.useState(0);
|
|
4198
|
-
// Keep stable references for nodes/edges to avoid unnecessary updates
|
|
4199
4386
|
const prevNodesRef = React.useRef([]);
|
|
4200
4387
|
const prevEdgesRef = React.useRef([]);
|
|
4201
4388
|
function retainStabilityById(prev, next, isSame) {
|
|
@@ -4525,7 +4712,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4525
4712
|
setNodeMenuOpen(false);
|
|
4526
4713
|
setSelectionMenuOpen(false);
|
|
4527
4714
|
};
|
|
4528
|
-
const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
|
|
4715
|
+
const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs, commit: true }), [wb]);
|
|
4529
4716
|
const onCloseMenu = React.useCallback(() => {
|
|
4530
4717
|
setMenuOpen(false);
|
|
4531
4718
|
}, []);
|
|
@@ -4550,16 +4737,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4550
4737
|
get: () => wb.getCopiedData(),
|
|
4551
4738
|
set: (data) => wb.setCopiedData(data),
|
|
4552
4739
|
};
|
|
4553
|
-
const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu,
|
|
4554
|
-
// Paste handler - checks storage dynamically when called
|
|
4555
|
-
// Only provide handler if storage has data or might have data (for dynamic checking)
|
|
4556
|
-
(position) => {
|
|
4740
|
+
const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu, (position) => {
|
|
4557
4741
|
const data = storage.get();
|
|
4558
4742
|
if (!data)
|
|
4559
4743
|
return;
|
|
4560
|
-
wb.pasteCopiedData(data, position);
|
|
4744
|
+
wb.pasteCopiedData(data, position, { commit: true, reason: "paste" });
|
|
4561
4745
|
onCloseMenu();
|
|
4562
|
-
}, runner, () => storage.get(), () => storage.set(null));
|
|
4746
|
+
}, runner, () => storage.get(), () => storage.set(null), wb.getHistory());
|
|
4563
4747
|
if (overrides?.getDefaultContextMenuHandlers) {
|
|
4564
4748
|
return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
|
|
4565
4749
|
}
|
|
@@ -4578,9 +4762,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4578
4762
|
}, runner);
|
|
4579
4763
|
if (overrides?.getSelectionContextMenuHandlers) {
|
|
4580
4764
|
const selection = wb.getSelection();
|
|
4581
|
-
return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, {
|
|
4582
|
-
getDefaultNodeSize: overrides.getDefaultNodeSize,
|
|
4583
|
-
});
|
|
4765
|
+
return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, { getDefaultNodeSize: overrides.getDefaultNodeSize });
|
|
4584
4766
|
}
|
|
4585
4767
|
return baseHandlers;
|
|
4586
4768
|
}, [wb, runner, overrides, onCloseSelectionMenu]);
|
|
@@ -4626,6 +4808,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4626
4808
|
redo: "⌘/Ctrl + Shift + Z",
|
|
4627
4809
|
copy: "⌘/Ctrl + C",
|
|
4628
4810
|
paste: "⌘/Ctrl + V",
|
|
4811
|
+
duplicate: "⌘/Ctrl + D",
|
|
4629
4812
|
delete: "Delete",
|
|
4630
4813
|
};
|
|
4631
4814
|
// Keyboard shortcut handler
|
|
@@ -4650,9 +4833,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4650
4833
|
e.preventDefault();
|
|
4651
4834
|
if (runner &&
|
|
4652
4835
|
"onUndo" in defaultContextMenuHandlers &&
|
|
4653
|
-
defaultContextMenuHandlers.onUndo
|
|
4654
|
-
|
|
4655
|
-
if (canUndo) {
|
|
4836
|
+
defaultContextMenuHandlers.onUndo &&
|
|
4837
|
+
defaultContextMenuHandlers.canUndo) {
|
|
4838
|
+
if (defaultContextMenuHandlers.canUndo) {
|
|
4656
4839
|
defaultContextMenuHandlers.onUndo();
|
|
4657
4840
|
}
|
|
4658
4841
|
}
|
|
@@ -4663,9 +4846,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4663
4846
|
e.preventDefault();
|
|
4664
4847
|
if (runner &&
|
|
4665
4848
|
"onRedo" in defaultContextMenuHandlers &&
|
|
4666
|
-
defaultContextMenuHandlers.onRedo
|
|
4667
|
-
|
|
4668
|
-
if (canRedo) {
|
|
4849
|
+
defaultContextMenuHandlers.onRedo &&
|
|
4850
|
+
defaultContextMenuHandlers.canRedo) {
|
|
4851
|
+
if (defaultContextMenuHandlers.canRedo) {
|
|
4669
4852
|
defaultContextMenuHandlers.onRedo();
|
|
4670
4853
|
}
|
|
4671
4854
|
}
|
|
@@ -4676,12 +4859,26 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4676
4859
|
const selection = wb.getSelection();
|
|
4677
4860
|
if (selection.nodes.length > 0 || selection.edges.length > 0) {
|
|
4678
4861
|
e.preventDefault();
|
|
4679
|
-
|
|
4862
|
+
// If single node selected, use node context menu handler; otherwise use selection handler
|
|
4863
|
+
if (selection.nodes.length === 1 && nodeContextMenuHandlers?.onCopy) {
|
|
4864
|
+
nodeContextMenuHandlers.onCopy();
|
|
4865
|
+
}
|
|
4866
|
+
else if (selectionContextMenuHandlers.onCopy) {
|
|
4680
4867
|
selectionContextMenuHandlers.onCopy();
|
|
4681
4868
|
}
|
|
4682
4869
|
}
|
|
4683
4870
|
return;
|
|
4684
4871
|
}
|
|
4872
|
+
// Duplicate: Cmd/Ctrl + D
|
|
4873
|
+
if (modKey && key === "d" && !e.shiftKey && !e.altKey) {
|
|
4874
|
+
const selection = wb.getSelection();
|
|
4875
|
+
if (selection.nodes.length === 1 &&
|
|
4876
|
+
nodeContextMenuHandlers?.onDuplicate) {
|
|
4877
|
+
e.preventDefault();
|
|
4878
|
+
nodeContextMenuHandlers.onDuplicate();
|
|
4879
|
+
}
|
|
4880
|
+
return;
|
|
4881
|
+
}
|
|
4685
4882
|
// Paste: Cmd/Ctrl + V
|
|
4686
4883
|
if (modKey && key === "v" && !e.shiftKey && !e.altKey) {
|
|
4687
4884
|
e.preventDefault();
|
|
@@ -4711,6 +4908,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4711
4908
|
runner,
|
|
4712
4909
|
defaultContextMenuHandlers,
|
|
4713
4910
|
selectionContextMenuHandlers,
|
|
4911
|
+
nodeContextMenuHandlers,
|
|
4714
4912
|
rfInstanceRef,
|
|
4715
4913
|
]);
|
|
4716
4914
|
// Get custom renderers from UI extension registry (reactive to uiVersion changes)
|
|
@@ -4727,7 +4925,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4727
4925
|
const onMoveEnd = React.useCallback(() => {
|
|
4728
4926
|
if (rfInstanceRef.current) {
|
|
4729
4927
|
const viewport = rfInstanceRef.current.getViewport();
|
|
4730
|
-
wb.setViewport({ x: viewport.x, y: viewport.y, zoom: viewport.zoom }
|
|
4928
|
+
wb.setViewport({ x: viewport.x, y: viewport.y, zoom: viewport.zoom });
|
|
4731
4929
|
}
|
|
4732
4930
|
}, [wb]);
|
|
4733
4931
|
const viewportRef = React.useRef(null);
|
|
@@ -4763,7 +4961,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4763
4961
|
? { enableKeyboardShortcuts, keyboardShortcuts }
|
|
4764
4962
|
: {}) })) : (jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts })), !!nodeAtMenu &&
|
|
4765
4963
|
nodeContextMenuHandlers &&
|
|
4766
|
-
(NodeContextMenuRenderer ? (jsxRuntime.jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs
|
|
4964
|
+
(NodeContextMenuRenderer ? (jsxRuntime.jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs, ...(enableKeyboardShortcuts !== false
|
|
4965
|
+
? { enableKeyboardShortcuts, keyboardShortcuts }
|
|
4966
|
+
: {}) })) : (jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))), selectionMenuOpen && selectionMenuPos && (jsxRuntime.jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))] }) }) }));
|
|
4767
4967
|
});
|
|
4768
4968
|
|
|
4769
4969
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
|