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