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