@bian-womp/spark-workbench 0.2.68 → 0.2.69
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 +284 -38
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/misc/DefaultContextMenu.d.ts +1 -1
- package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +1 -1
- package/lib/cjs/src/misc/SelectionContextMenu.d.ts +1 -1
- package/lib/cjs/src/misc/SelectionContextMenu.d.ts.map +1 -1
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts +16 -0
- package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts +1 -1
- package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +10 -22
- 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/runtime/AbstractGraphRunner.d.ts +6 -1
- package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/IGraphRunner.d.ts +6 -1
- package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +6 -1
- package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/lib/esm/index.js +284 -38
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/misc/DefaultContextMenu.d.ts +1 -1
- package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +1 -1
- package/lib/esm/src/misc/SelectionContextMenu.d.ts +1 -1
- package/lib/esm/src/misc/SelectionContextMenu.d.ts.map +1 -1
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts +16 -0
- package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
- package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts +1 -1
- package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts +10 -22
- 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/runtime/AbstractGraphRunner.d.ts +6 -1
- package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/IGraphRunner.d.ts +6 -1
- package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +6 -1
- package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
- package/package.json +4 -4
package/lib/cjs/index.cjs
CHANGED
|
@@ -627,11 +627,10 @@ class AbstractGraphRunner {
|
|
|
627
627
|
this.runtime.resume();
|
|
628
628
|
// Create and launch new engine (to be implemented by subclasses)
|
|
629
629
|
await this.createAndLaunchEngine(opts);
|
|
630
|
-
// Re-apply staged inputs to new engine
|
|
630
|
+
// Re-apply staged inputs to new engine using runner's setInputs method
|
|
631
|
+
// This ensures consistency and proper handling of staged inputs
|
|
631
632
|
for (const [nodeId, map] of Object.entries(currentInputs)) {
|
|
632
|
-
|
|
633
|
-
this.engine.setInputs(nodeId, map);
|
|
634
|
-
}
|
|
633
|
+
await this.setInputs(nodeId, map);
|
|
635
634
|
}
|
|
636
635
|
}
|
|
637
636
|
getInputDefaults(def) {
|
|
@@ -673,6 +672,21 @@ class AbstractGraphRunner {
|
|
|
673
672
|
getRunningEngine() {
|
|
674
673
|
return this.runningKind;
|
|
675
674
|
}
|
|
675
|
+
// Optional undo/redo support
|
|
676
|
+
async undo() {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
async redo() {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
async canUndo() {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
async canRedo() {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
// Optional commit support
|
|
689
|
+
async commit() { }
|
|
676
690
|
}
|
|
677
691
|
|
|
678
692
|
// Counter for generating readable runner IDs
|
|
@@ -1297,8 +1311,11 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1297
1311
|
this.engine = eng;
|
|
1298
1312
|
this.runningKind = opts?.engine ?? "push";
|
|
1299
1313
|
this.emit("status", { running: true, engine: this.runningKind });
|
|
1314
|
+
// Re-apply staged inputs using client.setInputs for consistency
|
|
1300
1315
|
for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
|
|
1301
|
-
|
|
1316
|
+
await client.setInputs(nodeId, map).catch(() => {
|
|
1317
|
+
// Ignore errors during launch - inputs will be set when user calls setInputs
|
|
1318
|
+
});
|
|
1302
1319
|
}
|
|
1303
1320
|
}
|
|
1304
1321
|
/**
|
|
@@ -1340,9 +1357,11 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1340
1357
|
this.engine = eng;
|
|
1341
1358
|
this.runningKind = opts?.engine ?? "push";
|
|
1342
1359
|
this.emit("status", { running: true, engine: this.runningKind });
|
|
1343
|
-
// Re-apply staged inputs
|
|
1360
|
+
// Re-apply staged inputs using client.setInputs for consistency
|
|
1344
1361
|
for (const [nodeId, map] of Object.entries(currentInputs)) {
|
|
1345
|
-
|
|
1362
|
+
await client.setInputs(nodeId, map).catch(() => {
|
|
1363
|
+
// Ignore errors during engine switch - inputs will be set when user calls setInputs
|
|
1364
|
+
});
|
|
1346
1365
|
}
|
|
1347
1366
|
}
|
|
1348
1367
|
async step() {
|
|
@@ -1357,7 +1376,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1357
1376
|
const client = await this.ensureClient();
|
|
1358
1377
|
await client.flush();
|
|
1359
1378
|
}
|
|
1360
|
-
setInputs(nodeId, inputs, options) {
|
|
1379
|
+
async setInputs(nodeId, inputs, options) {
|
|
1361
1380
|
// Update staged inputs (for getInputs to work correctly)
|
|
1362
1381
|
if (!this.stagedInputs[nodeId])
|
|
1363
1382
|
this.stagedInputs[nodeId] = {};
|
|
@@ -1369,21 +1388,17 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1369
1388
|
this.stagedInputs[nodeId][handle] = value;
|
|
1370
1389
|
}
|
|
1371
1390
|
}
|
|
1372
|
-
//
|
|
1373
|
-
|
|
1374
|
-
|
|
1391
|
+
// Use transport.request instead of transport.send for consistency
|
|
1392
|
+
const client = await this.ensureClient();
|
|
1393
|
+
try {
|
|
1394
|
+
await client.setInputs(nodeId, inputs, options);
|
|
1375
1395
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
// Emit synthetic events if connection fails
|
|
1383
|
-
for (const [handle, value] of Object.entries(inputs)) {
|
|
1384
|
-
this.emit("value", { nodeId, handle, value, io: "input" });
|
|
1385
|
-
}
|
|
1386
|
-
});
|
|
1396
|
+
catch (err) {
|
|
1397
|
+
// Emit synthetic events if connection fails
|
|
1398
|
+
for (const [handle, value] of Object.entries(inputs)) {
|
|
1399
|
+
this.emit("value", { nodeId, handle, value, io: "input" });
|
|
1400
|
+
}
|
|
1401
|
+
throw err;
|
|
1387
1402
|
}
|
|
1388
1403
|
}
|
|
1389
1404
|
async copyOutputs(fromNodeId, toNodeId, options) {
|
|
@@ -1418,6 +1433,52 @@ class RemoteGraphRunner extends AbstractGraphRunner {
|
|
|
1418
1433
|
const client = await this.ensureClient();
|
|
1419
1434
|
await client.setExtData(data);
|
|
1420
1435
|
}
|
|
1436
|
+
async commit() {
|
|
1437
|
+
const client = await this.ensureClient();
|
|
1438
|
+
try {
|
|
1439
|
+
await client.commit();
|
|
1440
|
+
}
|
|
1441
|
+
catch (err) {
|
|
1442
|
+
console.error("[RemoteGraphRunner] Error committing:", err);
|
|
1443
|
+
throw err;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
async undo() {
|
|
1447
|
+
const client = await this.ensureClient();
|
|
1448
|
+
try {
|
|
1449
|
+
return await client.undo();
|
|
1450
|
+
}
|
|
1451
|
+
catch {
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async redo() {
|
|
1456
|
+
const client = await this.ensureClient();
|
|
1457
|
+
try {
|
|
1458
|
+
return await client.redo();
|
|
1459
|
+
}
|
|
1460
|
+
catch {
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
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
|
+
}
|
|
1421
1482
|
async snapshotFull() {
|
|
1422
1483
|
const client = await this.ensureClient();
|
|
1423
1484
|
try {
|
|
@@ -2870,7 +2931,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2870
2931
|
}
|
|
2871
2932
|
return add("workbench", "graphChanged")(event);
|
|
2872
2933
|
});
|
|
2873
|
-
const
|
|
2934
|
+
const offWbGraphUiChangedForLog = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
|
|
2874
2935
|
const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
|
|
2875
2936
|
// Ensure newly added nodes start as invalidated until first evaluation
|
|
2876
2937
|
const offWbAddNode = wb.on("graphChanged", (e) => {
|
|
@@ -2884,39 +2945,60 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2884
2945
|
}
|
|
2885
2946
|
});
|
|
2886
2947
|
const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
|
|
2887
|
-
if (!runner.isRunning())
|
|
2948
|
+
if (!runner.isRunning()) {
|
|
2949
|
+
// If runner not running, commit immediately (no update needed)
|
|
2950
|
+
await runner.commit().catch((err) => {
|
|
2951
|
+
console.error("[WorkbenchContext] Error committing:", err);
|
|
2952
|
+
});
|
|
2888
2953
|
return;
|
|
2954
|
+
}
|
|
2889
2955
|
try {
|
|
2890
2956
|
if (event.change?.type === "addNode") {
|
|
2891
2957
|
const { nodeId, inputs, copyOutputsFrom } = event.change;
|
|
2892
2958
|
if (event.dry) {
|
|
2893
2959
|
await runner.update(event.def, { dry: true });
|
|
2894
2960
|
if (inputs) {
|
|
2895
|
-
runner.setInputs(nodeId, inputs, { dry: true });
|
|
2961
|
+
await runner.setInputs(nodeId, inputs, { dry: true });
|
|
2896
2962
|
}
|
|
2897
2963
|
if (copyOutputsFrom) {
|
|
2898
|
-
runner.copyOutputs(copyOutputsFrom, nodeId, { dry: true });
|
|
2964
|
+
await runner.copyOutputs(copyOutputsFrom, nodeId, { dry: true });
|
|
2899
2965
|
}
|
|
2900
2966
|
}
|
|
2901
2967
|
else {
|
|
2902
2968
|
await runner.update(event.def, { dry: !!inputs });
|
|
2903
2969
|
if (inputs) {
|
|
2904
|
-
runner.setInputs(nodeId, inputs, { dry: false });
|
|
2970
|
+
await runner.setInputs(nodeId, inputs, { dry: false });
|
|
2905
2971
|
}
|
|
2906
2972
|
}
|
|
2907
2973
|
}
|
|
2908
2974
|
else {
|
|
2909
2975
|
await runner.update(event.def, { dry: event.dry });
|
|
2910
2976
|
}
|
|
2977
|
+
// Wait for update to complete, then commit
|
|
2978
|
+
await runner.commit().catch((err) => {
|
|
2979
|
+
console.error("[WorkbenchContext] Error committing after update:", err);
|
|
2980
|
+
});
|
|
2911
2981
|
}
|
|
2912
2982
|
catch (err) {
|
|
2913
2983
|
console.error("[WorkbenchContext] Error updating graph:", err);
|
|
2914
2984
|
}
|
|
2915
2985
|
});
|
|
2916
2986
|
const offWbdSetValidation = wb.on("validationChanged", (r) => setValidation(r));
|
|
2917
|
-
const offWbSelectionChanged = wb.on("selectionChanged", (sel) => {
|
|
2987
|
+
const offWbSelectionChanged = wb.on("selectionChanged", async (sel) => {
|
|
2918
2988
|
setSelectedNodeId(sel.nodes?.[0]);
|
|
2919
2989
|
setSelectedEdgeId(sel.edges?.[0]);
|
|
2990
|
+
// Commit on selection change
|
|
2991
|
+
await runner.commit().catch((err) => {
|
|
2992
|
+
console.error("[WorkbenchContext] Error committing selection change:", err);
|
|
2993
|
+
});
|
|
2994
|
+
});
|
|
2995
|
+
const offWbGraphUiChanged = wb.on("graphUiChanged", async (event) => {
|
|
2996
|
+
// Only commit if commit flag is true (e.g., drag end, not during dragging)
|
|
2997
|
+
if (event.commit) {
|
|
2998
|
+
await runner.commit().catch((err) => {
|
|
2999
|
+
console.error("[WorkbenchContext] Error committing UI changes:", err);
|
|
3000
|
+
});
|
|
3001
|
+
}
|
|
2920
3002
|
});
|
|
2921
3003
|
const offWbError = wb.on("error", add("workbench", "error"));
|
|
2922
3004
|
// Registry updates: swap registry and refresh graph validation/UI
|
|
@@ -2953,6 +3035,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2953
3035
|
offRunnerInvalidate();
|
|
2954
3036
|
offRunnerStats();
|
|
2955
3037
|
offWbGraphChanged();
|
|
3038
|
+
offWbGraphUiChangedForLog();
|
|
2956
3039
|
offWbGraphUiChanged();
|
|
2957
3040
|
offWbValidationChanged();
|
|
2958
3041
|
offWbError();
|
|
@@ -3340,10 +3423,24 @@ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onC
|
|
|
3340
3423
|
/**
|
|
3341
3424
|
* Creates base default context menu handlers.
|
|
3342
3425
|
*/
|
|
3343
|
-
function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste) {
|
|
3426
|
+
function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, getCopiedData, clearCopiedData) {
|
|
3427
|
+
// Wrap paste handler to clear storage after paste
|
|
3428
|
+
const wrappedOnPaste = onPaste && getCopiedData && clearCopiedData
|
|
3429
|
+
? (position) => {
|
|
3430
|
+
onPaste(position);
|
|
3431
|
+
clearCopiedData();
|
|
3432
|
+
}
|
|
3433
|
+
: onPaste;
|
|
3434
|
+
// Function to check if paste data exists (called dynamically when menu opens)
|
|
3435
|
+
const hasPasteData = getCopiedData ? () => !!getCopiedData() : undefined;
|
|
3344
3436
|
return {
|
|
3345
3437
|
onAddNode,
|
|
3346
|
-
onPaste,
|
|
3438
|
+
onPaste: wrappedOnPaste,
|
|
3439
|
+
hasPasteData,
|
|
3440
|
+
onUndo: runner ? () => runner.undo().then(() => onClose()) : undefined,
|
|
3441
|
+
onRedo: runner ? () => runner.redo().then(() => onClose()) : undefined,
|
|
3442
|
+
canUndo: runner ? () => runner.canUndo().catch(() => false) : undefined,
|
|
3443
|
+
canRedo: runner ? () => runner.canRedo().catch(() => false) : undefined,
|
|
3347
3444
|
onClose,
|
|
3348
3445
|
};
|
|
3349
3446
|
}
|
|
@@ -3867,13 +3964,48 @@ function DefaultNodeContent({ data, isConnectable, }) {
|
|
|
3867
3964
|
} })] }));
|
|
3868
3965
|
}
|
|
3869
3966
|
|
|
3870
|
-
function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds,
|
|
3967
|
+
function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
3968
|
+
undo: "⌘/Ctrl + Z",
|
|
3969
|
+
redo: "⌘/Ctrl + Shift + Z",
|
|
3970
|
+
paste: "⌘/Ctrl + V",
|
|
3971
|
+
}, }) {
|
|
3871
3972
|
const rf = react.useReactFlow();
|
|
3872
3973
|
const [query, setQuery] = React.useState("");
|
|
3974
|
+
const [canUndo, setCanUndo] = React.useState(false);
|
|
3975
|
+
const [canRedo, setCanRedo] = React.useState(false);
|
|
3976
|
+
const [hasPasteData, setHasPasteData] = React.useState(false);
|
|
3873
3977
|
const q = query.trim().toLowerCase();
|
|
3874
3978
|
const filteredIds = q
|
|
3875
3979
|
? nodeIds.filter((id) => id.toLowerCase().includes(q))
|
|
3876
3980
|
: nodeIds;
|
|
3981
|
+
// Check undo/redo availability and paste data when menu opens
|
|
3982
|
+
React.useEffect(() => {
|
|
3983
|
+
if (!open)
|
|
3984
|
+
return;
|
|
3985
|
+
let cancelled = false;
|
|
3986
|
+
const checkAvailability = async () => {
|
|
3987
|
+
if (handlers.canUndo) {
|
|
3988
|
+
const result = await handlers.canUndo();
|
|
3989
|
+
if (!cancelled)
|
|
3990
|
+
setCanUndo(result);
|
|
3991
|
+
}
|
|
3992
|
+
if (handlers.canRedo) {
|
|
3993
|
+
const result = await handlers.canRedo();
|
|
3994
|
+
if (!cancelled)
|
|
3995
|
+
setCanRedo(result);
|
|
3996
|
+
}
|
|
3997
|
+
// Check paste data dynamically
|
|
3998
|
+
if (handlers.hasPasteData) {
|
|
3999
|
+
const result = handlers.hasPasteData();
|
|
4000
|
+
if (!cancelled)
|
|
4001
|
+
setHasPasteData(result);
|
|
4002
|
+
}
|
|
4003
|
+
};
|
|
4004
|
+
checkAvailability();
|
|
4005
|
+
return () => {
|
|
4006
|
+
cancelled = true;
|
|
4007
|
+
};
|
|
4008
|
+
}, [open, handlers.canUndo, handlers.canRedo, handlers.hasPasteData]);
|
|
3877
4009
|
const root = { __children: {} };
|
|
3878
4010
|
for (const id of filteredIds) {
|
|
3879
4011
|
const parts = id.split(".");
|
|
@@ -3938,6 +4070,12 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
|
|
|
3938
4070
|
handlers.onPaste(p);
|
|
3939
4071
|
handlers.onClose();
|
|
3940
4072
|
};
|
|
4073
|
+
// Helper to format shortcut for current platform
|
|
4074
|
+
const formatShortcut = (shortcut) => {
|
|
4075
|
+
const isMac = typeof navigator !== "undefined" &&
|
|
4076
|
+
navigator.userAgent.toLowerCase().includes("mac");
|
|
4077
|
+
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4078
|
+
};
|
|
3941
4079
|
const renderTree = (tree, path = []) => {
|
|
3942
4080
|
const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
|
|
3943
4081
|
return (jsxRuntime.jsx("div", { children: entries.map(([key, child]) => {
|
|
@@ -3955,7 +4093,10 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
|
|
|
3955
4093
|
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) => {
|
|
3956
4094
|
e.preventDefault();
|
|
3957
4095
|
e.stopPropagation();
|
|
3958
|
-
}, children: [handlers.onPaste && (jsxRuntime.
|
|
4096
|
+
}, children: [hasPasteData && handlers.onPaste && (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: handlePaste, children: [jsxRuntime.jsx("span", { children: "Paste" }), enableKeyboardShortcuts && keyboardShortcuts.paste && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.paste) }))] })), (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.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: handlers.onUndo, disabled: !canUndo, children: [jsxRuntime.jsx("span", { children: "Undo" }), enableKeyboardShortcuts && keyboardShortcuts.undo && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.undo) }))] })), handlers.onRedo && (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: handlers.onRedo, disabled: !canRedo, children: [jsxRuntime.jsx("span", { children: "Redo" }), enableKeyboardShortcuts && keyboardShortcuts.redo && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.redo) }))] }))] })), hasPasteData &&
|
|
4097
|
+
handlers.onPaste &&
|
|
4098
|
+
!handlers.onUndo &&
|
|
4099
|
+
!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 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" })) })] }));
|
|
3959
4100
|
}
|
|
3960
4101
|
|
|
3961
4102
|
function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, }) {
|
|
@@ -3999,7 +4140,10 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
3999
4140
|
}, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), 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("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopy, children: "Copy" }), 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)))] }))] }));
|
|
4000
4141
|
}
|
|
4001
4142
|
|
|
4002
|
-
function SelectionContextMenu({ open, clientPos, handlers,
|
|
4143
|
+
function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcuts = true, keyboardShortcuts = {
|
|
4144
|
+
copy: "⌘/Ctrl + C",
|
|
4145
|
+
delete: "Delete",
|
|
4146
|
+
}, }) {
|
|
4003
4147
|
const ref = React.useRef(null);
|
|
4004
4148
|
// Close on outside click and on ESC
|
|
4005
4149
|
React.useEffect(() => {
|
|
@@ -4026,6 +4170,12 @@ function SelectionContextMenu({ open, clientPos, handlers, }) {
|
|
|
4026
4170
|
if (open)
|
|
4027
4171
|
ref.current?.focus();
|
|
4028
4172
|
}, [open]);
|
|
4173
|
+
// Helper to format shortcut for current platform
|
|
4174
|
+
const formatShortcut = (shortcut) => {
|
|
4175
|
+
const isMac = typeof navigator !== "undefined" &&
|
|
4176
|
+
navigator.userAgent.toLowerCase().includes("mac");
|
|
4177
|
+
return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
|
|
4178
|
+
};
|
|
4029
4179
|
if (!open || !clientPos)
|
|
4030
4180
|
return null;
|
|
4031
4181
|
// Clamp menu position to viewport
|
|
@@ -4037,7 +4187,7 @@ function SelectionContextMenu({ open, clientPos, handlers, }) {
|
|
|
4037
4187
|
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) => {
|
|
4038
4188
|
e.preventDefault();
|
|
4039
4189
|
e.stopPropagation();
|
|
4040
|
-
}, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.
|
|
4190
|
+
}, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 flex items-center justify-between", onClick: handlers.onCopy, children: [jsxRuntime.jsx("span", { children: "Copy" }), enableKeyboardShortcuts && keyboardShortcuts.copy && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.copy) }))] }), jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 flex items-center justify-between", onClick: handlers.onDelete, children: [jsxRuntime.jsx("span", { children: "Delete" }), enableKeyboardShortcuts && keyboardShortcuts.delete && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.delete) }))] })] }));
|
|
4041
4191
|
}
|
|
4042
4192
|
|
|
4043
4193
|
const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
|
|
@@ -4409,12 +4559,12 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4409
4559
|
return;
|
|
4410
4560
|
wb.pasteCopiedData(data, position);
|
|
4411
4561
|
onCloseMenu();
|
|
4412
|
-
});
|
|
4562
|
+
}, runner, () => storage.get(), () => storage.set(null));
|
|
4413
4563
|
if (overrides?.getDefaultContextMenuHandlers) {
|
|
4414
4564
|
return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
|
|
4415
4565
|
}
|
|
4416
4566
|
return baseHandlers;
|
|
4417
|
-
}, [addNodeAt, onCloseMenu, overrides, wb]);
|
|
4567
|
+
}, [addNodeAt, onCloseMenu, overrides, wb, runner]);
|
|
4418
4568
|
const selectionContextMenuHandlers = React.useMemo(() => {
|
|
4419
4569
|
// Get storage from override or use workbench's internal storage
|
|
4420
4570
|
const storage = overrides?.getCopiedDataStorage
|
|
@@ -4469,6 +4619,100 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4469
4619
|
return [];
|
|
4470
4620
|
return getBakeableOutputs(nodeAtMenu, wb, registry, outputTypesMap);
|
|
4471
4621
|
}, [nodeAtMenu, wb, registry, outputTypesMap]);
|
|
4622
|
+
// Keyboard shortcuts configuration
|
|
4623
|
+
const enableKeyboardShortcuts = overrides?.enableKeyboardShortcuts !== false; // Default to true
|
|
4624
|
+
const keyboardShortcuts = overrides?.keyboardShortcuts || {
|
|
4625
|
+
undo: "⌘/Ctrl + Z",
|
|
4626
|
+
redo: "⌘/Ctrl + Shift + Z",
|
|
4627
|
+
copy: "⌘/Ctrl + C",
|
|
4628
|
+
paste: "⌘/Ctrl + V",
|
|
4629
|
+
delete: "Delete",
|
|
4630
|
+
};
|
|
4631
|
+
// Keyboard shortcut handler
|
|
4632
|
+
React.useEffect(() => {
|
|
4633
|
+
if (!enableKeyboardShortcuts)
|
|
4634
|
+
return;
|
|
4635
|
+
const handleKeyDown = async (e) => {
|
|
4636
|
+
// Ignore if typing in input/textarea
|
|
4637
|
+
const target = e.target;
|
|
4638
|
+
if (target.tagName === "INPUT" ||
|
|
4639
|
+
target.tagName === "TEXTAREA" ||
|
|
4640
|
+
target.isContentEditable) {
|
|
4641
|
+
return;
|
|
4642
|
+
}
|
|
4643
|
+
// Detect Mac platform using userAgent (navigator.platform is deprecated)
|
|
4644
|
+
const isMac = typeof navigator !== "undefined" &&
|
|
4645
|
+
navigator.userAgent.toLowerCase().includes("mac");
|
|
4646
|
+
const modKey = isMac ? e.metaKey : e.ctrlKey;
|
|
4647
|
+
const key = e.key.toLowerCase();
|
|
4648
|
+
// Undo: Cmd/Ctrl + Z
|
|
4649
|
+
if (modKey && key === "z" && !e.shiftKey && !e.altKey) {
|
|
4650
|
+
e.preventDefault();
|
|
4651
|
+
if (runner &&
|
|
4652
|
+
"onUndo" in defaultContextMenuHandlers &&
|
|
4653
|
+
defaultContextMenuHandlers.onUndo) {
|
|
4654
|
+
const canUndo = await runner.canUndo().catch(() => false);
|
|
4655
|
+
if (canUndo) {
|
|
4656
|
+
defaultContextMenuHandlers.onUndo();
|
|
4657
|
+
}
|
|
4658
|
+
}
|
|
4659
|
+
return;
|
|
4660
|
+
}
|
|
4661
|
+
// Redo: Cmd/Ctrl + Shift + Z
|
|
4662
|
+
if (modKey && e.shiftKey && key === "z" && !e.altKey) {
|
|
4663
|
+
e.preventDefault();
|
|
4664
|
+
if (runner &&
|
|
4665
|
+
"onRedo" in defaultContextMenuHandlers &&
|
|
4666
|
+
defaultContextMenuHandlers.onRedo) {
|
|
4667
|
+
const canRedo = await runner.canRedo().catch(() => false);
|
|
4668
|
+
if (canRedo) {
|
|
4669
|
+
defaultContextMenuHandlers.onRedo();
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
return;
|
|
4673
|
+
}
|
|
4674
|
+
// Copy: Cmd/Ctrl + C
|
|
4675
|
+
if (modKey && key === "c" && !e.shiftKey && !e.altKey) {
|
|
4676
|
+
const selection = wb.getSelection();
|
|
4677
|
+
if (selection.nodes.length > 0 || selection.edges.length > 0) {
|
|
4678
|
+
e.preventDefault();
|
|
4679
|
+
if (selectionContextMenuHandlers.onCopy) {
|
|
4680
|
+
selectionContextMenuHandlers.onCopy();
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
return;
|
|
4684
|
+
}
|
|
4685
|
+
// Paste: Cmd/Ctrl + V
|
|
4686
|
+
if (modKey && key === "v" && !e.shiftKey && !e.altKey) {
|
|
4687
|
+
e.preventDefault();
|
|
4688
|
+
if ("hasPasteData" in defaultContextMenuHandlers &&
|
|
4689
|
+
defaultContextMenuHandlers.hasPasteData &&
|
|
4690
|
+
defaultContextMenuHandlers.hasPasteData() &&
|
|
4691
|
+
"onPaste" in defaultContextMenuHandlers &&
|
|
4692
|
+
defaultContextMenuHandlers.onPaste) {
|
|
4693
|
+
const center = rfInstanceRef.current?.screenToFlowPosition({
|
|
4694
|
+
x: window.innerWidth / 2,
|
|
4695
|
+
y: window.innerHeight / 2,
|
|
4696
|
+
}) || { x: 0, y: 0 };
|
|
4697
|
+
defaultContextMenuHandlers.onPaste(center);
|
|
4698
|
+
}
|
|
4699
|
+
return;
|
|
4700
|
+
}
|
|
4701
|
+
// Note: Delete/Backspace is handled by ReactFlow's deleteKeyCode prop
|
|
4702
|
+
// which triggers onNodesDelete/onEdgesDelete, so we don't need to handle it here
|
|
4703
|
+
};
|
|
4704
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
4705
|
+
return () => {
|
|
4706
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
4707
|
+
};
|
|
4708
|
+
}, [
|
|
4709
|
+
enableKeyboardShortcuts,
|
|
4710
|
+
wb,
|
|
4711
|
+
runner,
|
|
4712
|
+
defaultContextMenuHandlers,
|
|
4713
|
+
selectionContextMenuHandlers,
|
|
4714
|
+
rfInstanceRef,
|
|
4715
|
+
]);
|
|
4472
4716
|
// Get custom renderers from UI extension registry (reactive to uiVersion changes)
|
|
4473
4717
|
const { BackgroundRenderer, MinimapRenderer, ControlsRenderer, DefaultContextMenuRenderer, NodeContextMenuRenderer, connectionLineRenderer, } = React.useMemo(() => {
|
|
4474
4718
|
return {
|
|
@@ -4515,9 +4759,11 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4515
4759
|
zoom: savedViewport.zoom,
|
|
4516
4760
|
});
|
|
4517
4761
|
}
|
|
4518
|
-
}, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onMoveEnd: onMoveEnd, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", fitView: true, children: [BackgroundRenderer ? (jsxRuntime.jsx(BackgroundRenderer, {})) : (jsxRuntime.jsx(react.Background, { id: "workbench-canvas-background", variant: react.BackgroundVariant.Dots, gap: 12, size: 1 })), MinimapRenderer ? jsxRuntime.jsx(MinimapRenderer, {}) : jsxRuntime.jsx(react.MiniMap, {}), ControlsRenderer ? jsxRuntime.jsx(ControlsRenderer, {}) : jsxRuntime.jsx(react.Controls, {}), DefaultContextMenuRenderer ? (jsxRuntime.jsx(DefaultContextMenuRenderer, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds
|
|
4762
|
+
}, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onMoveEnd: onMoveEnd, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", fitView: true, children: [BackgroundRenderer ? (jsxRuntime.jsx(BackgroundRenderer, {})) : (jsxRuntime.jsx(react.Background, { id: "workbench-canvas-background", variant: react.BackgroundVariant.Dots, gap: 12, size: 1 })), MinimapRenderer ? jsxRuntime.jsx(MinimapRenderer, {}) : jsxRuntime.jsx(react.MiniMap, {}), ControlsRenderer ? jsxRuntime.jsx(ControlsRenderer, {}) : jsxRuntime.jsx(react.Controls, {}), DefaultContextMenuRenderer ? (jsxRuntime.jsx(DefaultContextMenuRenderer, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds, ...(enableKeyboardShortcuts !== false
|
|
4763
|
+
? { enableKeyboardShortcuts, keyboardShortcuts }
|
|
4764
|
+
: {}) })) : (jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts })), !!nodeAtMenu &&
|
|
4519
4765
|
nodeContextMenuHandlers &&
|
|
4520
|
-
(NodeContextMenuRenderer ? (jsxRuntime.jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })) : (jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs }))), selectionMenuOpen && selectionMenuPos && (jsxRuntime.jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers }))] }) }) }));
|
|
4766
|
+
(NodeContextMenuRenderer ? (jsxRuntime.jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })) : (jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs }))), selectionMenuOpen && selectionMenuPos && (jsxRuntime.jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))] }) }) }));
|
|
4521
4767
|
});
|
|
4522
4768
|
|
|
4523
4769
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
|