@bian-womp/spark-workbench 0.2.70 → 0.2.72
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 +281 -129
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts +10 -9
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/core/contracts.d.ts +5 -2
- 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 +1 -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 +2 -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/{esm/src/misc → cjs/src/misc/context-menu}/NodeContextMenu.d.ts +1 -1
- 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 +281 -129
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/InMemoryWorkbench.d.ts +10 -9
- package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/esm/src/core/contracts.d.ts +5 -2
- 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 +1 -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 +2 -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/{cjs/src/misc → esm/src/misc/context-menu}/NodeContextMenu.d.ts +1 -1
- 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.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.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
|
@@ -125,13 +125,15 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
125
125
|
constructor() {
|
|
126
126
|
super(...arguments);
|
|
127
127
|
this.def = { nodes: [], edges: [] };
|
|
128
|
-
this.positions = {};
|
|
129
128
|
this.listeners = new Map();
|
|
129
|
+
this.positions = {};
|
|
130
130
|
this.selection = {
|
|
131
131
|
nodes: [],
|
|
132
132
|
edges: [],
|
|
133
133
|
};
|
|
134
134
|
this.viewport = null;
|
|
135
|
+
this.runtimeState = null;
|
|
136
|
+
this.historyState = undefined;
|
|
135
137
|
this.copiedData = null;
|
|
136
138
|
}
|
|
137
139
|
setRegistry(registry) {
|
|
@@ -277,14 +279,6 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
277
279
|
});
|
|
278
280
|
}
|
|
279
281
|
// Position and selection APIs for React Flow bridge
|
|
280
|
-
setPosition(nodeId, pos, options) {
|
|
281
|
-
this.positions[nodeId] = pos;
|
|
282
|
-
this.emit("graphUiChanged", {
|
|
283
|
-
def: this.def,
|
|
284
|
-
change: { type: "moveNode", nodeId, pos },
|
|
285
|
-
...options,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
282
|
setPositions(map, options) {
|
|
289
283
|
this.positions = { ...map };
|
|
290
284
|
this.emit("graphUiChanged", {
|
|
@@ -297,6 +291,8 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
297
291
|
return { ...this.positions };
|
|
298
292
|
}
|
|
299
293
|
setSelection(sel, options) {
|
|
294
|
+
if (lod.isEqual(this.selection, sel))
|
|
295
|
+
return;
|
|
300
296
|
this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
|
|
301
297
|
this.emit("selectionChanged", this.selection);
|
|
302
298
|
this.emit("graphUiChanged", {
|
|
@@ -374,6 +370,28 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
374
370
|
this.viewport = { ...ui.viewport };
|
|
375
371
|
}
|
|
376
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
|
+
this.emit("historyChanged", { history });
|
|
385
|
+
}
|
|
386
|
+
getNodeRuntimeMetadata(nodeId) {
|
|
387
|
+
return this.runtimeState?.nodes[nodeId];
|
|
388
|
+
}
|
|
389
|
+
updateNodeRuntimeMetadata(nodeId, updater) {
|
|
390
|
+
const current = this.runtimeState ?? { nodes: {} };
|
|
391
|
+
const nodeMeta = current.nodes[nodeId] ?? {};
|
|
392
|
+
const updated = updater({ ...nodeMeta });
|
|
393
|
+
this.runtimeState = { nodes: { ...current.nodes, [nodeId]: updated } };
|
|
394
|
+
}
|
|
377
395
|
on(event, handler) {
|
|
378
396
|
if (!this.listeners.has(event))
|
|
379
397
|
this.listeners.set(event, new Set());
|
|
@@ -679,14 +697,10 @@ class AbstractGraphRunner {
|
|
|
679
697
|
async redo() {
|
|
680
698
|
return false;
|
|
681
699
|
}
|
|
682
|
-
async canUndo() {
|
|
683
|
-
return false;
|
|
684
|
-
}
|
|
685
|
-
async canRedo() {
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
700
|
// Optional commit support
|
|
689
|
-
async commit(_reason) {
|
|
701
|
+
async commit(_reason) {
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
690
704
|
}
|
|
691
705
|
|
|
692
706
|
// Counter for generating readable runner IDs
|
|
@@ -1436,7 +1450,8 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1436
1450
|
async commit(reason) {
|
|
1437
1451
|
const client = await this.ensureClient();
|
|
1438
1452
|
try {
|
|
1439
|
-
await client.commit(reason);
|
|
1453
|
+
const history = await client.commit(reason);
|
|
1454
|
+
return history;
|
|
1440
1455
|
}
|
|
1441
1456
|
catch (err) {
|
|
1442
1457
|
console.error("[RemoteGraphRunner] Error committing:", err);
|
|
@@ -1461,24 +1476,6 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1461
1476
|
return false;
|
|
1462
1477
|
}
|
|
1463
1478
|
}
|
|
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
1479
|
async snapshotFull() {
|
|
1483
1480
|
const client = await this.ensureClient();
|
|
1484
1481
|
try {
|
|
@@ -1920,9 +1917,13 @@ function useWorkbenchBridge(wb) {
|
|
|
1920
1917
|
}, [wb]);
|
|
1921
1918
|
const onNodesChange = React.useCallback((changes) => {
|
|
1922
1919
|
// Apply position updates continuously, but mark commit only on drag end
|
|
1920
|
+
const positions = {};
|
|
1921
|
+
let commit = false;
|
|
1923
1922
|
changes.forEach((c) => {
|
|
1924
1923
|
if (c.type === "position" && c.position) {
|
|
1925
|
-
|
|
1924
|
+
positions[c.id] = c.position;
|
|
1925
|
+
if (!c.dragging)
|
|
1926
|
+
commit = true;
|
|
1926
1927
|
}
|
|
1927
1928
|
});
|
|
1928
1929
|
// Derive next node selection from change set
|
|
@@ -1953,10 +1954,10 @@ function useWorkbenchBridge(wb) {
|
|
|
1953
1954
|
}
|
|
1954
1955
|
}
|
|
1955
1956
|
if (selectionChanged) {
|
|
1956
|
-
wb.setSelection({
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
});
|
|
1957
|
+
wb.setSelection({ nodes: Array.from(nextNodeIds), edges: current.edges }, { commit: !(Object.keys(positions).length && commit) });
|
|
1958
|
+
}
|
|
1959
|
+
if (Object.keys(positions).length > 0) {
|
|
1960
|
+
wb.setPositions(positions, { commit });
|
|
1960
1961
|
}
|
|
1961
1962
|
}, [wb]);
|
|
1962
1963
|
const onEdgesDelete = React.useCallback((edges) => edges.forEach((e, idx) => wb.disconnect(e.id, { commit: idx === edges.length - 1 })), [wb]);
|
|
@@ -1988,10 +1989,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1988
1989
|
}
|
|
1989
1990
|
}
|
|
1990
1991
|
if (selectionChanged) {
|
|
1991
|
-
wb.setSelection({
|
|
1992
|
-
nodes: current.nodes,
|
|
1993
|
-
edges: Array.from(nextEdgeIds),
|
|
1994
|
-
});
|
|
1992
|
+
wb.setSelection({ nodes: current.nodes, edges: Array.from(nextEdgeIds) }, { commit: true });
|
|
1995
1993
|
}
|
|
1996
1994
|
}, [wb]);
|
|
1997
1995
|
const onNodesDelete = React.useCallback((nodes) => {
|
|
@@ -2439,6 +2437,7 @@ async function download(wb, runner) {
|
|
|
2439
2437
|
try {
|
|
2440
2438
|
const def = wb.export();
|
|
2441
2439
|
const uiState = wb.getUIState();
|
|
2440
|
+
const runtimeState = wb.getRuntimeState();
|
|
2442
2441
|
let snapshot;
|
|
2443
2442
|
if (runner.isRunning()) {
|
|
2444
2443
|
const fullSnapshot = await runner.snapshotFull();
|
|
@@ -2448,6 +2447,7 @@ async function download(wb, runner) {
|
|
|
2448
2447
|
extData: {
|
|
2449
2448
|
...(fullSnapshot.extData || {}),
|
|
2450
2449
|
ui: uiState,
|
|
2450
|
+
runtime: runtimeState || undefined,
|
|
2451
2451
|
},
|
|
2452
2452
|
};
|
|
2453
2453
|
}
|
|
@@ -2458,7 +2458,7 @@ async function download(wb, runner) {
|
|
|
2458
2458
|
inputs,
|
|
2459
2459
|
outputs: {},
|
|
2460
2460
|
environment: {},
|
|
2461
|
-
extData: { ui: uiState },
|
|
2461
|
+
extData: { ui: uiState, runtime: runtimeState || undefined },
|
|
2462
2462
|
};
|
|
2463
2463
|
}
|
|
2464
2464
|
downloadJSON(snapshot, `spark-snapshot-${generateTimestamp()}.json`);
|
|
@@ -2484,6 +2484,9 @@ async function upload(parsed, wb, runner) {
|
|
|
2484
2484
|
if (extData.ui && typeof extData.ui === "object") {
|
|
2485
2485
|
wb.setUIState(extData.ui);
|
|
2486
2486
|
}
|
|
2487
|
+
if (extData.runtime && typeof extData.runtime === "object") {
|
|
2488
|
+
wb.setRuntimeState(extData.runtime);
|
|
2489
|
+
}
|
|
2487
2490
|
if (runner.isRunning()) {
|
|
2488
2491
|
await runner.applySnapshotFull({
|
|
2489
2492
|
def,
|
|
@@ -2511,6 +2514,18 @@ function useWorkbenchContext() {
|
|
|
2511
2514
|
return ctx;
|
|
2512
2515
|
}
|
|
2513
2516
|
|
|
2517
|
+
// Helper to compute invalidated status from runtime metadata
|
|
2518
|
+
function computeInvalidatedFromMetadata(metadata) {
|
|
2519
|
+
if (!metadata)
|
|
2520
|
+
return true;
|
|
2521
|
+
const { lastSuccessAt, lastInputAt, lastRunAt } = metadata;
|
|
2522
|
+
if (!lastSuccessAt && !lastRunAt)
|
|
2523
|
+
return true;
|
|
2524
|
+
if (!lastInputAt || Object.keys(lastInputAt).length === 0)
|
|
2525
|
+
return false;
|
|
2526
|
+
const maxInputTime = Math.max(...Object.values(lastInputAt));
|
|
2527
|
+
return maxInputTime > (lastSuccessAt ?? lastRunAt ?? 0);
|
|
2528
|
+
}
|
|
2514
2529
|
function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVersion, children, }) {
|
|
2515
2530
|
const [nodeStatus, setNodeStatus] = React.useState({});
|
|
2516
2531
|
const [edgeStatus, setEdgeStatus] = React.useState({});
|
|
@@ -2601,27 +2616,35 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2601
2616
|
}
|
|
2602
2617
|
return out;
|
|
2603
2618
|
}, [def, outputsMap, registry]);
|
|
2604
|
-
// Initialize nodes
|
|
2619
|
+
// Initialize nodes and derive invalidated status from persisted metadata
|
|
2605
2620
|
React.useEffect(() => {
|
|
2621
|
+
const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
|
|
2606
2622
|
setNodeStatus((prev) => {
|
|
2607
2623
|
const next = { ...prev };
|
|
2624
|
+
const metadata = workbenchRuntimeState;
|
|
2608
2625
|
for (const n of def.nodes) {
|
|
2609
2626
|
const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
|
|
2627
|
+
const nodeMeta = metadata.nodes[n.nodeId];
|
|
2610
2628
|
const updates = {};
|
|
2611
2629
|
if (cur.invalidated === undefined) {
|
|
2612
|
-
updates.invalidated =
|
|
2630
|
+
updates.invalidated = computeInvalidatedFromMetadata(nodeMeta);
|
|
2613
2631
|
}
|
|
2614
|
-
// Ensure activeRunIds is always initialized as an array
|
|
2615
2632
|
if (cur.activeRunIds === undefined) {
|
|
2616
2633
|
updates.activeRunIds = [];
|
|
2617
2634
|
}
|
|
2635
|
+
if (cur.activeRuns === undefined) {
|
|
2636
|
+
updates.activeRuns = 0;
|
|
2637
|
+
}
|
|
2638
|
+
if (nodeMeta?.lastErrorSummary && cur.lastError === undefined) {
|
|
2639
|
+
updates.lastError = nodeMeta.lastErrorSummary;
|
|
2640
|
+
}
|
|
2618
2641
|
if (Object.keys(updates).length > 0) {
|
|
2619
2642
|
next[n.nodeId] = { ...cur, ...updates };
|
|
2620
2643
|
}
|
|
2621
2644
|
}
|
|
2622
2645
|
return next;
|
|
2623
2646
|
});
|
|
2624
|
-
}, [def]);
|
|
2647
|
+
}, [def, wb]);
|
|
2625
2648
|
// Auto layout (simple layered layout)
|
|
2626
2649
|
const runAutoLayout = React.useCallback(() => {
|
|
2627
2650
|
const cur = wb.export();
|
|
@@ -2692,6 +2715,27 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2692
2715
|
}, [wb, registry, overrides?.getDefaultNodeSize]);
|
|
2693
2716
|
const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
|
|
2694
2717
|
const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
|
|
2718
|
+
// Helper to save runtime metadata to extData.runtime and workbench state
|
|
2719
|
+
const saveRuntimeMetadata = React.useCallback(async () => {
|
|
2720
|
+
try {
|
|
2721
|
+
const current = wb.getRuntimeState() ?? { nodes: {} };
|
|
2722
|
+
const metadata = { nodes: { ...current.nodes } };
|
|
2723
|
+
// Clean up metadata for nodes that no longer exist
|
|
2724
|
+
const nodeIds = new Set(def.nodes.map((n) => n.nodeId));
|
|
2725
|
+
for (const nodeId of Object.keys(metadata.nodes)) {
|
|
2726
|
+
if (!nodeIds.has(nodeId)) {
|
|
2727
|
+
delete metadata.nodes[nodeId];
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
// Save cleaned metadata to workbench state
|
|
2731
|
+
wb.setRuntimeState(metadata);
|
|
2732
|
+
// Save to extData.runtime via runner (no snapshotFull)
|
|
2733
|
+
await runner.setExtData?.({ runtime: metadata });
|
|
2734
|
+
}
|
|
2735
|
+
catch (err) {
|
|
2736
|
+
console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
|
|
2737
|
+
}
|
|
2738
|
+
}, [wb, def, runner]);
|
|
2695
2739
|
// Subscribe to runner/workbench events
|
|
2696
2740
|
React.useEffect(() => {
|
|
2697
2741
|
const add = (source, type) => (payload) => setEvents((prev) => {
|
|
@@ -2734,9 +2778,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2734
2778
|
wb.refreshValidation();
|
|
2735
2779
|
};
|
|
2736
2780
|
const offRunnerValue = runner.on("value", (e) => {
|
|
2781
|
+
const now = Date.now();
|
|
2737
2782
|
if (e?.io === "input") {
|
|
2738
|
-
const nodeId = e
|
|
2739
|
-
const handle = e
|
|
2783
|
+
const nodeId = e.nodeId;
|
|
2784
|
+
const handle = e.handle;
|
|
2785
|
+
// Track input timestamp in workbench runtime state
|
|
2786
|
+
wb.updateNodeRuntimeMetadata(nodeId, (nodeMeta) => ({
|
|
2787
|
+
...nodeMeta,
|
|
2788
|
+
lastInputAt: {
|
|
2789
|
+
...(nodeMeta.lastInputAt ?? {}),
|
|
2790
|
+
[handle]: now,
|
|
2791
|
+
},
|
|
2792
|
+
}));
|
|
2740
2793
|
setNodeStatus((s) => ({
|
|
2741
2794
|
...s,
|
|
2742
2795
|
[nodeId]: { ...s[nodeId], invalidated: true },
|
|
@@ -2744,6 +2797,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2744
2797
|
// Clear validation errors for this input when a valid value is set
|
|
2745
2798
|
setInputValidationErrors((prev) => prev.filter((err) => !(err.nodeId === nodeId && err.handle === handle)));
|
|
2746
2799
|
}
|
|
2800
|
+
else if (e?.io === "output") {
|
|
2801
|
+
const nodeId = e.nodeId;
|
|
2802
|
+
const handle = e.handle;
|
|
2803
|
+
// Track output timestamp in workbench runtime state
|
|
2804
|
+
wb.updateNodeRuntimeMetadata(nodeId, (nodeMeta) => ({
|
|
2805
|
+
...nodeMeta,
|
|
2806
|
+
lastOutputAt: {
|
|
2807
|
+
...(nodeMeta.lastOutputAt ?? {}),
|
|
2808
|
+
[handle]: now,
|
|
2809
|
+
},
|
|
2810
|
+
}));
|
|
2811
|
+
}
|
|
2747
2812
|
return add("runner", "value")(e);
|
|
2748
2813
|
});
|
|
2749
2814
|
const offRunnerError = runner.on("error", (e) => {
|
|
@@ -2762,6 +2827,24 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2762
2827
|
else if (nodeError.kind === "node-run" && nodeError.nodeId) {
|
|
2763
2828
|
const nodeId = nodeError.nodeId;
|
|
2764
2829
|
const runId = nodeError.runId;
|
|
2830
|
+
const now = Date.now();
|
|
2831
|
+
// Track error timestamp and summary in workbench runtime state
|
|
2832
|
+
const err = nodeError.err;
|
|
2833
|
+
let errorSummary;
|
|
2834
|
+
if (err && typeof err === "object") {
|
|
2835
|
+
const message = err.message || String(err);
|
|
2836
|
+
const code = err.code || err.statusCode;
|
|
2837
|
+
errorSummary = {
|
|
2838
|
+
message: typeof message === "string" ? message : String(message),
|
|
2839
|
+
code: typeof code === "number" ? code : undefined,
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
wb.updateNodeRuntimeMetadata(nodeId, (nodeMeta) => ({
|
|
2843
|
+
...nodeMeta,
|
|
2844
|
+
lastErrorAt: now,
|
|
2845
|
+
lastRunAt: now,
|
|
2846
|
+
...(errorSummary ? { lastErrorSummary: errorSummary } : {}),
|
|
2847
|
+
}));
|
|
2765
2848
|
setNodeStatus((s) => ({
|
|
2766
2849
|
...s,
|
|
2767
2850
|
[nodeId]: {
|
|
@@ -2816,6 +2899,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2816
2899
|
// If resolvedHandles are included in the event, use them directly (more efficient)
|
|
2817
2900
|
if (e?.resolvedHandles && Object.keys(e.resolvedHandles).length > 0) {
|
|
2818
2901
|
applyResolvedHandles(e.resolvedHandles);
|
|
2902
|
+
// Mark nodes whose handles changed as invalid
|
|
2903
|
+
const affectedNodeIds = Object.keys(e.resolvedHandles);
|
|
2904
|
+
if (affectedNodeIds.length > 0) {
|
|
2905
|
+
setNodeStatus((prev) => {
|
|
2906
|
+
const next = { ...prev };
|
|
2907
|
+
for (const id of affectedNodeIds) {
|
|
2908
|
+
const cur = next[id] ?? (next[id] = { activeRuns: 0, activeRunIds: [] });
|
|
2909
|
+
next[id] = { ...cur, invalidated: true };
|
|
2910
|
+
}
|
|
2911
|
+
return next;
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
// For broader invalidations (e.g. registry-changed, graph-updated), mark all nodes invalid
|
|
2916
|
+
if (e?.reason === "registry-changed" || e?.reason === "graph-updated") {
|
|
2917
|
+
setNodeStatus((prev) => {
|
|
2918
|
+
const next = { ...prev };
|
|
2919
|
+
for (const n of def.nodes) {
|
|
2920
|
+
const cur = next[n.nodeId] ??
|
|
2921
|
+
(next[n.nodeId] = { activeRuns: 0, activeRunIds: [] });
|
|
2922
|
+
next[n.nodeId] = { ...cur, invalidated: true };
|
|
2923
|
+
}
|
|
2924
|
+
return next;
|
|
2925
|
+
});
|
|
2819
2926
|
}
|
|
2820
2927
|
return add("runner", "invalidate")(e);
|
|
2821
2928
|
});
|
|
@@ -2825,6 +2932,12 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2825
2932
|
if (s.kind === "node-start") {
|
|
2826
2933
|
const id = s.nodeId;
|
|
2827
2934
|
const runId = s.runId;
|
|
2935
|
+
const now = Date.now();
|
|
2936
|
+
// Track run timestamp in workbench runtime state
|
|
2937
|
+
wb.updateNodeRuntimeMetadata(id, (nodeMeta) => ({
|
|
2938
|
+
...nodeMeta,
|
|
2939
|
+
lastRunAt: now,
|
|
2940
|
+
}));
|
|
2828
2941
|
// Validate runId is a non-empty string
|
|
2829
2942
|
const isValidRunId = runId && typeof runId === "string" && runId.length > 0;
|
|
2830
2943
|
if (!isValidRunId) {
|
|
@@ -2847,7 +2960,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2847
2960
|
};
|
|
2848
2961
|
});
|
|
2849
2962
|
// Start fallback animation window
|
|
2850
|
-
setFallbackStarts((prev) => ({ ...prev, [id]:
|
|
2963
|
+
setFallbackStarts((prev) => ({ ...prev, [id]: now }));
|
|
2851
2964
|
}
|
|
2852
2965
|
else if (s.kind === "node-progress") {
|
|
2853
2966
|
const id = s.nodeId;
|
|
@@ -2862,8 +2975,21 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2862
2975
|
else if (s.kind === "node-done") {
|
|
2863
2976
|
const id = s.nodeId;
|
|
2864
2977
|
const runId = s.runId;
|
|
2978
|
+
const now = Date.now();
|
|
2865
2979
|
// Validate runId is a non-empty string
|
|
2866
2980
|
const isValidRunId = runId && typeof runId === "string" && runId.length > 0;
|
|
2981
|
+
const hadError = !!(isValidRunId && errorRunsRef.current[id]?.[runId]);
|
|
2982
|
+
// Track success timestamp if no error in workbench runtime state
|
|
2983
|
+
if (!hadError) {
|
|
2984
|
+
wb.updateNodeRuntimeMetadata(id, (nodeMeta) => {
|
|
2985
|
+
const updated = { ...nodeMeta, lastSuccessAt: now };
|
|
2986
|
+
// Clear error summary on success
|
|
2987
|
+
if (updated.lastErrorSummary) {
|
|
2988
|
+
delete updated.lastErrorSummary;
|
|
2989
|
+
}
|
|
2990
|
+
return updated;
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2867
2993
|
setNodeStatus((prev) => {
|
|
2868
2994
|
const current = prev[id]?.activeRuns ?? 0;
|
|
2869
2995
|
const currentRunIds = prev[id]?.activeRunIds ?? [];
|
|
@@ -2875,7 +3001,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2875
3001
|
const nextRunIds = isValidRunId
|
|
2876
3002
|
? currentRunIds.filter((rid) => rid !== runId)
|
|
2877
3003
|
: currentRunIds;
|
|
2878
|
-
const hadError = !!(isValidRunId && errorRunsRef.current[id]?.[runId]);
|
|
2879
3004
|
const keepProgress = hadError || nextActive > 0;
|
|
2880
3005
|
// Clear error flag for this runId
|
|
2881
3006
|
if (isValidRunId && errorRunsRef.current[id]?.[runId]) {
|
|
@@ -2969,10 +3094,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2969
3094
|
}
|
|
2970
3095
|
if (!runner.isRunning()) {
|
|
2971
3096
|
if (event.commit) {
|
|
2972
|
-
|
|
2973
|
-
await runner.commit(reason).catch((err) => {
|
|
3097
|
+
await saveRuntimeMetadata();
|
|
3098
|
+
const history = await runner.commit(reason).catch((err) => {
|
|
2974
3099
|
console.error("[WorkbenchContext] Error committing:", err);
|
|
3100
|
+
return undefined;
|
|
2975
3101
|
});
|
|
3102
|
+
if (history) {
|
|
3103
|
+
wb.setHistory(history);
|
|
3104
|
+
}
|
|
2976
3105
|
}
|
|
2977
3106
|
return;
|
|
2978
3107
|
}
|
|
@@ -2999,10 +3128,16 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2999
3128
|
await runner.update(event.def, { dry: event.dry });
|
|
3000
3129
|
}
|
|
3001
3130
|
if (event.commit) {
|
|
3002
|
-
|
|
3003
|
-
|
|
3131
|
+
await saveRuntimeMetadata();
|
|
3132
|
+
const history = await runner
|
|
3133
|
+
.commit(event.reason ?? reason)
|
|
3134
|
+
.catch((err) => {
|
|
3004
3135
|
console.error("[WorkbenchContext] Error committing after update:", err);
|
|
3136
|
+
return undefined;
|
|
3005
3137
|
});
|
|
3138
|
+
if (history) {
|
|
3139
|
+
wb.setHistory(history);
|
|
3140
|
+
}
|
|
3006
3141
|
}
|
|
3007
3142
|
}
|
|
3008
3143
|
catch (err) {
|
|
@@ -3014,23 +3149,48 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3014
3149
|
setSelectedNodeId(sel.nodes?.[0]);
|
|
3015
3150
|
setSelectedEdgeId(sel.edges?.[0]);
|
|
3016
3151
|
if (sel.commit) {
|
|
3017
|
-
|
|
3018
|
-
|
|
3152
|
+
await saveRuntimeMetadata();
|
|
3153
|
+
const history = await runner
|
|
3154
|
+
.commit(sel.reason ?? "selection")
|
|
3155
|
+
.catch((err) => {
|
|
3019
3156
|
console.error("[WorkbenchContext] Error committing selection change:", err);
|
|
3157
|
+
return undefined;
|
|
3020
3158
|
});
|
|
3159
|
+
if (history) {
|
|
3160
|
+
wb.setHistory(history);
|
|
3161
|
+
}
|
|
3021
3162
|
}
|
|
3022
3163
|
});
|
|
3023
3164
|
const offWbGraphUiChanged = wb.on("graphUiChanged", async (event) => {
|
|
3024
3165
|
// Only commit if commit flag is true (e.g., drag end, not during dragging)
|
|
3025
3166
|
if (event.commit) {
|
|
3167
|
+
// Build detailed reason from change type
|
|
3168
|
+
let reason = "ui-changed";
|
|
3026
3169
|
if (event.change) {
|
|
3027
|
-
event.change.type;
|
|
3170
|
+
const changeType = event.change.type;
|
|
3171
|
+
if (changeType === "moveNode") {
|
|
3172
|
+
reason = "move-node";
|
|
3173
|
+
}
|
|
3174
|
+
else if (changeType === "moveNodes") {
|
|
3175
|
+
reason = "move-nodes";
|
|
3176
|
+
}
|
|
3177
|
+
else if (changeType === "selection") {
|
|
3178
|
+
reason = "selection";
|
|
3179
|
+
}
|
|
3180
|
+
else if (changeType === "viewport") {
|
|
3181
|
+
reason = "viewport";
|
|
3182
|
+
}
|
|
3028
3183
|
}
|
|
3029
|
-
await
|
|
3030
|
-
|
|
3184
|
+
await saveRuntimeMetadata();
|
|
3185
|
+
const history = await runner
|
|
3186
|
+
.commit(event.reason ?? reason)
|
|
3031
3187
|
.catch((err) => {
|
|
3032
3188
|
console.error("[WorkbenchContext] Error committing UI changes:", err);
|
|
3189
|
+
return undefined;
|
|
3033
3190
|
});
|
|
3191
|
+
if (history) {
|
|
3192
|
+
wb.setHistory(history);
|
|
3193
|
+
}
|
|
3034
3194
|
}
|
|
3035
3195
|
});
|
|
3036
3196
|
const offWbError = wb.on("error", add("workbench", "error"));
|
|
@@ -3051,11 +3211,24 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3051
3211
|
console.error("Failed to handle registry changed event");
|
|
3052
3212
|
}
|
|
3053
3213
|
});
|
|
3054
|
-
// Handle transport
|
|
3214
|
+
// Handle transport changes: reset runtime status when connection is lost
|
|
3055
3215
|
const offRunnerTransport = runner.on("transport", (t) => {
|
|
3056
3216
|
if (t.state === "disconnected") {
|
|
3057
3217
|
console.info("[WorkbenchContext] Transport disconnected, resetting node status");
|
|
3058
|
-
|
|
3218
|
+
// Reinitialize node status with invalidated=true for all nodes
|
|
3219
|
+
setNodeStatus(() => {
|
|
3220
|
+
const next = {};
|
|
3221
|
+
const metadata = wb.getRuntimeState() ?? { nodes: {} };
|
|
3222
|
+
for (const n of def.nodes) {
|
|
3223
|
+
const nodeMeta = metadata.nodes[n.nodeId];
|
|
3224
|
+
next[n.nodeId] = {
|
|
3225
|
+
activeRuns: 0,
|
|
3226
|
+
activeRunIds: [],
|
|
3227
|
+
invalidated: computeInvalidatedFromMetadata(nodeMeta),
|
|
3228
|
+
};
|
|
3229
|
+
}
|
|
3230
|
+
return next;
|
|
3231
|
+
});
|
|
3059
3232
|
setEdgeStatus({});
|
|
3060
3233
|
setFallbackStarts({});
|
|
3061
3234
|
errorRunsRef.current = {};
|
|
@@ -3453,7 +3626,7 @@ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onC
|
|
|
3453
3626
|
/**
|
|
3454
3627
|
* Creates base default context menu handlers.
|
|
3455
3628
|
*/
|
|
3456
|
-
function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, getCopiedData, clearCopiedData) {
|
|
3629
|
+
function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, getCopiedData, clearCopiedData, history) {
|
|
3457
3630
|
// Wrap paste handler to clear storage after paste
|
|
3458
3631
|
const wrappedOnPaste = onPaste && getCopiedData && clearCopiedData
|
|
3459
3632
|
? (position) => {
|
|
@@ -3461,16 +3634,17 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
|
|
|
3461
3634
|
clearCopiedData();
|
|
3462
3635
|
}
|
|
3463
3636
|
: onPaste;
|
|
3464
|
-
// Function to check if paste data exists (called dynamically when menu opens)
|
|
3465
3637
|
const hasPasteData = getCopiedData ? () => !!getCopiedData() : undefined;
|
|
3638
|
+
const canUndo = history ? history.undoCount > 0 : undefined;
|
|
3639
|
+
const canRedo = history ? history.redoCount > 0 : undefined;
|
|
3466
3640
|
return {
|
|
3467
3641
|
onAddNode,
|
|
3468
3642
|
onPaste: wrappedOnPaste,
|
|
3469
3643
|
hasPasteData,
|
|
3470
3644
|
onUndo: runner ? () => runner.undo().then(() => onClose()) : undefined,
|
|
3471
3645
|
onRedo: runner ? () => runner.redo().then(() => onClose()) : undefined,
|
|
3472
|
-
canUndo
|
|
3473
|
-
canRedo
|
|
3646
|
+
canUndo,
|
|
3647
|
+
canRedo,
|
|
3474
3648
|
onClose,
|
|
3475
3649
|
};
|
|
3476
3650
|
}
|
|
@@ -3994,6 +4168,16 @@ function DefaultNodeContent({ data, isConnectable, }) {
|
|
|
3994
4168
|
} })] }));
|
|
3995
4169
|
}
|
|
3996
4170
|
|
|
4171
|
+
// Helper to format shortcut for current platform
|
|
4172
|
+
function formatShortcut(shortcut) {
|
|
4173
|
+
const isMac = typeof navigator !== "undefined" &&
|
|
4174
|
+
navigator.userAgent.toLowerCase().includes("mac");
|
|
4175
|
+
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4176
|
+
}
|
|
4177
|
+
function ContextMenuButton({ label, onClick, disabled = false, shortcut, enableKeyboardShortcuts = true, }) {
|
|
4178
|
+
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) }))] }));
|
|
4179
|
+
}
|
|
4180
|
+
|
|
3997
4181
|
function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
3998
4182
|
undo: "⌘/Ctrl + Z",
|
|
3999
4183
|
redo: "⌘/Ctrl + Shift + Z",
|
|
@@ -4001,41 +4185,24 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
|
|
|
4001
4185
|
}, }) {
|
|
4002
4186
|
const rf = react.useReactFlow();
|
|
4003
4187
|
const [query, setQuery] = React.useState("");
|
|
4004
|
-
const [canUndo, setCanUndo] = React.useState(false);
|
|
4005
|
-
const [canRedo, setCanRedo] = React.useState(false);
|
|
4006
4188
|
const [hasPasteData, setHasPasteData] = React.useState(false);
|
|
4007
4189
|
const q = query.trim().toLowerCase();
|
|
4008
4190
|
const filteredIds = q
|
|
4009
4191
|
? nodeIds.filter((id) => id.toLowerCase().includes(q))
|
|
4010
4192
|
: nodeIds;
|
|
4011
|
-
|
|
4193
|
+
const canUndo = handlers.canUndo ?? false;
|
|
4194
|
+
const canRedo = handlers.canRedo ?? false;
|
|
4012
4195
|
React.useEffect(() => {
|
|
4013
4196
|
if (!open)
|
|
4014
4197
|
return;
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
const result = await handlers.canRedo();
|
|
4024
|
-
if (!cancelled)
|
|
4025
|
-
setCanRedo(result);
|
|
4026
|
-
}
|
|
4027
|
-
// Check paste data dynamically
|
|
4028
|
-
if (handlers.hasPasteData) {
|
|
4029
|
-
const result = handlers.hasPasteData();
|
|
4030
|
-
if (!cancelled)
|
|
4031
|
-
setHasPasteData(result);
|
|
4032
|
-
}
|
|
4033
|
-
};
|
|
4034
|
-
checkAvailability();
|
|
4035
|
-
return () => {
|
|
4036
|
-
cancelled = true;
|
|
4037
|
-
};
|
|
4038
|
-
}, [open, handlers.canUndo, handlers.canRedo, handlers.hasPasteData]);
|
|
4198
|
+
if (handlers.hasPasteData) {
|
|
4199
|
+
const result = handlers.hasPasteData();
|
|
4200
|
+
setHasPasteData(result);
|
|
4201
|
+
}
|
|
4202
|
+
else {
|
|
4203
|
+
setHasPasteData(false);
|
|
4204
|
+
}
|
|
4205
|
+
}, [open, handlers.hasPasteData]);
|
|
4039
4206
|
const root = { __children: {} };
|
|
4040
4207
|
for (const id of filteredIds) {
|
|
4041
4208
|
const parts = id.split(".");
|
|
@@ -4100,12 +4267,6 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
|
|
|
4100
4267
|
handlers.onPaste(p);
|
|
4101
4268
|
handlers.onClose();
|
|
4102
4269
|
};
|
|
4103
|
-
// Helper to format shortcut for current platform
|
|
4104
|
-
const formatShortcut = (shortcut) => {
|
|
4105
|
-
const isMac = typeof navigator !== "undefined" &&
|
|
4106
|
-
navigator.userAgent.toLowerCase().includes("mac");
|
|
4107
|
-
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4108
|
-
};
|
|
4109
4270
|
const renderTree = (tree, path = []) => {
|
|
4110
4271
|
const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
|
|
4111
4272
|
return (jsxRuntime.jsx("div", { children: entries.map(([key, child]) => {
|
|
@@ -4123,7 +4284,7 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
|
|
|
4123
4284
|
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) => {
|
|
4124
4285
|
e.preventDefault();
|
|
4125
4286
|
e.stopPropagation();
|
|
4126
|
-
}, children: [hasPasteData && handlers.onPaste && (jsxRuntime.
|
|
4287
|
+
}, 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 &&
|
|
4127
4288
|
handlers.onPaste &&
|
|
4128
4289
|
!handlers.onUndo &&
|
|
4129
4290
|
!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" })) })] }));
|
|
@@ -4160,12 +4321,6 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
4160
4321
|
if (open)
|
|
4161
4322
|
ref.current?.focus();
|
|
4162
4323
|
}, [open]);
|
|
4163
|
-
// Helper to format shortcut for current platform
|
|
4164
|
-
const formatShortcut = (shortcut) => {
|
|
4165
|
-
const isMac = typeof navigator !== "undefined" &&
|
|
4166
|
-
navigator.userAgent.toLowerCase().includes("mac");
|
|
4167
|
-
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4168
|
-
};
|
|
4169
4324
|
if (!open || !clientPos || !nodeId)
|
|
4170
4325
|
return null;
|
|
4171
4326
|
// clamp
|
|
@@ -4177,7 +4332,7 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
4177
4332
|
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) => {
|
|
4178
4333
|
e.preventDefault();
|
|
4179
4334
|
e.stopPropagation();
|
|
4180
|
-
}, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.
|
|
4335
|
+
}, 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)))] }))] }));
|
|
4181
4336
|
}
|
|
4182
4337
|
|
|
4183
4338
|
function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
@@ -4210,12 +4365,6 @@ function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcu
|
|
|
4210
4365
|
if (open)
|
|
4211
4366
|
ref.current?.focus();
|
|
4212
4367
|
}, [open]);
|
|
4213
|
-
// Helper to format shortcut for current platform
|
|
4214
|
-
const formatShortcut = (shortcut) => {
|
|
4215
|
-
const isMac = typeof navigator !== "undefined" &&
|
|
4216
|
-
navigator.userAgent.toLowerCase().includes("mac");
|
|
4217
|
-
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4218
|
-
};
|
|
4219
4368
|
if (!open || !clientPos)
|
|
4220
4369
|
return null;
|
|
4221
4370
|
// Clamp menu position to viewport
|
|
@@ -4227,7 +4376,7 @@ function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcu
|
|
|
4227
4376
|
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) => {
|
|
4228
4377
|
e.preventDefault();
|
|
4229
4378
|
e.stopPropagation();
|
|
4230
|
-
}, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.
|
|
4379
|
+
}, 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 })] }));
|
|
4231
4380
|
}
|
|
4232
4381
|
|
|
4233
4382
|
const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
|
|
@@ -4235,7 +4384,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4235
4384
|
const nodeValidation = validationByNode;
|
|
4236
4385
|
const edgeValidation = validationByEdge.errors;
|
|
4237
4386
|
const [registryVersion, setRegistryVersion] = React.useState(0);
|
|
4238
|
-
|
|
4387
|
+
const [historyState, setHistoryState] = React.useState(wb.getHistory());
|
|
4239
4388
|
const prevNodesRef = React.useRef([]);
|
|
4240
4389
|
const prevEdgesRef = React.useRef([]);
|
|
4241
4390
|
function retainStabilityById(prev, next, isSame) {
|
|
@@ -4581,6 +4730,12 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4581
4730
|
});
|
|
4582
4731
|
return () => off();
|
|
4583
4732
|
}, [runner]);
|
|
4733
|
+
React.useEffect(() => {
|
|
4734
|
+
const off = wb.on("historyChanged", (event) => {
|
|
4735
|
+
setHistoryState(event.history);
|
|
4736
|
+
});
|
|
4737
|
+
return () => off();
|
|
4738
|
+
}, [wb]);
|
|
4584
4739
|
const nodeIds = React.useMemo(() => Array.from(registry.nodes.keys()), [registry, registryVersion]);
|
|
4585
4740
|
const defaultContextMenuHandlers = React.useMemo(() => {
|
|
4586
4741
|
// Get storage from override or use workbench's internal storage
|
|
@@ -4590,21 +4745,18 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4590
4745
|
get: () => wb.getCopiedData(),
|
|
4591
4746
|
set: (data) => wb.setCopiedData(data),
|
|
4592
4747
|
};
|
|
4593
|
-
const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu,
|
|
4594
|
-
// Paste handler - checks storage dynamically when called
|
|
4595
|
-
// Only provide handler if storage has data or might have data (for dynamic checking)
|
|
4596
|
-
(position) => {
|
|
4748
|
+
const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu, (position) => {
|
|
4597
4749
|
const data = storage.get();
|
|
4598
4750
|
if (!data)
|
|
4599
4751
|
return;
|
|
4600
4752
|
wb.pasteCopiedData(data, position, { commit: true, reason: "paste" });
|
|
4601
4753
|
onCloseMenu();
|
|
4602
|
-
}, runner, () => storage.get(), () => storage.set(null));
|
|
4754
|
+
}, runner, () => storage.get(), () => storage.set(null), historyState);
|
|
4603
4755
|
if (overrides?.getDefaultContextMenuHandlers) {
|
|
4604
4756
|
return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
|
|
4605
4757
|
}
|
|
4606
4758
|
return baseHandlers;
|
|
4607
|
-
}, [addNodeAt, onCloseMenu, overrides, wb, runner]);
|
|
4759
|
+
}, [addNodeAt, onCloseMenu, overrides, wb, runner, historyState]);
|
|
4608
4760
|
const selectionContextMenuHandlers = React.useMemo(() => {
|
|
4609
4761
|
// Get storage from override or use workbench's internal storage
|
|
4610
4762
|
const storage = overrides?.getCopiedDataStorage
|
|
@@ -4689,9 +4841,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4689
4841
|
e.preventDefault();
|
|
4690
4842
|
if (runner &&
|
|
4691
4843
|
"onUndo" in defaultContextMenuHandlers &&
|
|
4692
|
-
defaultContextMenuHandlers.onUndo
|
|
4693
|
-
|
|
4694
|
-
if (canUndo) {
|
|
4844
|
+
defaultContextMenuHandlers.onUndo &&
|
|
4845
|
+
defaultContextMenuHandlers.canUndo) {
|
|
4846
|
+
if (defaultContextMenuHandlers.canUndo) {
|
|
4695
4847
|
defaultContextMenuHandlers.onUndo();
|
|
4696
4848
|
}
|
|
4697
4849
|
}
|
|
@@ -4702,9 +4854,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4702
4854
|
e.preventDefault();
|
|
4703
4855
|
if (runner &&
|
|
4704
4856
|
"onRedo" in defaultContextMenuHandlers &&
|
|
4705
|
-
defaultContextMenuHandlers.onRedo
|
|
4706
|
-
|
|
4707
|
-
if (canRedo) {
|
|
4857
|
+
defaultContextMenuHandlers.onRedo &&
|
|
4858
|
+
defaultContextMenuHandlers.canRedo) {
|
|
4859
|
+
if (defaultContextMenuHandlers.canRedo) {
|
|
4708
4860
|
defaultContextMenuHandlers.onRedo();
|
|
4709
4861
|
}
|
|
4710
4862
|
}
|