@bian-womp/spark-workbench 0.2.64 → 0.2.66
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 +589 -205
- package/lib/cjs/index.cjs.map +1 -1
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts +54 -1
- package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/cjs/src/index.d.ts +1 -0
- package/lib/cjs/src/index.d.ts.map +1 -1
- package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +1 -1
- package/lib/cjs/src/misc/NodeContextMenu.d.ts.map +1 -1
- package/lib/cjs/src/misc/SelectionContextMenu.d.ts +3 -0
- package/lib/cjs/src/misc/SelectionContextMenu.d.ts.map +1 -0
- package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts +18 -0
- package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts +39 -2
- package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
- package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +41 -0
- 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/esm/index.js +584 -206
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/src/core/InMemoryWorkbench.d.ts +54 -1
- package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
- package/lib/esm/src/index.d.ts +1 -0
- package/lib/esm/src/index.d.ts.map +1 -1
- package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +1 -1
- package/lib/esm/src/misc/NodeContextMenu.d.ts.map +1 -1
- package/lib/esm/src/misc/SelectionContextMenu.d.ts +3 -0
- package/lib/esm/src/misc/SelectionContextMenu.d.ts.map +1 -0
- package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
- package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts +18 -0
- package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
- package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts +39 -2
- package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
- package/lib/esm/src/misc/context/WorkbenchContext.d.ts +41 -0
- 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/package.json +4 -4
package/lib/cjs/index.cjs
CHANGED
|
@@ -307,6 +307,22 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
307
307
|
edges: [...this.selection.edges],
|
|
308
308
|
};
|
|
309
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Delete all selected nodes and edges.
|
|
312
|
+
*/
|
|
313
|
+
deleteSelection() {
|
|
314
|
+
const selection = this.getSelection();
|
|
315
|
+
// Delete all selected nodes (this will also remove connected edges)
|
|
316
|
+
for (const nodeId of selection.nodes) {
|
|
317
|
+
this.removeNode(nodeId);
|
|
318
|
+
}
|
|
319
|
+
// Delete remaining selected edges (edges not connected to deleted nodes)
|
|
320
|
+
for (const edgeId of selection.edges) {
|
|
321
|
+
this.disconnect(edgeId);
|
|
322
|
+
}
|
|
323
|
+
// Clear selection
|
|
324
|
+
this.setSelection({ nodes: [], edges: [] });
|
|
325
|
+
}
|
|
310
326
|
setViewport(viewport, opts) {
|
|
311
327
|
this.viewport = { ...viewport };
|
|
312
328
|
this.emit("graphUiChanged", {
|
|
@@ -367,6 +383,142 @@ class InMemoryWorkbench extends AbstractWorkbench {
|
|
|
367
383
|
for (const h of Array.from(set))
|
|
368
384
|
h(payload);
|
|
369
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Copy selected nodes and their internal edges.
|
|
388
|
+
* Returns data in a format suitable for pasting.
|
|
389
|
+
* Positions are normalized relative to the selection bounds center.
|
|
390
|
+
* Uses the same logic as duplicate: copies inputs without bindings and supports copyOutputsFrom.
|
|
391
|
+
*/
|
|
392
|
+
copySelection(runner, getNodeSize) {
|
|
393
|
+
const selection = this.getSelection();
|
|
394
|
+
if (selection.nodes.length === 0)
|
|
395
|
+
return null;
|
|
396
|
+
const def = this.export();
|
|
397
|
+
const positions = this.getPositions();
|
|
398
|
+
const selectedNodeSet = new Set(selection.nodes);
|
|
399
|
+
// Collect nodes to copy
|
|
400
|
+
const nodesToCopy = def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
|
|
401
|
+
if (nodesToCopy.length === 0)
|
|
402
|
+
return null;
|
|
403
|
+
// Calculate bounds
|
|
404
|
+
let minX = Infinity;
|
|
405
|
+
let minY = Infinity;
|
|
406
|
+
let maxX = -Infinity;
|
|
407
|
+
let maxY = -Infinity;
|
|
408
|
+
nodesToCopy.forEach((node) => {
|
|
409
|
+
const pos = positions[node.nodeId] || { x: 0, y: 0 };
|
|
410
|
+
const size = getNodeSize?.(node.nodeId, node.typeId) || {
|
|
411
|
+
width: 200,
|
|
412
|
+
height: 100,
|
|
413
|
+
};
|
|
414
|
+
minX = Math.min(minX, pos.x);
|
|
415
|
+
minY = Math.min(minY, pos.y);
|
|
416
|
+
maxX = Math.max(maxX, pos.x + size.width);
|
|
417
|
+
maxY = Math.max(maxY, pos.y + size.height);
|
|
418
|
+
});
|
|
419
|
+
const bounds = { minX, minY, maxX, maxY };
|
|
420
|
+
const centerX = (bounds.minX + bounds.maxX) / 2;
|
|
421
|
+
const centerY = (bounds.minY + bounds.maxY) / 2;
|
|
422
|
+
// Get inputs for each node
|
|
423
|
+
// Include values from inbound edges if those edges are selected
|
|
424
|
+
const allInputs = runner.getInputs(def);
|
|
425
|
+
const selectedEdgeSet = new Set(selection.edges);
|
|
426
|
+
const copiedNodes = nodesToCopy.map((node) => {
|
|
427
|
+
const pos = positions[node.nodeId] || { x: 0, y: 0 };
|
|
428
|
+
// Get all inbound edges for this node
|
|
429
|
+
const inboundEdges = def.edges.filter((e) => e.target.nodeId === node.nodeId);
|
|
430
|
+
// Build set of handles that have inbound edges
|
|
431
|
+
// But only exclude handles whose edges are NOT selected
|
|
432
|
+
const inboundHandlesToExclude = new Set(inboundEdges
|
|
433
|
+
.filter((e) => !selectedEdgeSet.has(e.id)) // Only exclude if edge is not selected
|
|
434
|
+
.map((e) => e.target.handle));
|
|
435
|
+
const allNodeInputs = allInputs[node.nodeId] || {};
|
|
436
|
+
// Include inputs that either:
|
|
437
|
+
// 1. Don't have inbound edges (literal values)
|
|
438
|
+
// 2. Have inbound edges that ARE selected (preserve the value from the edge)
|
|
439
|
+
const inputsToCopy = Object.fromEntries(Object.entries(allNodeInputs).filter(([handle]) => !inboundHandlesToExclude.has(handle)));
|
|
440
|
+
return {
|
|
441
|
+
typeId: node.typeId,
|
|
442
|
+
params: node.params,
|
|
443
|
+
resolvedHandles: node.resolvedHandles,
|
|
444
|
+
position: {
|
|
445
|
+
x: pos.x - centerX,
|
|
446
|
+
y: pos.y - centerY,
|
|
447
|
+
},
|
|
448
|
+
inputs: inputsToCopy,
|
|
449
|
+
originalNodeId: node.nodeId,
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
// Collect edges between copied nodes
|
|
453
|
+
const copiedEdges = def.edges
|
|
454
|
+
.filter((edge) => {
|
|
455
|
+
return (selectedNodeSet.has(edge.source.nodeId) &&
|
|
456
|
+
selectedNodeSet.has(edge.target.nodeId));
|
|
457
|
+
})
|
|
458
|
+
.map((edge) => ({
|
|
459
|
+
sourceNodeId: edge.source.nodeId,
|
|
460
|
+
sourceHandle: edge.source.handle,
|
|
461
|
+
targetNodeId: edge.target.nodeId,
|
|
462
|
+
targetHandle: edge.target.handle,
|
|
463
|
+
typeId: edge.typeId,
|
|
464
|
+
}));
|
|
465
|
+
return {
|
|
466
|
+
nodes: copiedNodes,
|
|
467
|
+
edges: copiedEdges,
|
|
468
|
+
bounds,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Paste copied graph data at the specified center position.
|
|
473
|
+
* Returns the mapping from original node IDs to new node IDs.
|
|
474
|
+
* Uses copyOutputsFrom to copy outputs from original nodes (like duplicate does).
|
|
475
|
+
*/
|
|
476
|
+
pasteCopiedData(data, center) {
|
|
477
|
+
const nodeIdMap = new Map();
|
|
478
|
+
const edgeIds = [];
|
|
479
|
+
// Add nodes
|
|
480
|
+
for (const nodeData of data.nodes) {
|
|
481
|
+
const newNodeId = this.addNode({
|
|
482
|
+
typeId: nodeData.typeId,
|
|
483
|
+
params: nodeData.params,
|
|
484
|
+
resolvedHandles: nodeData.resolvedHandles,
|
|
485
|
+
position: {
|
|
486
|
+
x: nodeData.position.x + center.x,
|
|
487
|
+
y: nodeData.position.y + center.y,
|
|
488
|
+
},
|
|
489
|
+
}, {
|
|
490
|
+
inputs: nodeData.inputs,
|
|
491
|
+
copyOutputsFrom: nodeData.originalNodeId,
|
|
492
|
+
dry: true,
|
|
493
|
+
});
|
|
494
|
+
nodeIdMap.set(nodeData.originalNodeId, newNodeId);
|
|
495
|
+
}
|
|
496
|
+
// Add edges
|
|
497
|
+
for (const edgeData of data.edges) {
|
|
498
|
+
const newSourceNodeId = nodeIdMap.get(edgeData.sourceNodeId);
|
|
499
|
+
const newTargetNodeId = nodeIdMap.get(edgeData.targetNodeId);
|
|
500
|
+
if (newSourceNodeId && newTargetNodeId) {
|
|
501
|
+
const edgeId = this.connect({
|
|
502
|
+
source: {
|
|
503
|
+
nodeId: newSourceNodeId,
|
|
504
|
+
handle: edgeData.sourceHandle,
|
|
505
|
+
},
|
|
506
|
+
target: {
|
|
507
|
+
nodeId: newTargetNodeId,
|
|
508
|
+
handle: edgeData.targetHandle,
|
|
509
|
+
},
|
|
510
|
+
typeId: edgeData.typeId,
|
|
511
|
+
}, { dry: true });
|
|
512
|
+
edgeIds.push(edgeId);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// Select the newly pasted nodes
|
|
516
|
+
this.setSelection({
|
|
517
|
+
nodes: Array.from(nodeIdMap.values()),
|
|
518
|
+
edges: edgeIds,
|
|
519
|
+
});
|
|
520
|
+
return { nodeIdMap, edgeIds };
|
|
521
|
+
}
|
|
370
522
|
}
|
|
371
523
|
|
|
372
524
|
class CLIWorkbench {
|
|
@@ -2913,6 +3065,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2913
3065
|
updateEdgeType,
|
|
2914
3066
|
triggerExternal,
|
|
2915
3067
|
uiVersion,
|
|
3068
|
+
overrides,
|
|
2916
3069
|
}), [
|
|
2917
3070
|
wb,
|
|
2918
3071
|
runner,
|
|
@@ -2952,10 +3105,272 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
|
|
|
2952
3105
|
wb,
|
|
2953
3106
|
runner,
|
|
2954
3107
|
uiVersion,
|
|
3108
|
+
overrides,
|
|
2955
3109
|
]);
|
|
2956
3110
|
return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
|
|
2957
3111
|
}
|
|
2958
3112
|
|
|
3113
|
+
function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose, getDefaultNodeSize, onCopyResult) {
|
|
3114
|
+
const doBake = async (handleId) => {
|
|
3115
|
+
try {
|
|
3116
|
+
const typeId = outputTypesMap?.[nodeId]?.[handleId];
|
|
3117
|
+
const raw = outputsMap?.[nodeId]?.[handleId];
|
|
3118
|
+
if (!typeId || raw === undefined)
|
|
3119
|
+
return;
|
|
3120
|
+
const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
|
|
3121
|
+
const coerceIfNeeded = async (fromType, toType, value) => {
|
|
3122
|
+
if (!toType || toType === fromType || !runner?.coerce)
|
|
3123
|
+
return value;
|
|
3124
|
+
try {
|
|
3125
|
+
return await runner.coerce(fromType, toType, value);
|
|
3126
|
+
}
|
|
3127
|
+
catch {
|
|
3128
|
+
return value;
|
|
3129
|
+
}
|
|
3130
|
+
};
|
|
3131
|
+
const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3132
|
+
const isArray = typeId.endsWith("[]");
|
|
3133
|
+
const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
|
|
3134
|
+
const tArr = isArray ? registry.types.get(typeId) : undefined;
|
|
3135
|
+
const tElem = registry.types.get(baseTypeId);
|
|
3136
|
+
const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
|
|
3137
|
+
const arrTarget = isArray ? tArr?.bakeTarget : undefined;
|
|
3138
|
+
const elemTarget = isArray ? tElem?.bakeTarget : undefined;
|
|
3139
|
+
if (singleTarget) {
|
|
3140
|
+
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
3141
|
+
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
3142
|
+
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3143
|
+
wb.addNode({
|
|
3144
|
+
typeId: singleTarget.nodeTypeId,
|
|
3145
|
+
position: { x: pos.x + 180, y: pos.y },
|
|
3146
|
+
}, { inputs: { [singleTarget.inputHandle]: coerced } });
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
if (isArray && arrTarget) {
|
|
3150
|
+
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
3151
|
+
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
3152
|
+
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3153
|
+
wb.addNode({
|
|
3154
|
+
typeId: arrTarget.nodeTypeId,
|
|
3155
|
+
position: { x: pos.x + 180, y: pos.y },
|
|
3156
|
+
}, { inputs: { [arrTarget.inputHandle]: coerced } });
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
if (isArray && elemTarget) {
|
|
3160
|
+
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
3161
|
+
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
3162
|
+
const src = unwrap(raw);
|
|
3163
|
+
const items = Array.isArray(src) ? src : [src];
|
|
3164
|
+
const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
|
|
3165
|
+
const COLS = 4;
|
|
3166
|
+
const DX = 180;
|
|
3167
|
+
const DY = 160;
|
|
3168
|
+
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
3169
|
+
const col = idx % COLS;
|
|
3170
|
+
const row = Math.floor(idx / COLS);
|
|
3171
|
+
wb.addNode({
|
|
3172
|
+
typeId: elemTarget.nodeTypeId,
|
|
3173
|
+
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
3174
|
+
}, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
|
|
3175
|
+
}
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
catch { }
|
|
3180
|
+
};
|
|
3181
|
+
return {
|
|
3182
|
+
onDelete: () => {
|
|
3183
|
+
wb.removeNode(nodeId);
|
|
3184
|
+
onClose();
|
|
3185
|
+
},
|
|
3186
|
+
onDuplicate: async () => {
|
|
3187
|
+
const def = wb.export();
|
|
3188
|
+
const n = def.nodes.find((n) => n.nodeId === nodeId);
|
|
3189
|
+
if (!n)
|
|
3190
|
+
return onClose();
|
|
3191
|
+
const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3192
|
+
const inboundHandles = new Set(def.edges
|
|
3193
|
+
.filter((e) => e.target.nodeId === nodeId)
|
|
3194
|
+
.map((e) => e.target.handle));
|
|
3195
|
+
const allInputs = runner.getInputs(def)[nodeId] || {};
|
|
3196
|
+
const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
|
|
3197
|
+
const newNodeId = wb.addNode({
|
|
3198
|
+
typeId: n.typeId,
|
|
3199
|
+
params: n.params,
|
|
3200
|
+
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
3201
|
+
resolvedHandles: n.resolvedHandles,
|
|
3202
|
+
}, {
|
|
3203
|
+
inputs: inputsWithoutBindings,
|
|
3204
|
+
copyOutputsFrom: nodeId,
|
|
3205
|
+
dry: true,
|
|
3206
|
+
});
|
|
3207
|
+
// Select the newly duplicated node
|
|
3208
|
+
wb.setSelection({
|
|
3209
|
+
nodes: [newNodeId],
|
|
3210
|
+
edges: [],
|
|
3211
|
+
});
|
|
3212
|
+
onClose();
|
|
3213
|
+
},
|
|
3214
|
+
onDuplicateWithEdges: async () => {
|
|
3215
|
+
const def = wb.export();
|
|
3216
|
+
const n = def.nodes.find((n) => n.nodeId === nodeId);
|
|
3217
|
+
if (!n)
|
|
3218
|
+
return onClose();
|
|
3219
|
+
const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
|
|
3220
|
+
// Get inputs without bindings (literal values only)
|
|
3221
|
+
const inputs = runner.getInputs(def)[nodeId] || {};
|
|
3222
|
+
// Add the duplicated node
|
|
3223
|
+
const newNodeId = wb.addNode({
|
|
3224
|
+
typeId: n.typeId,
|
|
3225
|
+
params: n.params,
|
|
3226
|
+
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
3227
|
+
resolvedHandles: n.resolvedHandles,
|
|
3228
|
+
}, {
|
|
3229
|
+
inputs,
|
|
3230
|
+
copyOutputsFrom: nodeId,
|
|
3231
|
+
dry: true,
|
|
3232
|
+
});
|
|
3233
|
+
// Find all incoming edges (edges where target is the original node)
|
|
3234
|
+
const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
3235
|
+
// Duplicate each incoming edge to point to the new node
|
|
3236
|
+
for (const edge of incomingEdges) {
|
|
3237
|
+
wb.connect({
|
|
3238
|
+
source: edge.source, // Keep the same source
|
|
3239
|
+
target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
|
|
3240
|
+
typeId: edge.typeId,
|
|
3241
|
+
}, { dry: true });
|
|
3242
|
+
}
|
|
3243
|
+
// Select the newly duplicated node and edges
|
|
3244
|
+
wb.setSelection({
|
|
3245
|
+
nodes: [newNodeId],
|
|
3246
|
+
edges: [],
|
|
3247
|
+
});
|
|
3248
|
+
onClose();
|
|
3249
|
+
},
|
|
3250
|
+
onRunPull: async () => {
|
|
3251
|
+
try {
|
|
3252
|
+
await runner.computeNode(nodeId);
|
|
3253
|
+
}
|
|
3254
|
+
catch { }
|
|
3255
|
+
onClose();
|
|
3256
|
+
},
|
|
3257
|
+
onBake: async (handleId) => {
|
|
3258
|
+
await doBake(handleId);
|
|
3259
|
+
onClose();
|
|
3260
|
+
},
|
|
3261
|
+
onCopy: () => {
|
|
3262
|
+
const copyHandler = createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyResult);
|
|
3263
|
+
copyHandler();
|
|
3264
|
+
onClose();
|
|
3265
|
+
},
|
|
3266
|
+
onCopyId: async () => {
|
|
3267
|
+
try {
|
|
3268
|
+
await navigator.clipboard.writeText(nodeId);
|
|
3269
|
+
}
|
|
3270
|
+
catch { }
|
|
3271
|
+
onClose();
|
|
3272
|
+
},
|
|
3273
|
+
onClose,
|
|
3274
|
+
};
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* Creates a copy handler that copies the current selection and optionally stores the result.
|
|
3278
|
+
*/
|
|
3279
|
+
function createCopyHandler(wb, runner, getDefaultNodeSize, onCopyResult) {
|
|
3280
|
+
return () => {
|
|
3281
|
+
const getNodeSize = getDefaultNodeSize
|
|
3282
|
+
? (nodeId, typeId) => getDefaultNodeSize(typeId)
|
|
3283
|
+
: undefined;
|
|
3284
|
+
const data = wb.copySelection(runner, getNodeSize);
|
|
3285
|
+
if (onCopyResult) {
|
|
3286
|
+
onCopyResult(data);
|
|
3287
|
+
}
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
/**
|
|
3291
|
+
* Creates a copy handler for a single node (temporarily selects it, copies, then restores selection).
|
|
3292
|
+
*/
|
|
3293
|
+
function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyResult) {
|
|
3294
|
+
return () => {
|
|
3295
|
+
// Select the node first, then copy
|
|
3296
|
+
const currentSelection = wb.getSelection();
|
|
3297
|
+
wb.setSelection({ nodes: [nodeId], edges: [] });
|
|
3298
|
+
const getNodeSize = getDefaultNodeSize
|
|
3299
|
+
? (nodeId, typeId) => getDefaultNodeSize(typeId)
|
|
3300
|
+
: undefined;
|
|
3301
|
+
const data = wb.copySelection(runner, getNodeSize);
|
|
3302
|
+
// Restore original selection
|
|
3303
|
+
wb.setSelection(currentSelection);
|
|
3304
|
+
if (onCopyResult) {
|
|
3305
|
+
onCopyResult(data);
|
|
3306
|
+
}
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
/**
|
|
3310
|
+
* Creates base selection context menu handlers.
|
|
3311
|
+
*/
|
|
3312
|
+
function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
|
|
3313
|
+
return {
|
|
3314
|
+
onCopy: runner
|
|
3315
|
+
? createCopyHandler(wb, runner, getDefaultNodeSize, onCopyResult)
|
|
3316
|
+
: () => {
|
|
3317
|
+
// No-op if runner not available
|
|
3318
|
+
onClose();
|
|
3319
|
+
},
|
|
3320
|
+
onDelete: () => {
|
|
3321
|
+
wb.deleteSelection();
|
|
3322
|
+
onClose();
|
|
3323
|
+
},
|
|
3324
|
+
onClose,
|
|
3325
|
+
};
|
|
3326
|
+
}
|
|
3327
|
+
/**
|
|
3328
|
+
* Creates base default context menu handlers.
|
|
3329
|
+
*/
|
|
3330
|
+
function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste) {
|
|
3331
|
+
return {
|
|
3332
|
+
onAddNode,
|
|
3333
|
+
onPaste,
|
|
3334
|
+
onClose,
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
|
|
3338
|
+
try {
|
|
3339
|
+
const def = wb.export();
|
|
3340
|
+
const node = def.nodes.find((n) => n.nodeId === nodeId);
|
|
3341
|
+
if (!node)
|
|
3342
|
+
return [];
|
|
3343
|
+
const desc = registry.nodes.get(node.typeId);
|
|
3344
|
+
const handles = Object.keys(desc?.outputs || {});
|
|
3345
|
+
const out = [];
|
|
3346
|
+
for (const h of handles) {
|
|
3347
|
+
const tId = outputTypesMap?.[nodeId]?.[h];
|
|
3348
|
+
if (!tId)
|
|
3349
|
+
continue;
|
|
3350
|
+
if (tId.endsWith("[]")) {
|
|
3351
|
+
const base = tId.slice(0, -2);
|
|
3352
|
+
const tArr = registry.types.get(tId);
|
|
3353
|
+
const tElem = registry.types.get(base);
|
|
3354
|
+
const arrT = tArr?.bakeTarget;
|
|
3355
|
+
const elemT = tElem?.bakeTarget;
|
|
3356
|
+
if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
|
|
3357
|
+
(elemT && registry.nodes.has(elemT.nodeTypeId)))
|
|
3358
|
+
out.push(h);
|
|
3359
|
+
}
|
|
3360
|
+
else {
|
|
3361
|
+
const t = registry.types.get(tId);
|
|
3362
|
+
const bt = t?.bakeTarget;
|
|
3363
|
+
if (bt && registry.nodes.has(bt.nodeTypeId))
|
|
3364
|
+
out.push(h);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
return out;
|
|
3368
|
+
}
|
|
3369
|
+
catch {
|
|
3370
|
+
return [];
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
2959
3374
|
function IssueBadge({ level, title, size = 12, className, }) {
|
|
2960
3375
|
const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
|
|
2961
3376
|
return (jsxRuntime.jsx("button", { type: "button", className: `inline-flex items-center justify-center shrink-0 ${colorClass} ${className ?? ""}`, title: title, style: { width: size, height: size }, children: level === "error" ? (jsxRuntime.jsx(react$1.XCircleIcon, { size: size, weight: "fill" })) : (jsxRuntime.jsx(react$1.WarningCircleIcon, { size: size, weight: "fill" })) }));
|
|
@@ -3168,7 +3583,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3168
3583
|
}
|
|
3169
3584
|
catch { }
|
|
3170
3585
|
};
|
|
3171
|
-
return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), jsxRuntime.jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
3586
|
+
return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-auto select-none`, children: [jsxRuntime.jsxs("div", { className: "flex-1 overflow-auto", children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), inputValidationErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [inputValidationErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Input Validation Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), jsxRuntime.jsxs("div", { className: "text-[10px] text-red-600 mt-1", children: [err.nodeId, ".", err.handle, " (type: ", err.typeId, ")"] })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeInputValidationError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), inputValidationErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearInputValidationErrors, children: "Clear all" }))] })), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: jsxRuntime.jsx(react$1.XIcon, { size: 10 }) })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
3172
3587
|
e.stopPropagation();
|
|
3173
3588
|
deleteEdgeById(m.data?.edgeId);
|
|
3174
3589
|
}, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), renderEdgeStatus(), jsxRuntime.jsx("div", { className: "mt-1", children: jsxRuntime.jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
|
|
@@ -3236,7 +3651,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3236
3651
|
const title = inIssues
|
|
3237
3652
|
.map((v) => `${v.code}: ${v.message}`)
|
|
3238
3653
|
.join("; ");
|
|
3239
|
-
return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1", value: current !== undefined && current !== null
|
|
3654
|
+
return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1 select-text", value: current !== undefined && current !== null
|
|
3240
3655
|
? String(current)
|
|
3241
3656
|
: "", onChange: (e) => {
|
|
3242
3657
|
const val = e.target.value;
|
|
@@ -3250,7 +3665,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
|
|
|
3250
3665
|
? `Default: ${placeholder}`
|
|
3251
3666
|
: "(select)" }), registry.enums
|
|
3252
3667
|
.get(typeId)
|
|
3253
|
-
?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-1", children: jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1", placeholder: placeholder
|
|
3668
|
+
?.options.map((opt) => (jsxRuntime.jsx("option", { value: String(opt.value), children: opt.label }, opt.value)))] }), hasValue && !isLinked && (jsxRuntime.jsx("button", { className: "flex-shrink-0 p-1 hover:bg-gray-100 rounded text-gray-500 hover:text-gray-700", onClick: clearInput, title: "Clear input value", children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 16 }) }))] })) : isLinked ? (jsxRuntime.jsx("div", { className: "flex items-center gap-1 flex-1", children: jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: renderLinkedInputDisplay(typeId, current) }) })) : (jsxRuntime.jsxs("div", { className: "flex items-center gap-1 flex-1", children: [jsxRuntime.jsx("input", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1 select-text", placeholder: placeholder
|
|
3254
3669
|
? `Default: ${placeholder}`
|
|
3255
3670
|
: undefined, value: displayValue, onChange: (e) => onChangeText(e.target.value), onBlur: commit, onKeyDown: (e) => {
|
|
3256
3671
|
if (e.key === "Enter")
|
|
@@ -3503,6 +3918,13 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
|
|
|
3503
3918
|
handlers.onAddNode(typeId, { position: p });
|
|
3504
3919
|
handlers.onClose();
|
|
3505
3920
|
};
|
|
3921
|
+
const handlePaste = () => {
|
|
3922
|
+
if (!handlers.onPaste)
|
|
3923
|
+
return;
|
|
3924
|
+
const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
|
|
3925
|
+
handlers.onPaste(p);
|
|
3926
|
+
handlers.onClose();
|
|
3927
|
+
};
|
|
3506
3928
|
const renderTree = (tree, path = []) => {
|
|
3507
3929
|
const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
|
|
3508
3930
|
return (jsxRuntime.jsx("div", { children: entries.map(([key, child]) => {
|
|
@@ -3517,10 +3939,10 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
|
|
|
3517
3939
|
return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "px-2 py-1 text-[11px] uppercase tracking-wide text-gray-400", children: label }), child.__self && (jsxRuntime.jsx("button", { onClick: () => handleClick(child.__self), className: "block w-full text-left px-3 py-1 hover:bg-gray-100 cursor-pointer", title: child.__self, children: label })), jsxRuntime.jsx("div", { className: "pl-2 border-l border-gray-200 ml-2", children: renderTree(child, [...path, key]) })] }, idKey));
|
|
3518
3940
|
}) }));
|
|
3519
3941
|
};
|
|
3520
|
-
return (jsxRuntime.jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-
|
|
3942
|
+
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) => {
|
|
3521
3943
|
e.preventDefault();
|
|
3522
3944
|
e.stopPropagation();
|
|
3523
|
-
}, children: [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", 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" })) })] }));
|
|
3945
|
+
}, children: [handlers.onPaste && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed", onClick: handlePaste, children: "Paste" })), handlers.onPaste && 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" })) })] }));
|
|
3524
3946
|
}
|
|
3525
3947
|
|
|
3526
3948
|
function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, }) {
|
|
@@ -3558,199 +3980,55 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
|
|
|
3558
3980
|
const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
|
|
3559
3981
|
(MENU_MIN_WIDTH + PADDING));
|
|
3560
3982
|
const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
|
|
3561
|
-
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", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
|
|
3983
|
+
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) => {
|
|
3562
3984
|
e.preventDefault();
|
|
3563
3985
|
e.stopPropagation();
|
|
3564
|
-
}, 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" }),
|
|
3986
|
+
}, 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)))] }))] }));
|
|
3565
3987
|
}
|
|
3566
3988
|
|
|
3567
|
-
function
|
|
3568
|
-
const
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
const coerceIfNeeded = async (fromType, toType, value) => {
|
|
3576
|
-
if (!toType || toType === fromType || !runner?.coerce)
|
|
3577
|
-
return value;
|
|
3578
|
-
try {
|
|
3579
|
-
return await runner.coerce(fromType, toType, value);
|
|
3580
|
-
}
|
|
3581
|
-
catch {
|
|
3582
|
-
return value;
|
|
3583
|
-
}
|
|
3584
|
-
};
|
|
3585
|
-
const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
|
|
3586
|
-
const isArray = typeId.endsWith("[]");
|
|
3587
|
-
const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
|
|
3588
|
-
const tArr = isArray ? registry.types.get(typeId) : undefined;
|
|
3589
|
-
const tElem = registry.types.get(baseTypeId);
|
|
3590
|
-
const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
|
|
3591
|
-
const arrTarget = isArray ? tArr?.bakeTarget : undefined;
|
|
3592
|
-
const elemTarget = isArray ? tElem?.bakeTarget : undefined;
|
|
3593
|
-
if (singleTarget) {
|
|
3594
|
-
const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
|
|
3595
|
-
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
|
|
3596
|
-
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3597
|
-
wb.addNode({
|
|
3598
|
-
typeId: singleTarget.nodeTypeId,
|
|
3599
|
-
position: { x: pos.x + 180, y: pos.y },
|
|
3600
|
-
}, { inputs: { [singleTarget.inputHandle]: coerced } });
|
|
3601
|
-
return;
|
|
3602
|
-
}
|
|
3603
|
-
if (isArray && arrTarget) {
|
|
3604
|
-
const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
|
|
3605
|
-
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
|
|
3606
|
-
const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
|
|
3607
|
-
wb.addNode({
|
|
3608
|
-
typeId: arrTarget.nodeTypeId,
|
|
3609
|
-
position: { x: pos.x + 180, y: pos.y },
|
|
3610
|
-
}, { inputs: { [arrTarget.inputHandle]: coerced } });
|
|
3611
|
-
return;
|
|
3612
|
-
}
|
|
3613
|
-
if (isArray && elemTarget) {
|
|
3614
|
-
const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
|
|
3615
|
-
const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
|
|
3616
|
-
const src = unwrap(raw);
|
|
3617
|
-
const items = Array.isArray(src) ? src : [src];
|
|
3618
|
-
const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
|
|
3619
|
-
const COLS = 4;
|
|
3620
|
-
const DX = 180;
|
|
3621
|
-
const DY = 160;
|
|
3622
|
-
for (let idx = 0; idx < coercedItems.length; idx++) {
|
|
3623
|
-
const col = idx % COLS;
|
|
3624
|
-
const row = Math.floor(idx / COLS);
|
|
3625
|
-
wb.addNode({
|
|
3626
|
-
typeId: elemTarget.nodeTypeId,
|
|
3627
|
-
position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
|
|
3628
|
-
}, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
|
|
3629
|
-
}
|
|
3989
|
+
function SelectionContextMenu({ open, clientPos, handlers, }) {
|
|
3990
|
+
const ref = React.useRef(null);
|
|
3991
|
+
// Close on outside click and on ESC
|
|
3992
|
+
React.useEffect(() => {
|
|
3993
|
+
if (!open)
|
|
3994
|
+
return;
|
|
3995
|
+
const onDown = (e) => {
|
|
3996
|
+
if (!ref.current)
|
|
3630
3997
|
return;
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
onClose();
|
|
3662
|
-
},
|
|
3663
|
-
onDuplicateWithEdges: async () => {
|
|
3664
|
-
const def = wb.export();
|
|
3665
|
-
const n = def.nodes.find((n) => n.nodeId === nodeId);
|
|
3666
|
-
if (!n)
|
|
3667
|
-
return onClose();
|
|
3668
|
-
const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
|
|
3669
|
-
// Get inputs without bindings (literal values only)
|
|
3670
|
-
const inputs = runner.getInputs(def)[nodeId] || {};
|
|
3671
|
-
// Add the duplicated node
|
|
3672
|
-
const newNodeId = wb.addNode({
|
|
3673
|
-
typeId: n.typeId,
|
|
3674
|
-
params: n.params,
|
|
3675
|
-
position: { x: pos.x + 24, y: pos.y + 24 },
|
|
3676
|
-
resolvedHandles: n.resolvedHandles,
|
|
3677
|
-
}, {
|
|
3678
|
-
inputs,
|
|
3679
|
-
copyOutputsFrom: nodeId,
|
|
3680
|
-
dry: true,
|
|
3681
|
-
});
|
|
3682
|
-
// Find all incoming edges (edges where target is the original node)
|
|
3683
|
-
const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
|
|
3684
|
-
// Duplicate each incoming edge to point to the new node
|
|
3685
|
-
for (const edge of incomingEdges) {
|
|
3686
|
-
wb.connect({
|
|
3687
|
-
source: edge.source, // Keep the same source
|
|
3688
|
-
target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
|
|
3689
|
-
typeId: edge.typeId,
|
|
3690
|
-
}, { dry: true });
|
|
3691
|
-
}
|
|
3692
|
-
onClose();
|
|
3693
|
-
},
|
|
3694
|
-
onRunPull: async () => {
|
|
3695
|
-
try {
|
|
3696
|
-
await runner.computeNode(nodeId);
|
|
3697
|
-
}
|
|
3698
|
-
catch { }
|
|
3699
|
-
onClose();
|
|
3700
|
-
},
|
|
3701
|
-
onBake: async (handleId) => {
|
|
3702
|
-
await doBake(handleId);
|
|
3703
|
-
onClose();
|
|
3704
|
-
},
|
|
3705
|
-
onCopyId: async () => {
|
|
3706
|
-
try {
|
|
3707
|
-
await navigator.clipboard.writeText(nodeId);
|
|
3708
|
-
}
|
|
3709
|
-
catch { }
|
|
3710
|
-
onClose();
|
|
3711
|
-
},
|
|
3712
|
-
onClose,
|
|
3713
|
-
};
|
|
3714
|
-
}
|
|
3715
|
-
function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
|
|
3716
|
-
try {
|
|
3717
|
-
const def = wb.export();
|
|
3718
|
-
const node = def.nodes.find((n) => n.nodeId === nodeId);
|
|
3719
|
-
if (!node)
|
|
3720
|
-
return [];
|
|
3721
|
-
const desc = registry.nodes.get(node.typeId);
|
|
3722
|
-
const handles = Object.keys(desc?.outputs || {});
|
|
3723
|
-
const out = [];
|
|
3724
|
-
for (const h of handles) {
|
|
3725
|
-
const tId = outputTypesMap?.[nodeId]?.[h];
|
|
3726
|
-
if (!tId)
|
|
3727
|
-
continue;
|
|
3728
|
-
if (tId.endsWith("[]")) {
|
|
3729
|
-
const base = tId.slice(0, -2);
|
|
3730
|
-
const tArr = registry.types.get(tId);
|
|
3731
|
-
const tElem = registry.types.get(base);
|
|
3732
|
-
const arrT = tArr?.bakeTarget;
|
|
3733
|
-
const elemT = tElem?.bakeTarget;
|
|
3734
|
-
if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
|
|
3735
|
-
(elemT && registry.nodes.has(elemT.nodeTypeId)))
|
|
3736
|
-
out.push(h);
|
|
3737
|
-
}
|
|
3738
|
-
else {
|
|
3739
|
-
const t = registry.types.get(tId);
|
|
3740
|
-
const bt = t?.bakeTarget;
|
|
3741
|
-
if (bt && registry.nodes.has(bt.nodeTypeId))
|
|
3742
|
-
out.push(h);
|
|
3743
|
-
}
|
|
3744
|
-
}
|
|
3745
|
-
return out;
|
|
3746
|
-
}
|
|
3747
|
-
catch {
|
|
3748
|
-
return [];
|
|
3749
|
-
}
|
|
3998
|
+
if (!ref.current.contains(e.target))
|
|
3999
|
+
handlers.onClose();
|
|
4000
|
+
};
|
|
4001
|
+
const onKey = (e) => {
|
|
4002
|
+
if (e.key === "Escape")
|
|
4003
|
+
handlers.onClose();
|
|
4004
|
+
};
|
|
4005
|
+
window.addEventListener("mousedown", onDown, true);
|
|
4006
|
+
window.addEventListener("keydown", onKey);
|
|
4007
|
+
return () => {
|
|
4008
|
+
window.removeEventListener("mousedown", onDown, true);
|
|
4009
|
+
window.removeEventListener("keydown", onKey);
|
|
4010
|
+
};
|
|
4011
|
+
}, [open, handlers]);
|
|
4012
|
+
React.useEffect(() => {
|
|
4013
|
+
if (open)
|
|
4014
|
+
ref.current?.focus();
|
|
4015
|
+
}, [open]);
|
|
4016
|
+
if (!open || !clientPos)
|
|
4017
|
+
return null;
|
|
4018
|
+
// Clamp menu position to viewport
|
|
4019
|
+
const MENU_MIN_WIDTH = 180;
|
|
4020
|
+
const PADDING = 16;
|
|
4021
|
+
const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
|
|
4022
|
+
(MENU_MIN_WIDTH + PADDING));
|
|
4023
|
+
const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 100);
|
|
4024
|
+
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) => {
|
|
4025
|
+
e.preventDefault();
|
|
4026
|
+
e.stopPropagation();
|
|
4027
|
+
}, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), 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.onDelete, children: "Delete" })] }));
|
|
3750
4028
|
}
|
|
3751
4029
|
|
|
3752
4030
|
const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
|
|
3753
|
-
const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, } = useWorkbenchContext();
|
|
4031
|
+
const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, overrides, } = useWorkbenchContext();
|
|
3754
4032
|
const nodeValidation = validationByNode;
|
|
3755
4033
|
const edgeValidation = validationByEdge.errors;
|
|
3756
4034
|
const [registryVersion, setRegistryVersion] = React.useState(0);
|
|
@@ -3989,23 +4267,100 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
3989
4267
|
const [nodeMenuOpen, setNodeMenuOpen] = React.useState(false);
|
|
3990
4268
|
const [nodeMenuPos, setNodeMenuPos] = React.useState(null);
|
|
3991
4269
|
const [nodeAtMenu, setNodeAtMenu] = React.useState(null);
|
|
4270
|
+
const [selectionMenuPos, setSelectionMenuPos] = React.useState(null);
|
|
4271
|
+
const [selectionMenuOpen, setSelectionMenuOpen] = React.useState(false);
|
|
4272
|
+
// Compute the rectangular screen-space bounds of the current selection
|
|
4273
|
+
const getSelectionScreenBounds = () => {
|
|
4274
|
+
if (typeof document === "undefined")
|
|
4275
|
+
return null;
|
|
4276
|
+
const selection = wb.getSelection();
|
|
4277
|
+
if (!selection.nodes.length)
|
|
4278
|
+
return null;
|
|
4279
|
+
let bounds = null;
|
|
4280
|
+
for (const nodeId of selection.nodes) {
|
|
4281
|
+
const el = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`);
|
|
4282
|
+
if (!el)
|
|
4283
|
+
continue;
|
|
4284
|
+
const rect = el.getBoundingClientRect();
|
|
4285
|
+
if (!bounds) {
|
|
4286
|
+
bounds = {
|
|
4287
|
+
left: rect.left,
|
|
4288
|
+
top: rect.top,
|
|
4289
|
+
right: rect.right,
|
|
4290
|
+
bottom: rect.bottom,
|
|
4291
|
+
};
|
|
4292
|
+
}
|
|
4293
|
+
else {
|
|
4294
|
+
bounds.left = Math.min(bounds.left, rect.left);
|
|
4295
|
+
bounds.top = Math.min(bounds.top, rect.top);
|
|
4296
|
+
bounds.right = Math.max(bounds.right, rect.right);
|
|
4297
|
+
bounds.bottom = Math.max(bounds.bottom, rect.bottom);
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
return bounds;
|
|
4301
|
+
};
|
|
3992
4302
|
const onContextMenu = (e) => {
|
|
3993
4303
|
e.preventDefault();
|
|
3994
|
-
//
|
|
4304
|
+
// First, check if the cursor is inside the rectangular bounds of the current selection
|
|
4305
|
+
const selectionBounds = getSelectionScreenBounds();
|
|
4306
|
+
if (selectionBounds) {
|
|
4307
|
+
const { left, top, right, bottom } = selectionBounds;
|
|
4308
|
+
if (e.clientX >= left &&
|
|
4309
|
+
e.clientX <= right &&
|
|
4310
|
+
e.clientY >= top &&
|
|
4311
|
+
e.clientY <= bottom) {
|
|
4312
|
+
setSelectionMenuPos({ x: e.clientX, y: e.clientY });
|
|
4313
|
+
setSelectionMenuOpen(true);
|
|
4314
|
+
setMenuOpen(false);
|
|
4315
|
+
setNodeMenuOpen(false);
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
// Determine if right-clicked over a node by hit-testing
|
|
3995
4320
|
const target = e.target?.closest(".react-flow__node");
|
|
3996
4321
|
if (target) {
|
|
3997
4322
|
// Resolve node id from data-id attribute React Flow sets
|
|
3998
4323
|
const nodeId = target.getAttribute("data-id");
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4324
|
+
const selection = wb.getSelection();
|
|
4325
|
+
const isSelected = nodeId && selection.nodes.includes(nodeId);
|
|
4326
|
+
if (isSelected) {
|
|
4327
|
+
// Right-clicked on a selected node - show selection menu
|
|
4328
|
+
setSelectionMenuPos({ x: e.clientX, y: e.clientY });
|
|
4329
|
+
setSelectionMenuOpen(true);
|
|
4330
|
+
setMenuOpen(false);
|
|
4331
|
+
setNodeMenuOpen(false);
|
|
4332
|
+
return;
|
|
4333
|
+
}
|
|
4334
|
+
else {
|
|
4335
|
+
// Right-clicked on a non-selected node - show node menu
|
|
4336
|
+
setNodeAtMenu(nodeId);
|
|
4337
|
+
setNodeMenuPos({ x: e.clientX, y: e.clientY });
|
|
4338
|
+
setNodeMenuOpen(true);
|
|
4339
|
+
setMenuOpen(false);
|
|
4340
|
+
setSelectionMenuOpen(false);
|
|
4341
|
+
return;
|
|
4342
|
+
}
|
|
4003
4343
|
}
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4344
|
+
// Check if right-clicked on a selected edge
|
|
4345
|
+
const edgeTarget = e.target?.closest(".react-flow__edge");
|
|
4346
|
+
if (edgeTarget) {
|
|
4347
|
+
const edgeId = edgeTarget.getAttribute("data-id");
|
|
4348
|
+
const selection = wb.getSelection();
|
|
4349
|
+
const isSelected = edgeId && selection.edges.includes(edgeId);
|
|
4350
|
+
if (isSelected) {
|
|
4351
|
+
// Right-clicked on a selected edge - show selection menu
|
|
4352
|
+
setSelectionMenuPos({ x: e.clientX, y: e.clientY });
|
|
4353
|
+
setSelectionMenuOpen(true);
|
|
4354
|
+
setMenuOpen(false);
|
|
4355
|
+
setNodeMenuOpen(false);
|
|
4356
|
+
return;
|
|
4357
|
+
}
|
|
4008
4358
|
}
|
|
4359
|
+
// Right-clicked on empty space - show default menu
|
|
4360
|
+
setMenuPos({ x: e.clientX, y: e.clientY });
|
|
4361
|
+
setMenuOpen(true);
|
|
4362
|
+
setNodeMenuOpen(false);
|
|
4363
|
+
setSelectionMenuOpen(false);
|
|
4009
4364
|
};
|
|
4010
4365
|
const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
|
|
4011
4366
|
const onCloseMenu = React.useCallback(() => {
|
|
@@ -4014,6 +4369,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4014
4369
|
const onCloseNodeMenu = React.useCallback(() => {
|
|
4015
4370
|
setNodeMenuOpen(false);
|
|
4016
4371
|
}, []);
|
|
4372
|
+
const onCloseSelectionMenu = React.useCallback(() => {
|
|
4373
|
+
setSelectionMenuOpen(false);
|
|
4374
|
+
}, []);
|
|
4017
4375
|
React.useEffect(() => {
|
|
4018
4376
|
const off = runner.on("registry", () => {
|
|
4019
4377
|
setRegistryVersion((v) => v + 1);
|
|
@@ -4021,14 +4379,32 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4021
4379
|
return () => off();
|
|
4022
4380
|
}, [runner]);
|
|
4023
4381
|
const nodeIds = React.useMemo(() => Array.from(registry.nodes.keys()), [registry, registryVersion]);
|
|
4024
|
-
const defaultContextMenuHandlers = React.useMemo(() =>
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4382
|
+
const defaultContextMenuHandlers = React.useMemo(() => {
|
|
4383
|
+
const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu);
|
|
4384
|
+
if (overrides?.getDefaultContextMenuHandlers) {
|
|
4385
|
+
return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
|
|
4386
|
+
}
|
|
4387
|
+
return baseHandlers;
|
|
4388
|
+
}, [addNodeAt, onCloseMenu, overrides, wb]);
|
|
4389
|
+
const selectionContextMenuHandlers = React.useMemo(() => {
|
|
4390
|
+
const baseHandlers = createSelectionContextMenuHandlers(wb, onCloseSelectionMenu, overrides?.getDefaultNodeSize, undefined, // onCopyResult - will be provided by overrides
|
|
4391
|
+
runner);
|
|
4392
|
+
if (overrides?.getSelectionContextMenuHandlers) {
|
|
4393
|
+
const selection = wb.getSelection();
|
|
4394
|
+
return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, {
|
|
4395
|
+
getDefaultNodeSize: overrides.getDefaultNodeSize,
|
|
4396
|
+
});
|
|
4397
|
+
}
|
|
4398
|
+
return baseHandlers;
|
|
4399
|
+
}, [wb, runner, overrides, onCloseSelectionMenu]);
|
|
4028
4400
|
const nodeContextMenuHandlers = React.useMemo(() => {
|
|
4029
4401
|
if (!nodeAtMenu)
|
|
4030
4402
|
return null;
|
|
4031
|
-
|
|
4403
|
+
const baseHandlers = createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu, overrides?.getDefaultNodeSize);
|
|
4404
|
+
if (overrides?.getNodeContextMenuHandlers) {
|
|
4405
|
+
return overrides.getNodeContextMenuHandlers(wb, nodeAtMenu, baseHandlers);
|
|
4406
|
+
}
|
|
4407
|
+
return baseHandlers;
|
|
4032
4408
|
}, [
|
|
4033
4409
|
nodeAtMenu,
|
|
4034
4410
|
wb,
|
|
@@ -4037,6 +4413,8 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4037
4413
|
outputsMap,
|
|
4038
4414
|
outputTypesMap,
|
|
4039
4415
|
onCloseNodeMenu,
|
|
4416
|
+
overrides?.getDefaultNodeSize,
|
|
4417
|
+
overrides?.getNodeContextMenuHandlers,
|
|
4040
4418
|
]);
|
|
4041
4419
|
const canRunPull = React.useMemo(() => engineKind()?.toString() === "pull", [engineKind]);
|
|
4042
4420
|
const bakeableOutputs = React.useMemo(() => {
|
|
@@ -4092,7 +4470,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
|
|
|
4092
4470
|
}
|
|
4093
4471
|
}, 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 })) : (jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })), !!nodeAtMenu &&
|
|
4094
4472
|
nodeContextMenuHandlers &&
|
|
4095
|
-
(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 })))] }) }) }));
|
|
4473
|
+
(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 }))] }) }) }));
|
|
4096
4474
|
});
|
|
4097
4475
|
|
|
4098
4476
|
function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
|
|
@@ -4646,12 +5024,18 @@ exports.WorkbenchProvider = WorkbenchProvider;
|
|
|
4646
5024
|
exports.WorkbenchStudio = WorkbenchStudio;
|
|
4647
5025
|
exports.computeEffectiveHandles = computeEffectiveHandles;
|
|
4648
5026
|
exports.countVisibleHandles = countVisibleHandles;
|
|
5027
|
+
exports.createCopyHandler = createCopyHandler;
|
|
5028
|
+
exports.createDefaultContextMenuHandlers = createDefaultContextMenuHandlers;
|
|
4649
5029
|
exports.createHandleBounds = createHandleBounds;
|
|
4650
5030
|
exports.createHandleLayout = createHandleLayout;
|
|
5031
|
+
exports.createNodeContextMenuHandlers = createNodeContextMenuHandlers;
|
|
5032
|
+
exports.createNodeCopyHandler = createNodeCopyHandler;
|
|
5033
|
+
exports.createSelectionContextMenuHandlers = createSelectionContextMenuHandlers;
|
|
4651
5034
|
exports.download = download;
|
|
4652
5035
|
exports.estimateNodeSize = estimateNodeSize;
|
|
4653
5036
|
exports.formatDataUrlAsLabel = formatDataUrlAsLabel;
|
|
4654
5037
|
exports.formatDeclaredTypeSignature = formatDeclaredTypeSignature;
|
|
5038
|
+
exports.getBakeableOutputs = getBakeableOutputs;
|
|
4655
5039
|
exports.getHandleBoundsX = getHandleBoundsX;
|
|
4656
5040
|
exports.getHandleBoundsY = getHandleBoundsY;
|
|
4657
5041
|
exports.getHandleClassName = getHandleClassName;
|