@bian-womp/spark-workbench 0.2.70 → 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 +272 -128
- 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/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 +272 -128
- 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/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,27 @@ 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
|
+
}
|
|
385
|
+
getNodeRuntimeMetadata(nodeId) {
|
|
386
|
+
return this.runtimeState?.nodes[nodeId];
|
|
387
|
+
}
|
|
388
|
+
updateNodeRuntimeMetadata(nodeId, updater) {
|
|
389
|
+
const current = this.runtimeState ?? { nodes: {} };
|
|
390
|
+
const nodeMeta = current.nodes[nodeId] ?? {};
|
|
391
|
+
const updated = updater({ ...nodeMeta });
|
|
392
|
+
this.runtimeState = { nodes: { ...current.nodes, [nodeId]: updated } };
|
|
393
|
+
}
|
|
377
394
|
on(event, handler) {
|
|
378
395
|
if (!this.listeners.has(event))
|
|
379
396
|
this.listeners.set(event, new Set());
|
|
@@ -679,14 +696,10 @@ class AbstractGraphRunner {
|
|
|
679
696
|
async redo() {
|
|
680
697
|
return false;
|
|
681
698
|
}
|
|
682
|
-
async canUndo() {
|
|
683
|
-
return false;
|
|
684
|
-
}
|
|
685
|
-
async canRedo() {
|
|
686
|
-
return false;
|
|
687
|
-
}
|
|
688
699
|
// Optional commit support
|
|
689
|
-
async commit(_reason) {
|
|
700
|
+
async commit(_reason) {
|
|
701
|
+
return undefined;
|
|
702
|
+
}
|
|
690
703
|
}
|
|
691
704
|
|
|
692
705
|
// Counter for generating readable runner IDs
|
|
@@ -1436,7 +1449,8 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1436
1449
|
async commit(reason) {
|
|
1437
1450
|
const client = await this.ensureClient();
|
|
1438
1451
|
try {
|
|
1439
|
-
await client.commit(reason);
|
|
1452
|
+
const history = await client.commit(reason);
|
|
1453
|
+
return history;
|
|
1440
1454
|
}
|
|
1441
1455
|
catch (err) {
|
|
1442
1456
|
console.error("[RemoteGraphRunner] Error committing:", err);
|
|
@@ -1461,24 +1475,6 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1461
1475
|
return false;
|
|
1462
1476
|
}
|
|
1463
1477
|
}
|
|
1464
|
-
async canUndo() {
|
|
1465
|
-
const client = await this.ensureClient();
|
|
1466
|
-
try {
|
|
1467
|
-
return await client.canUndo();
|
|
1468
|
-
}
|
|
1469
|
-
catch {
|
|
1470
|
-
return false;
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
async canRedo() {
|
|
1474
|
-
const client = await this.ensureClient();
|
|
1475
|
-
try {
|
|
1476
|
-
return await client.canRedo();
|
|
1477
|
-
}
|
|
1478
|
-
catch {
|
|
1479
|
-
return false;
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
1478
|
async snapshotFull() {
|
|
1483
1479
|
const client = await this.ensureClient();
|
|
1484
1480
|
try {
|
|
@@ -1920,9 +1916,13 @@ function useWorkbenchBridge(wb) {
|
|
|
1920
1916
|
}, [wb]);
|
|
1921
1917
|
const onNodesChange = React.useCallback((changes) => {
|
|
1922
1918
|
// Apply position updates continuously, but mark commit only on drag end
|
|
1919
|
+
const positions = {};
|
|
1920
|
+
let commit = false;
|
|
1923
1921
|
changes.forEach((c) => {
|
|
1924
1922
|
if (c.type === "position" && c.position) {
|
|
1925
|
-
|
|
1923
|
+
positions[c.id] = c.position;
|
|
1924
|
+
if (!c.dragging)
|
|
1925
|
+
commit = true;
|
|
1926
1926
|
}
|
|
1927
1927
|
});
|
|
1928
1928
|
// Derive next node selection from change set
|
|
@@ -1953,10 +1953,10 @@ function useWorkbenchBridge(wb) {
|
|
|
1953
1953
|
}
|
|
1954
1954
|
}
|
|
1955
1955
|
if (selectionChanged) {
|
|
1956
|
-
wb.setSelection({
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
});
|
|
1956
|
+
wb.setSelection({ nodes: Array.from(nextNodeIds), edges: current.edges }, { commit: !(Object.keys(positions).length && commit) });
|
|
1957
|
+
}
|
|
1958
|
+
if (Object.keys(positions).length > 0) {
|
|
1959
|
+
wb.setPositions(positions, { commit });
|
|
1960
1960
|
}
|
|
1961
1961
|
}, [wb]);
|
|
1962
1962
|
const onEdgesDelete = React.useCallback((edges) => edges.forEach((e, idx) => wb.disconnect(e.id, { commit: idx === edges.length - 1 })), [wb]);
|
|
@@ -1988,10 +1988,7 @@ function useWorkbenchBridge(wb) {
|
|
|
1988
1988
|
}
|
|
1989
1989
|
}
|
|
1990
1990
|
if (selectionChanged) {
|
|
1991
|
-
wb.setSelection({
|
|
1992
|
-
nodes: current.nodes,
|
|
1993
|
-
edges: Array.from(nextEdgeIds),
|
|
1994
|
-
});
|
|
1991
|
+
wb.setSelection({ nodes: current.nodes, edges: Array.from(nextEdgeIds) }, { commit: true });
|
|
1995
1992
|
}
|
|
1996
1993
|
}, [wb]);
|
|
1997
1994
|
const onNodesDelete = React.useCallback((nodes) => {
|
|
@@ -2439,6 +2436,7 @@ async function download(wb, runner) {
|
|
|
2439
2436
|
try {
|
|
2440
2437
|
const def = wb.export();
|
|
2441
2438
|
const uiState = wb.getUIState();
|
|
2439
|
+
const runtimeState = wb.getRuntimeState();
|
|
2442
2440
|
let snapshot;
|
|
2443
2441
|
if (runner.isRunning()) {
|
|
2444
2442
|
const fullSnapshot = await runner.snapshotFull();
|
|
@@ -2448,6 +2446,7 @@ async function download(wb, runner) {
|
|
|
2448
2446
|
extData: {
|
|
2449
2447
|
...(fullSnapshot.extData || {}),
|
|
2450
2448
|
ui: uiState,
|
|
2449
|
+
runtime: runtimeState || undefined,
|
|
2451
2450
|
},
|
|
2452
2451
|
};
|
|
2453
2452
|
}
|
|
@@ -2458,7 +2457,7 @@ async function download(wb, runner) {
|
|
|
2458
2457
|
inputs,
|
|
2459
2458
|
outputs: {},
|
|
2460
2459
|
environment: {},
|
|
2461
|
-
extData: { ui: uiState },
|
|
2460
|
+
extData: { ui: uiState, runtime: runtimeState || undefined },
|
|
2462
2461
|
};
|
|
2463
2462
|
}
|
|
2464
2463
|
downloadJSON(snapshot, `spark-snapshot-${generateTimestamp()}.json`);
|
|
@@ -2484,6 +2483,9 @@ async function upload(parsed, wb, runner) {
|
|
|
2484
2483
|
if (extData.ui && typeof extData.ui === "object") {
|
|
2485
2484
|
wb.setUIState(extData.ui);
|
|
2486
2485
|
}
|
|
2486
|
+
if (extData.runtime && typeof extData.runtime === "object") {
|
|
2487
|
+
wb.setRuntimeState(extData.runtime);
|
|
2488
|
+
}
|
|
2487
2489
|
if (runner.isRunning()) {
|
|
2488
2490
|
await runner.applySnapshotFull({
|
|
2489
2491
|
def,
|
|
@@ -2511,6 +2513,18 @@ function useWorkbenchContext() {
|
|
|
2511
2513
|
return ctx;
|
|
2512
2514
|
}
|
|
2513
2515
|
|
|
2516
|
+
// Helper to compute invalidated status from runtime metadata
|
|
2517
|
+
function computeInvalidatedFromMetadata(metadata) {
|
|
2518
|
+
if (!metadata)
|
|
2519
|
+
return true;
|
|
2520
|
+
const { lastSuccessAt, lastInputAt, lastRunAt } = metadata;
|
|
2521
|
+
if (!lastSuccessAt && !lastRunAt)
|
|
2522
|
+
return true;
|
|
2523
|
+
if (!lastInputAt || Object.keys(lastInputAt).length === 0)
|
|
2524
|
+
return false;
|
|
2525
|
+
const maxInputTime = Math.max(...Object.values(lastInputAt));
|
|
2526
|
+
return maxInputTime > (lastSuccessAt ?? lastRunAt ?? 0);
|
|
2527
|
+
}
|
|
2514
2528
|
function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVersion, children, }) {
|
|
2515
2529
|
const [nodeStatus, setNodeStatus] = React.useState({});
|
|
2516
2530
|
const [edgeStatus, setEdgeStatus] = React.useState({});
|
|
@@ -2601,27 +2615,35 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2601
2615
|
}
|
|
2602
2616
|
return out;
|
|
2603
2617
|
}, [def, outputsMap, registry]);
|
|
2604
|
-
// Initialize nodes
|
|
2618
|
+
// Initialize nodes and derive invalidated status from persisted metadata
|
|
2605
2619
|
React.useEffect(() => {
|
|
2620
|
+
const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
|
|
2606
2621
|
setNodeStatus((prev) => {
|
|
2607
2622
|
const next = { ...prev };
|
|
2623
|
+
const metadata = workbenchRuntimeState;
|
|
2608
2624
|
for (const n of def.nodes) {
|
|
2609
2625
|
const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
|
|
2626
|
+
const nodeMeta = metadata.nodes[n.nodeId];
|
|
2610
2627
|
const updates = {};
|
|
2611
2628
|
if (cur.invalidated === undefined) {
|
|
2612
|
-
updates.invalidated =
|
|
2629
|
+
updates.invalidated = computeInvalidatedFromMetadata(nodeMeta);
|
|
2613
2630
|
}
|
|
2614
|
-
// Ensure activeRunIds is always initialized as an array
|
|
2615
2631
|
if (cur.activeRunIds === undefined) {
|
|
2616
2632
|
updates.activeRunIds = [];
|
|
2617
2633
|
}
|
|
2634
|
+
if (cur.activeRuns === undefined) {
|
|
2635
|
+
updates.activeRuns = 0;
|
|
2636
|
+
}
|
|
2637
|
+
if (nodeMeta?.lastErrorSummary && cur.lastError === undefined) {
|
|
2638
|
+
updates.lastError = nodeMeta.lastErrorSummary;
|
|
2639
|
+
}
|
|
2618
2640
|
if (Object.keys(updates).length > 0) {
|
|
2619
2641
|
next[n.nodeId] = { ...cur, ...updates };
|
|
2620
2642
|
}
|
|
2621
2643
|
}
|
|
2622
2644
|
return next;
|
|
2623
2645
|
});
|
|
2624
|
-
}, [def]);
|
|
2646
|
+
}, [def, wb]);
|
|
2625
2647
|
// Auto layout (simple layered layout)
|
|
2626
2648
|
const runAutoLayout = React.useCallback(() => {
|
|
2627
2649
|
const cur = wb.export();
|
|
@@ -2692,6 +2714,27 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2692
2714
|
}, [wb, registry, overrides?.getDefaultNodeSize]);
|
|
2693
2715
|
const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
|
|
2694
2716
|
const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
|
|
2717
|
+
// Helper to save runtime metadata to extData.runtime and workbench state
|
|
2718
|
+
const saveRuntimeMetadata = React.useCallback(async () => {
|
|
2719
|
+
try {
|
|
2720
|
+
const current = wb.getRuntimeState() ?? { nodes: {} };
|
|
2721
|
+
const metadata = { nodes: { ...current.nodes } };
|
|
2722
|
+
// Clean up metadata for nodes that no longer exist
|
|
2723
|
+
const nodeIds = new Set(def.nodes.map((n) => n.nodeId));
|
|
2724
|
+
for (const nodeId of Object.keys(metadata.nodes)) {
|
|
2725
|
+
if (!nodeIds.has(nodeId)) {
|
|
2726
|
+
delete metadata.nodes[nodeId];
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
// Save cleaned metadata to workbench state
|
|
2730
|
+
wb.setRuntimeState(metadata);
|
|
2731
|
+
// Save to extData.runtime via runner (no snapshotFull)
|
|
2732
|
+
await runner.setExtData?.({ runtime: metadata });
|
|
2733
|
+
}
|
|
2734
|
+
catch (err) {
|
|
2735
|
+
console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
|
|
2736
|
+
}
|
|
2737
|
+
}, [wb, def, runner]);
|
|
2695
2738
|
// Subscribe to runner/workbench events
|
|
2696
2739
|
React.useEffect(() => {
|
|
2697
2740
|
const add = (source, type) => (payload) => setEvents((prev) => {
|
|
@@ -2734,9 +2777,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2734
2777
|
wb.refreshValidation();
|
|
2735
2778
|
};
|
|
2736
2779
|
const offRunnerValue = runner.on("value", (e) => {
|
|
2780
|
+
const now = Date.now();
|
|
2737
2781
|
if (e?.io === "input") {
|
|
2738
|
-
const nodeId = e
|
|
2739
|
-
const handle = e
|
|
2782
|
+
const nodeId = e.nodeId;
|
|
2783
|
+
const handle = e.handle;
|
|
2784
|
+
// Track input timestamp in workbench runtime state
|
|
2785
|
+
wb.updateNodeRuntimeMetadata(nodeId, (nodeMeta) => ({
|
|
2786
|
+
...nodeMeta,
|
|
2787
|
+
lastInputAt: {
|
|
2788
|
+
...(nodeMeta.lastInputAt ?? {}),
|
|
2789
|
+
[handle]: now,
|
|
2790
|
+
},
|
|
2791
|
+
}));
|
|
2740
2792
|
setNodeStatus((s) => ({
|
|
2741
2793
|
...s,
|
|
2742
2794
|
[nodeId]: { ...s[nodeId], invalidated: true },
|
|
@@ -2744,6 +2796,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2744
2796
|
// Clear validation errors for this input when a valid value is set
|
|
2745
2797
|
setInputValidationErrors((prev) => prev.filter((err) => !(err.nodeId === nodeId && err.handle === handle)));
|
|
2746
2798
|
}
|
|
2799
|
+
else if (e?.io === "output") {
|
|
2800
|
+
const nodeId = e.nodeId;
|
|
2801
|
+
const handle = e.handle;
|
|
2802
|
+
// Track output timestamp in workbench runtime state
|
|
2803
|
+
wb.updateNodeRuntimeMetadata(nodeId, (nodeMeta) => ({
|
|
2804
|
+
...nodeMeta,
|
|
2805
|
+
lastOutputAt: {
|
|
2806
|
+
...(nodeMeta.lastOutputAt ?? {}),
|
|
2807
|
+
[handle]: now,
|
|
2808
|
+
},
|
|
2809
|
+
}));
|
|
2810
|
+
}
|
|
2747
2811
|
return add("runner", "value")(e);
|
|
2748
2812
|
});
|
|
2749
2813
|
const offRunnerError = runner.on("error", (e) => {
|
|
@@ -2762,6 +2826,24 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2762
2826
|
else if (nodeError.kind === "node-run" && nodeError.nodeId) {
|
|
2763
2827
|
const nodeId = nodeError.nodeId;
|
|
2764
2828
|
const runId = nodeError.runId;
|
|
2829
|
+
const now = Date.now();
|
|
2830
|
+
// Track error timestamp and summary in workbench runtime state
|
|
2831
|
+
const err = nodeError.err;
|
|
2832
|
+
let errorSummary;
|
|
2833
|
+
if (err && typeof err === "object") {
|
|
2834
|
+
const message = err.message || String(err);
|
|
2835
|
+
const code = err.code || err.statusCode;
|
|
2836
|
+
errorSummary = {
|
|
2837
|
+
message: typeof message === "string" ? message : String(message),
|
|
2838
|
+
code: typeof code === "number" ? code : undefined,
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
wb.updateNodeRuntimeMetadata(nodeId, (nodeMeta) => ({
|
|
2842
|
+
...nodeMeta,
|
|
2843
|
+
lastErrorAt: now,
|
|
2844
|
+
lastRunAt: now,
|
|
2845
|
+
...(errorSummary ? { lastErrorSummary: errorSummary } : {}),
|
|
2846
|
+
}));
|
|
2765
2847
|
setNodeStatus((s) => ({
|
|
2766
2848
|
...s,
|
|
2767
2849
|
[nodeId]: {
|
|
@@ -2816,6 +2898,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2816
2898
|
// If resolvedHandles are included in the event, use them directly (more efficient)
|
|
2817
2899
|
if (e?.resolvedHandles && Object.keys(e.resolvedHandles).length > 0) {
|
|
2818
2900
|
applyResolvedHandles(e.resolvedHandles);
|
|
2901
|
+
// Mark nodes whose handles changed as invalid
|
|
2902
|
+
const affectedNodeIds = Object.keys(e.resolvedHandles);
|
|
2903
|
+
if (affectedNodeIds.length > 0) {
|
|
2904
|
+
setNodeStatus((prev) => {
|
|
2905
|
+
const next = { ...prev };
|
|
2906
|
+
for (const id of affectedNodeIds) {
|
|
2907
|
+
const cur = next[id] ?? (next[id] = { activeRuns: 0, activeRunIds: [] });
|
|
2908
|
+
next[id] = { ...cur, invalidated: true };
|
|
2909
|
+
}
|
|
2910
|
+
return next;
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
// For broader invalidations (e.g. registry-changed, graph-updated), mark all nodes invalid
|
|
2915
|
+
if (e?.reason === "registry-changed" || e?.reason === "graph-updated") {
|
|
2916
|
+
setNodeStatus((prev) => {
|
|
2917
|
+
const next = { ...prev };
|
|
2918
|
+
for (const n of def.nodes) {
|
|
2919
|
+
const cur = next[n.nodeId] ??
|
|
2920
|
+
(next[n.nodeId] = { activeRuns: 0, activeRunIds: [] });
|
|
2921
|
+
next[n.nodeId] = { ...cur, invalidated: true };
|
|
2922
|
+
}
|
|
2923
|
+
return next;
|
|
2924
|
+
});
|
|
2819
2925
|
}
|
|
2820
2926
|
return add("runner", "invalidate")(e);
|
|
2821
2927
|
});
|
|
@@ -2825,6 +2931,12 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2825
2931
|
if (s.kind === "node-start") {
|
|
2826
2932
|
const id = s.nodeId;
|
|
2827
2933
|
const runId = s.runId;
|
|
2934
|
+
const now = Date.now();
|
|
2935
|
+
// Track run timestamp in workbench runtime state
|
|
2936
|
+
wb.updateNodeRuntimeMetadata(id, (nodeMeta) => ({
|
|
2937
|
+
...nodeMeta,
|
|
2938
|
+
lastRunAt: now,
|
|
2939
|
+
}));
|
|
2828
2940
|
// Validate runId is a non-empty string
|
|
2829
2941
|
const isValidRunId = runId && typeof runId === "string" && runId.length > 0;
|
|
2830
2942
|
if (!isValidRunId) {
|
|
@@ -2847,7 +2959,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2847
2959
|
};
|
|
2848
2960
|
});
|
|
2849
2961
|
// Start fallback animation window
|
|
2850
|
-
setFallbackStarts((prev) => ({ ...prev, [id]:
|
|
2962
|
+
setFallbackStarts((prev) => ({ ...prev, [id]: now }));
|
|
2851
2963
|
}
|
|
2852
2964
|
else if (s.kind === "node-progress") {
|
|
2853
2965
|
const id = s.nodeId;
|
|
@@ -2862,8 +2974,21 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2862
2974
|
else if (s.kind === "node-done") {
|
|
2863
2975
|
const id = s.nodeId;
|
|
2864
2976
|
const runId = s.runId;
|
|
2977
|
+
const now = Date.now();
|
|
2865
2978
|
// Validate runId is a non-empty string
|
|
2866
2979
|
const isValidRunId = runId && typeof runId === "string" && runId.length > 0;
|
|
2980
|
+
const hadError = !!(isValidRunId && errorRunsRef.current[id]?.[runId]);
|
|
2981
|
+
// Track success timestamp if no error in workbench runtime state
|
|
2982
|
+
if (!hadError) {
|
|
2983
|
+
wb.updateNodeRuntimeMetadata(id, (nodeMeta) => {
|
|
2984
|
+
const updated = { ...nodeMeta, lastSuccessAt: now };
|
|
2985
|
+
// Clear error summary on success
|
|
2986
|
+
if (updated.lastErrorSummary) {
|
|
2987
|
+
delete updated.lastErrorSummary;
|
|
2988
|
+
}
|
|
2989
|
+
return updated;
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2867
2992
|
setNodeStatus((prev) => {
|
|
2868
2993
|
const current = prev[id]?.activeRuns ?? 0;
|
|
2869
2994
|
const currentRunIds = prev[id]?.activeRunIds ?? [];
|
|
@@ -2875,7 +3000,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2875
3000
|
const nextRunIds = isValidRunId
|
|
2876
3001
|
? currentRunIds.filter((rid) => rid !== runId)
|
|
2877
3002
|
: currentRunIds;
|
|
2878
|
-
const hadError = !!(isValidRunId && errorRunsRef.current[id]?.[runId]);
|
|
2879
3003
|
const keepProgress = hadError || nextActive > 0;
|
|
2880
3004
|
// Clear error flag for this runId
|
|
2881
3005
|
if (isValidRunId && errorRunsRef.current[id]?.[runId]) {
|
|
@@ -2969,10 +3093,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2969
3093
|
}
|
|
2970
3094
|
if (!runner.isRunning()) {
|
|
2971
3095
|
if (event.commit) {
|
|
2972
|
-
|
|
2973
|
-
await runner.commit(reason).catch((err) => {
|
|
3096
|
+
await saveRuntimeMetadata();
|
|
3097
|
+
const history = await runner.commit(reason).catch((err) => {
|
|
2974
3098
|
console.error("[WorkbenchContext] Error committing:", err);
|
|
3099
|
+
return undefined;
|
|
2975
3100
|
});
|
|
3101
|
+
if (history) {
|
|
3102
|
+
wb.setHistory(history);
|
|
3103
|
+
}
|
|
2976
3104
|
}
|
|
2977
3105
|
return;
|
|
2978
3106
|
}
|
|
@@ -2999,10 +3127,16 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2999
3127
|
await runner.update(event.def, { dry: event.dry });
|
|
3000
3128
|
}
|
|
3001
3129
|
if (event.commit) {
|
|
3002
|
-
|
|
3003
|
-
|
|
3130
|
+
await saveRuntimeMetadata();
|
|
3131
|
+
const history = await runner
|
|
3132
|
+
.commit(event.reason ?? reason)
|
|
3133
|
+
.catch((err) => {
|
|
3004
3134
|
console.error("[WorkbenchContext] Error committing after update:", err);
|
|
3135
|
+
return undefined;
|
|
3005
3136
|
});
|
|
3137
|
+
if (history) {
|
|
3138
|
+
wb.setHistory(history);
|
|
3139
|
+
}
|
|
3006
3140
|
}
|
|
3007
3141
|
}
|
|
3008
3142
|
catch (err) {
|
|
@@ -3014,23 +3148,48 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3014
3148
|
setSelectedNodeId(sel.nodes?.[0]);
|
|
3015
3149
|
setSelectedEdgeId(sel.edges?.[0]);
|
|
3016
3150
|
if (sel.commit) {
|
|
3017
|
-
|
|
3018
|
-
|
|
3151
|
+
await saveRuntimeMetadata();
|
|
3152
|
+
const history = await runner
|
|
3153
|
+
.commit(sel.reason ?? "selection")
|
|
3154
|
+
.catch((err) => {
|
|
3019
3155
|
console.error("[WorkbenchContext] Error committing selection change:", err);
|
|
3156
|
+
return undefined;
|
|
3020
3157
|
});
|
|
3158
|
+
if (history) {
|
|
3159
|
+
wb.setHistory(history);
|
|
3160
|
+
}
|
|
3021
3161
|
}
|
|
3022
3162
|
});
|
|
3023
3163
|
const offWbGraphUiChanged = wb.on("graphUiChanged", async (event) => {
|
|
3024
3164
|
// Only commit if commit flag is true (e.g., drag end, not during dragging)
|
|
3025
3165
|
if (event.commit) {
|
|
3166
|
+
// Build detailed reason from change type
|
|
3167
|
+
let reason = "ui-changed";
|
|
3026
3168
|
if (event.change) {
|
|
3027
|
-
event.change.type;
|
|
3169
|
+
const changeType = event.change.type;
|
|
3170
|
+
if (changeType === "moveNode") {
|
|
3171
|
+
reason = "move-node";
|
|
3172
|
+
}
|
|
3173
|
+
else if (changeType === "moveNodes") {
|
|
3174
|
+
reason = "move-nodes";
|
|
3175
|
+
}
|
|
3176
|
+
else if (changeType === "selection") {
|
|
3177
|
+
reason = "selection";
|
|
3178
|
+
}
|
|
3179
|
+
else if (changeType === "viewport") {
|
|
3180
|
+
reason = "viewport";
|
|
3181
|
+
}
|
|
3028
3182
|
}
|
|
3029
|
-
await
|
|
3030
|
-
|
|
3183
|
+
await saveRuntimeMetadata();
|
|
3184
|
+
const history = await runner
|
|
3185
|
+
.commit(event.reason ?? reason)
|
|
3031
3186
|
.catch((err) => {
|
|
3032
3187
|
console.error("[WorkbenchContext] Error committing UI changes:", err);
|
|
3188
|
+
return undefined;
|
|
3033
3189
|
});
|
|
3190
|
+
if (history) {
|
|
3191
|
+
wb.setHistory(history);
|
|
3192
|
+
}
|
|
3034
3193
|
}
|
|
3035
3194
|
});
|
|
3036
3195
|
const offWbError = wb.on("error", add("workbench", "error"));
|
|
@@ -3051,11 +3210,24 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
3051
3210
|
console.error("Failed to handle registry changed event");
|
|
3052
3211
|
}
|
|
3053
3212
|
});
|
|
3054
|
-
// Handle transport
|
|
3213
|
+
// Handle transport changes: reset runtime status when connection is lost
|
|
3055
3214
|
const offRunnerTransport = runner.on("transport", (t) => {
|
|
3056
3215
|
if (t.state === "disconnected") {
|
|
3057
3216
|
console.info("[WorkbenchContext] Transport disconnected, resetting node status");
|
|
3058
|
-
|
|
3217
|
+
// Reinitialize node status with invalidated=true for all nodes
|
|
3218
|
+
setNodeStatus(() => {
|
|
3219
|
+
const next = {};
|
|
3220
|
+
const metadata = wb.getRuntimeState() ?? { nodes: {} };
|
|
3221
|
+
for (const n of def.nodes) {
|
|
3222
|
+
const nodeMeta = metadata.nodes[n.nodeId];
|
|
3223
|
+
next[n.nodeId] = {
|
|
3224
|
+
activeRuns: 0,
|
|
3225
|
+
activeRunIds: [],
|
|
3226
|
+
invalidated: computeInvalidatedFromMetadata(nodeMeta),
|
|
3227
|
+
};
|
|
3228
|
+
}
|
|
3229
|
+
return next;
|
|
3230
|
+
});
|
|
3059
3231
|
setEdgeStatus({});
|
|
3060
3232
|
setFallbackStarts({});
|
|
3061
3233
|
errorRunsRef.current = {};
|
|
@@ -3453,7 +3625,7 @@ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onC
|
|
|
3453
3625
|
/**
|
|
3454
3626
|
* Creates base default context menu handlers.
|
|
3455
3627
|
*/
|
|
3456
|
-
function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, getCopiedData, clearCopiedData) {
|
|
3628
|
+
function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, getCopiedData, clearCopiedData, history) {
|
|
3457
3629
|
// Wrap paste handler to clear storage after paste
|
|
3458
3630
|
const wrappedOnPaste = onPaste && getCopiedData && clearCopiedData
|
|
3459
3631
|
? (position) => {
|
|
@@ -3461,16 +3633,17 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
|
|
|
3461
3633
|
clearCopiedData();
|
|
3462
3634
|
}
|
|
3463
3635
|
: onPaste;
|
|
3464
|
-
// Function to check if paste data exists (called dynamically when menu opens)
|
|
3465
3636
|
const hasPasteData = getCopiedData ? () => !!getCopiedData() : undefined;
|
|
3637
|
+
const canUndo = history ? history.undoCount > 0 : undefined;
|
|
3638
|
+
const canRedo = history ? history.redoCount > 0 : undefined;
|
|
3466
3639
|
return {
|
|
3467
3640
|
onAddNode,
|
|
3468
3641
|
onPaste: wrappedOnPaste,
|
|
3469
3642
|
hasPasteData,
|
|
3470
3643
|
onUndo: runner ? () => runner.undo().then(() => onClose()) : undefined,
|
|
3471
3644
|
onRedo: runner ? () => runner.redo().then(() => onClose()) : undefined,
|
|
3472
|
-
canUndo
|
|
3473
|
-
canRedo
|
|
3645
|
+
canUndo,
|
|
3646
|
+
canRedo,
|
|
3474
3647
|
onClose,
|
|
3475
3648
|
};
|
|
3476
3649
|
}
|
|
@@ -3994,6 +4167,16 @@ function DefaultNodeContent({ data, isConnectable, }) {
|
|
|
3994
4167
|
} })] }));
|
|
3995
4168
|
}
|
|
3996
4169
|
|
|
4170
|
+
// Helper to format shortcut for current platform
|
|
4171
|
+
function formatShortcut(shortcut) {
|
|
4172
|
+
const isMac = typeof navigator !== "undefined" &&
|
|
4173
|
+
navigator.userAgent.toLowerCase().includes("mac");
|
|
4174
|
+
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4175
|
+
}
|
|
4176
|
+
function ContextMenuButton({ label, onClick, disabled = false, shortcut, enableKeyboardShortcuts = true, }) {
|
|
4177
|
+
return (jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-between", onClick: onClick, disabled: disabled, children: [jsxRuntime.jsx("span", { children: label }), enableKeyboardShortcuts && shortcut && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(shortcut) }))] }));
|
|
4178
|
+
}
|
|
4179
|
+
|
|
3997
4180
|
function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
3998
4181
|
undo: "⌘/Ctrl + Z",
|
|
3999
4182
|
redo: "⌘/Ctrl + Shift + Z",
|
|
@@ -4001,41 +4184,24 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
|
|
|
4001
4184
|
}, }) {
|
|
4002
4185
|
const rf = react.useReactFlow();
|
|
4003
4186
|
const [query, setQuery] = React.useState("");
|
|
4004
|
-
const [canUndo, setCanUndo] = React.useState(false);
|
|
4005
|
-
const [canRedo, setCanRedo] = React.useState(false);
|
|
4006
4187
|
const [hasPasteData, setHasPasteData] = React.useState(false);
|
|
4007
4188
|
const q = query.trim().toLowerCase();
|
|
4008
4189
|
const filteredIds = q
|
|
4009
4190
|
? nodeIds.filter((id) => id.toLowerCase().includes(q))
|
|
4010
4191
|
: nodeIds;
|
|
4011
|
-
|
|
4192
|
+
const canUndo = handlers.canUndo ?? false;
|
|
4193
|
+
const canRedo = handlers.canRedo ?? false;
|
|
4012
4194
|
React.useEffect(() => {
|
|
4013
4195
|
if (!open)
|
|
4014
4196
|
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]);
|
|
4197
|
+
if (handlers.hasPasteData) {
|
|
4198
|
+
const result = handlers.hasPasteData();
|
|
4199
|
+
setHasPasteData(result);
|
|
4200
|
+
}
|
|
4201
|
+
else {
|
|
4202
|
+
setHasPasteData(false);
|
|
4203
|
+
}
|
|
4204
|
+
}, [open, handlers.hasPasteData]);
|
|
4039
4205
|
const root = { __children: {} };
|
|
4040
4206
|
for (const id of filteredIds) {
|
|
4041
4207
|
const parts = id.split(".");
|
|
@@ -4100,12 +4266,6 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
|
|
|
4100
4266
|
handlers.onPaste(p);
|
|
4101
4267
|
handlers.onClose();
|
|
4102
4268
|
};
|
|
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
4269
|
const renderTree = (tree, path = []) => {
|
|
4110
4270
|
const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
|
|
4111
4271
|
return (jsxRuntime.jsx("div", { children: entries.map(([key, child]) => {
|
|
@@ -4123,7 +4283,7 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
|
|
|
4123
4283
|
return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
|
|
4124
4284
|
e.preventDefault();
|
|
4125
4285
|
e.stopPropagation();
|
|
4126
|
-
}, children: [hasPasteData && handlers.onPaste && (jsxRuntime.
|
|
4286
|
+
}, children: [hasPasteData && handlers.onPaste && (jsxRuntime.jsx(ContextMenuButton, { label: "Paste", onClick: handlePaste, shortcut: keyboardShortcuts.paste, enableKeyboardShortcuts: enableKeyboardShortcuts })), (handlers.onUndo || handlers.onRedo) && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [hasPasteData && handlers.onPaste && (jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" })), handlers.onUndo && (jsxRuntime.jsx(ContextMenuButton, { label: "Undo", onClick: handlers.onUndo, disabled: !canUndo, shortcut: keyboardShortcuts.undo, enableKeyboardShortcuts: enableKeyboardShortcuts })), handlers.onRedo && (jsxRuntime.jsx(ContextMenuButton, { label: "Redo", onClick: handlers.onRedo, disabled: !canRedo, shortcut: keyboardShortcuts.redo, enableKeyboardShortcuts: enableKeyboardShortcuts }))] })), hasPasteData &&
|
|
4127
4287
|
handlers.onPaste &&
|
|
4128
4288
|
!handlers.onUndo &&
|
|
4129
4289
|
!handlers.onRedo && jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxRuntime.jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsxRuntime.jsx("div", { className: "px-2 pb-1", children: jsxRuntime.jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 rounded px-2 py-1 text-sm outline-none focus:border-gray-400 select-text", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsxRuntime.jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
|
|
@@ -4160,12 +4320,6 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
4160
4320
|
if (open)
|
|
4161
4321
|
ref.current?.focus();
|
|
4162
4322
|
}, [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
4323
|
if (!open || !clientPos || !nodeId)
|
|
4170
4324
|
return null;
|
|
4171
4325
|
// clamp
|
|
@@ -4177,7 +4331,7 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
4177
4331
|
return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
|
|
4178
4332
|
e.preventDefault();
|
|
4179
4333
|
e.stopPropagation();
|
|
4180
|
-
}, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.
|
|
4334
|
+
}, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx(ContextMenuButton, { label: "Delete", onClick: handlers.onDelete, shortcut: keyboardShortcuts.delete, enableKeyboardShortcuts: enableKeyboardShortcuts }), jsxRuntime.jsx(ContextMenuButton, { label: "Duplicate", onClick: handlers.onDuplicate, shortcut: keyboardShortcuts.duplicate, enableKeyboardShortcuts: enableKeyboardShortcuts }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), canRunPull && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsx(ContextMenuButton, { label: "Copy", onClick: handlers.onCopy, shortcut: keyboardShortcuts.copy, enableKeyboardShortcuts: enableKeyboardShortcuts }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" }), bakeableOutputs.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h)))] }))] }));
|
|
4181
4335
|
}
|
|
4182
4336
|
|
|
4183
4337
|
function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
@@ -4210,12 +4364,6 @@ function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcu
|
|
|
4210
4364
|
if (open)
|
|
4211
4365
|
ref.current?.focus();
|
|
4212
4366
|
}, [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
4367
|
if (!open || !clientPos)
|
|
4220
4368
|
return null;
|
|
4221
4369
|
// Clamp menu position to viewport
|
|
@@ -4227,7 +4375,7 @@ function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcu
|
|
|
4227
4375
|
return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
|
|
4228
4376
|
e.preventDefault();
|
|
4229
4377
|
e.stopPropagation();
|
|
4230
|
-
}, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.
|
|
4378
|
+
}, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.jsx(ContextMenuButton, { label: "Copy", onClick: handlers.onCopy, shortcut: keyboardShortcuts.copy, enableKeyboardShortcuts: enableKeyboardShortcuts }), jsxRuntime.jsx(ContextMenuButton, { label: "Delete", onClick: handlers.onDelete, shortcut: keyboardShortcuts.delete, enableKeyboardShortcuts: enableKeyboardShortcuts })] }));
|
|
4231
4379
|
}
|
|
4232
4380
|
|
|
4233
4381
|
const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
|
|
@@ -4235,7 +4383,6 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4235
4383
|
const nodeValidation = validationByNode;
|
|
4236
4384
|
const edgeValidation = validationByEdge.errors;
|
|
4237
4385
|
const [registryVersion, setRegistryVersion] = React.useState(0);
|
|
4238
|
-
// Keep stable references for nodes/edges to avoid unnecessary updates
|
|
4239
4386
|
const prevNodesRef = React.useRef([]);
|
|
4240
4387
|
const prevEdgesRef = React.useRef([]);
|
|
4241
4388
|
function retainStabilityById(prev, next, isSame) {
|
|
@@ -4590,16 +4737,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4590
4737
|
get: () => wb.getCopiedData(),
|
|
4591
4738
|
set: (data) => wb.setCopiedData(data),
|
|
4592
4739
|
};
|
|
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) => {
|
|
4740
|
+
const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu, (position) => {
|
|
4597
4741
|
const data = storage.get();
|
|
4598
4742
|
if (!data)
|
|
4599
4743
|
return;
|
|
4600
4744
|
wb.pasteCopiedData(data, position, { commit: true, reason: "paste" });
|
|
4601
4745
|
onCloseMenu();
|
|
4602
|
-
}, runner, () => storage.get(), () => storage.set(null));
|
|
4746
|
+
}, runner, () => storage.get(), () => storage.set(null), wb.getHistory());
|
|
4603
4747
|
if (overrides?.getDefaultContextMenuHandlers) {
|
|
4604
4748
|
return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
|
|
4605
4749
|
}
|
|
@@ -4689,9 +4833,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4689
4833
|
e.preventDefault();
|
|
4690
4834
|
if (runner &&
|
|
4691
4835
|
"onUndo" in defaultContextMenuHandlers &&
|
|
4692
|
-
defaultContextMenuHandlers.onUndo
|
|
4693
|
-
|
|
4694
|
-
if (canUndo) {
|
|
4836
|
+
defaultContextMenuHandlers.onUndo &&
|
|
4837
|
+
defaultContextMenuHandlers.canUndo) {
|
|
4838
|
+
if (defaultContextMenuHandlers.canUndo) {
|
|
4695
4839
|
defaultContextMenuHandlers.onUndo();
|
|
4696
4840
|
}
|
|
4697
4841
|
}
|
|
@@ -4702,9 +4846,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4702
4846
|
e.preventDefault();
|
|
4703
4847
|
if (runner &&
|
|
4704
4848
|
"onRedo" in defaultContextMenuHandlers &&
|
|
4705
|
-
defaultContextMenuHandlers.onRedo
|
|
4706
|
-
|
|
4707
|
-
if (canRedo) {
|
|
4849
|
+
defaultContextMenuHandlers.onRedo &&
|
|
4850
|
+
defaultContextMenuHandlers.canRedo) {
|
|
4851
|
+
if (defaultContextMenuHandlers.canRedo) {
|
|
4708
4852
|
defaultContextMenuHandlers.onRedo();
|
|
4709
4853
|
}
|
|
4710
4854
|
}
|