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