@bian-womp/spark-workbench 0.2.79 → 0.2.81

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.
Files changed (37) hide show
  1. package/lib/cjs/index.cjs +155 -4
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +16 -1
  4. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  5. package/lib/cjs/src/core/contracts.d.ts +3 -0
  6. package/lib/cjs/src/core/contracts.d.ts.map +1 -1
  7. package/lib/cjs/src/index.d.ts +2 -0
  8. package/lib/cjs/src/index.d.ts.map +1 -1
  9. package/lib/cjs/src/misc/DefaultNode.d.ts +3 -2
  10. package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
  11. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +2 -0
  12. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  13. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  14. package/lib/cjs/src/misc/load.d.ts.map +1 -1
  15. package/lib/cjs/src/misc/merge-utils.d.ts +7 -0
  16. package/lib/cjs/src/misc/merge-utils.d.ts.map +1 -0
  17. package/lib/cjs/src/misc/types.d.ts +14 -0
  18. package/lib/cjs/src/misc/types.d.ts.map +1 -0
  19. package/lib/esm/index.js +155 -5
  20. package/lib/esm/index.js.map +1 -1
  21. package/lib/esm/src/core/InMemoryWorkbench.d.ts +16 -1
  22. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  23. package/lib/esm/src/core/contracts.d.ts +3 -0
  24. package/lib/esm/src/core/contracts.d.ts.map +1 -1
  25. package/lib/esm/src/index.d.ts +2 -0
  26. package/lib/esm/src/index.d.ts.map +1 -1
  27. package/lib/esm/src/misc/DefaultNode.d.ts +3 -2
  28. package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
  29. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +2 -0
  30. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  31. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  32. package/lib/esm/src/misc/load.d.ts.map +1 -1
  33. package/lib/esm/src/misc/merge-utils.d.ts +7 -0
  34. package/lib/esm/src/misc/merge-utils.d.ts.map +1 -0
  35. package/lib/esm/src/misc/types.d.ts +14 -0
  36. package/lib/esm/src/misc/types.d.ts.map +1 -0
  37. package/package.json +4 -4
package/lib/cjs/index.cjs CHANGED
@@ -130,8 +130,9 @@ class InMemoryWorkbench extends AbstractWorkbench {
130
130
  nodes: [],
131
131
  edges: [],
132
132
  };
133
- this.viewport = null;
133
+ this.nodeNames = {};
134
134
  this.runtimeState = null;
135
+ this.viewport = null;
135
136
  this.historyState = undefined;
136
137
  this.copiedData = null;
137
138
  }
@@ -228,6 +229,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
228
229
  this._def.nodes = this._def.nodes.filter((n) => n.nodeId !== nodeId);
229
230
  this._def.edges = this._def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
230
231
  delete this.positions[nodeId];
232
+ delete this.nodeNames[nodeId];
231
233
  this.emit("graphChanged", {
232
234
  def: this._def,
233
235
  change: { type: "removeNode", nodeId },
@@ -350,6 +352,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
350
352
  const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
351
353
  const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
352
354
  const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
355
+ const filteredNodeNames = Object.fromEntries(Object.entries(this.nodeNames).filter(([id]) => defNodeIds.has(id)));
353
356
  return {
354
357
  positions: Object.keys(filteredPositions).length > 0
355
358
  ? filteredPositions
@@ -361,6 +364,9 @@ class InMemoryWorkbench extends AbstractWorkbench {
361
364
  }
362
365
  : undefined,
363
366
  viewport: this.viewport ? { ...this.viewport } : undefined,
367
+ nodeNames: Object.keys(filteredNodeNames).length > 0
368
+ ? filteredNodeNames
369
+ : undefined,
364
370
  };
365
371
  }
366
372
  setUIState(ui) {
@@ -379,6 +385,9 @@ class InMemoryWorkbench extends AbstractWorkbench {
379
385
  if (ui.viewport) {
380
386
  this.viewport = { ...ui.viewport };
381
387
  }
388
+ if (ui.nodeNames !== undefined) {
389
+ this.nodeNames = { ...ui.nodeNames };
390
+ }
382
391
  }
383
392
  getRuntimeState() {
384
393
  return this.runtimeState ? { ...this.runtimeState } : null;
@@ -748,6 +757,29 @@ class InMemoryWorkbench extends AbstractWorkbench {
748
757
  setCopiedData(data) {
749
758
  this.copiedData = data;
750
759
  }
760
+ /**
761
+ * Get the custom name for a node, if set.
762
+ */
763
+ getNodeName(nodeId) {
764
+ return this.nodeNames[nodeId];
765
+ }
766
+ /**
767
+ * Set a custom name for a node. Empty string or undefined removes the custom name.
768
+ * This is included in undo/redo history via extData.ui.
769
+ */
770
+ setNodeName(nodeId, name, options) {
771
+ if (name === undefined || name.trim() === "") {
772
+ delete this.nodeNames[nodeId];
773
+ }
774
+ else {
775
+ this.nodeNames[nodeId] = name.trim();
776
+ }
777
+ this.emit("graphUiChanged", {
778
+ def: this._def,
779
+ change: { type: "nodeName", nodeId },
780
+ ...options,
781
+ });
782
+ }
751
783
  }
752
784
 
753
785
  class CLIWorkbench {
@@ -2740,6 +2772,50 @@ async function upload(parsed, wb, runner) {
2740
2772
  }
2741
2773
  }
2742
2774
 
2775
+ /**
2776
+ * Merge UI state from source into target, remapping node IDs using nodeIdMap.
2777
+ * Preserves target state and adds/updates source state with remapped IDs.
2778
+ */
2779
+ function mergeUIState(targetUI, sourceUI, nodeIdMap) {
2780
+ const result = {
2781
+ ...targetUI,
2782
+ };
2783
+ if (!sourceUI)
2784
+ return result;
2785
+ // Merge positions (already handled by mergeSnapshotData, but included for completeness)
2786
+ if (sourceUI.positions) {
2787
+ result.positions = {
2788
+ ...(targetUI?.positions || {}),
2789
+ ...Object.fromEntries(Object.entries(sourceUI.positions).map(([oldId, pos]) => [
2790
+ nodeIdMap[oldId] || oldId,
2791
+ pos,
2792
+ ])),
2793
+ };
2794
+ }
2795
+ // Merge selection: remap node IDs and edge IDs
2796
+ if (sourceUI.selection) {
2797
+ const remappedNodes = (sourceUI.selection.nodes || [])
2798
+ .map((id) => nodeIdMap[id] || id)
2799
+ .filter((id) => id); // Filter out invalid mappings
2800
+ const remappedEdges = sourceUI.selection.edges || []; // Edge IDs don't need remapping typically
2801
+ result.selection = {
2802
+ nodes: [...(targetUI?.selection?.nodes || []), ...remappedNodes],
2803
+ edges: [...(targetUI?.selection?.edges || []), ...remappedEdges],
2804
+ };
2805
+ }
2806
+ // Merge nodeNames: remap node IDs
2807
+ if (sourceUI.nodeNames) {
2808
+ result.nodeNames = {
2809
+ ...(targetUI?.nodeNames || {}),
2810
+ ...Object.fromEntries(Object.entries(sourceUI.nodeNames).map(([oldId, name]) => [
2811
+ nodeIdMap[oldId] || oldId,
2812
+ name,
2813
+ ])),
2814
+ };
2815
+ }
2816
+ return result;
2817
+ }
2818
+
2743
2819
  const WorkbenchContext = React.createContext(null);
2744
2820
  function useWorkbenchContext() {
2745
2821
  const ctx = React.useContext(WorkbenchContext);
@@ -2947,6 +3023,19 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2947
3023
  }, [wb, wb.def, registry, overrides?.getDefaultNodeSize]);
2948
3024
  const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
2949
3025
  const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
3026
+ const getNodeDisplayName = React.useCallback((nodeId) => {
3027
+ const customName = wb.getNodeName(nodeId);
3028
+ if (customName)
3029
+ return customName;
3030
+ const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
3031
+ if (!node)
3032
+ return nodeId;
3033
+ const desc = registry.nodes.get(node.typeId);
3034
+ return desc?.displayName || node.typeId;
3035
+ }, [wb, registry]);
3036
+ const setNodeName = React.useCallback((nodeId, name) => {
3037
+ wb.setNodeName(nodeId, name, { commit: true, reason: "rename-node" });
3038
+ }, [wb]);
2950
3039
  // Helper to save runtime metadata and UI state to extData
2951
3040
  const saveUiRuntimeMetadata = React.useCallback(async (workbench, graphRunner) => {
2952
3041
  try {
@@ -3608,6 +3697,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3608
3697
  triggerExternal,
3609
3698
  uiVersion,
3610
3699
  overrides,
3700
+ getNodeDisplayName,
3701
+ setNodeName,
3611
3702
  }), [
3612
3703
  wb,
3613
3704
  runner,
@@ -3647,6 +3738,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3647
3738
  runner,
3648
3739
  uiVersion,
3649
3740
  overrides,
3741
+ getNodeDisplayName,
3742
+ setNodeName,
3650
3743
  ]);
3651
3744
  return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
3652
3745
  }
@@ -4235,11 +4328,27 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
4235
4328
  position: "relative",
4236
4329
  minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
4237
4330
  minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
4238
- }, children: [jsxRuntime.jsx(DefaultNodeHeader, { id: id, title: typeId, validation: validation, showId: data.showValues }), jsxRuntime.jsx(DefaultNodeContent, { data: data, isConnectable: isConnectable })] }));
4331
+ }, children: [jsxRuntime.jsx(DefaultNodeHeader, { id: id, typeId: typeId, validation: validation, showId: data.showValues }), jsxRuntime.jsx(DefaultNodeContent, { data: data, isConnectable: isConnectable })] }));
4239
4332
  });
4240
4333
  DefaultNode.displayName = "DefaultNode";
4241
- function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate, }) {
4334
+ function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInvalidate, }) {
4242
4335
  const ctx = useWorkbenchContext();
4336
+ const [isEditing, setIsEditing] = React.useState(false);
4337
+ const [editValue, setEditValue] = React.useState("");
4338
+ const inputRef = React.useRef(null);
4339
+ // Use getNodeDisplayName if typeId is provided, otherwise use title prop
4340
+ const displayName = typeId ? ctx.getNodeDisplayName(id) : title ?? id;
4341
+ const effectiveTypeId = typeId ?? title ?? id;
4342
+ // Get the default display name (without custom name) for comparison
4343
+ const getDefaultDisplayName = React.useCallback(() => {
4344
+ if (!typeId)
4345
+ return title ?? id;
4346
+ const node = ctx.wb.def.nodes.find((n) => n.nodeId === id);
4347
+ if (!node)
4348
+ return id;
4349
+ const desc = ctx.registry.nodes.get(node.typeId);
4350
+ return desc?.displayName || node.typeId;
4351
+ }, [ctx, id, typeId, title]);
4243
4352
  const handleInvalidate = React.useCallback(() => {
4244
4353
  try {
4245
4354
  if (onInvalidate)
@@ -4252,10 +4361,51 @@ function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate,
4252
4361
  }
4253
4362
  catch { }
4254
4363
  }, [ctx, id, onInvalidate]);
4364
+ const handleDoubleClick = React.useCallback((e) => {
4365
+ // Only allow editing if typeId is provided (enables renaming)
4366
+ if (!typeId)
4367
+ return;
4368
+ e.stopPropagation();
4369
+ setIsEditing(true);
4370
+ setEditValue(displayName);
4371
+ }, [typeId, displayName]);
4372
+ const handleSave = React.useCallback(() => {
4373
+ if (!typeId)
4374
+ return;
4375
+ const trimmed = editValue.trim();
4376
+ const defaultDisplayName = getDefaultDisplayName();
4377
+ // If the trimmed value matches the default display name or typeId, clear the custom name
4378
+ ctx.setNodeName(id, trimmed === defaultDisplayName || trimmed === effectiveTypeId
4379
+ ? undefined
4380
+ : trimmed);
4381
+ setIsEditing(false);
4382
+ }, [ctx, id, editValue, getDefaultDisplayName, effectiveTypeId, typeId]);
4383
+ const handleCancel = React.useCallback(() => {
4384
+ setIsEditing(false);
4385
+ setEditValue(displayName);
4386
+ }, [displayName]);
4387
+ const handleKeyDown = React.useCallback((e) => {
4388
+ if (e.key === "Enter") {
4389
+ e.preventDefault();
4390
+ e.stopPropagation();
4391
+ handleSave();
4392
+ }
4393
+ else if (e.key === "Escape") {
4394
+ e.preventDefault();
4395
+ e.stopPropagation();
4396
+ handleCancel();
4397
+ }
4398
+ }, [handleSave, handleCancel]);
4399
+ React.useEffect(() => {
4400
+ if (isEditing && inputRef.current) {
4401
+ inputRef.current.focus();
4402
+ inputRef.current.select();
4403
+ }
4404
+ }, [isEditing]);
4255
4405
  return (jsxRuntime.jsxs("div", { className: "flex items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", style: {
4256
4406
  maxHeight: NODE_HEADER_HEIGHT_PX,
4257
4407
  minHeight: NODE_HEADER_HEIGHT_PX,
4258
- }, children: [jsxRuntime.jsx("strong", { className: "flex-1 h-full text-sm", style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` }, children: title }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("button", { className: "w-4 h-4 border border-gray-400 rounded text-[10px] leading-3 flex items-center justify-center", title: "Invalidate and re-run", onClick: (e) => {
4408
+ }, children: [isEditing ? (jsxRuntime.jsx("input", { ref: inputRef, type: "text", value: editValue, onChange: (e) => setEditValue(e.target.value), onBlur: handleSave, onKeyDown: handleKeyDown, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), className: "flex-1 h-full text-sm bg-transparent border border-blue-500 rounded px-1 outline-none wb-nodrag", style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` } })) : (jsxRuntime.jsx("strong", { className: `flex-1 h-full text-sm select-none truncate ${typeId ? "cursor-text" : ""}`, style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` }, onDoubleClick: handleDoubleClick, title: typeId ? "Double-click to rename" : undefined, children: displayName })), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.jsx("button", { className: "w-4 h-4 border border-gray-400 rounded text-[10px] leading-3 flex items-center justify-center", title: "Invalidate and re-run", onClick: (e) => {
4259
4409
  e.stopPropagation();
4260
4410
  handleInvalidate();
4261
4411
  }, children: jsxRuntime.jsx(react$1.ArrowClockwiseIcon, { size: 10 }) }), right, validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
@@ -5765,6 +5915,7 @@ exports.getHandleLayoutY = getHandleLayoutY;
5765
5915
  exports.getNodeBorderClassNames = getNodeBorderClassNames;
5766
5916
  exports.isValidViewport = isValidViewport;
5767
5917
  exports.layoutNode = layoutNode;
5918
+ exports.mergeUIState = mergeUIState;
5768
5919
  exports.preformatValueForDisplay = preformatValueForDisplay;
5769
5920
  exports.prettyHandle = prettyHandle;
5770
5921
  exports.resolveOutputDisplay = resolveOutputDisplay;