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