@bian-womp/spark-workbench 0.2.79 → 0.2.80

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 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 {
@@ -2947,6 +2979,19 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2947
2979
  }, [wb, wb.def, registry, overrides?.getDefaultNodeSize]);
2948
2980
  const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
2949
2981
  const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
2982
+ const getNodeDisplayName = React.useCallback((nodeId) => {
2983
+ const customName = wb.getNodeName(nodeId);
2984
+ if (customName)
2985
+ return customName;
2986
+ const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
2987
+ if (!node)
2988
+ return nodeId;
2989
+ const desc = registry.nodes.get(node.typeId);
2990
+ return desc?.displayName || node.typeId;
2991
+ }, [wb, registry]);
2992
+ const setNodeName = React.useCallback((nodeId, name) => {
2993
+ wb.setNodeName(nodeId, name, { commit: true, reason: "rename-node" });
2994
+ }, [wb]);
2950
2995
  // Helper to save runtime metadata and UI state to extData
2951
2996
  const saveUiRuntimeMetadata = React.useCallback(async (workbench, graphRunner) => {
2952
2997
  try {
@@ -3608,6 +3653,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3608
3653
  triggerExternal,
3609
3654
  uiVersion,
3610
3655
  overrides,
3656
+ getNodeDisplayName,
3657
+ setNodeName,
3611
3658
  }), [
3612
3659
  wb,
3613
3660
  runner,
@@ -3647,6 +3694,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3647
3694
  runner,
3648
3695
  uiVersion,
3649
3696
  overrides,
3697
+ getNodeDisplayName,
3698
+ setNodeName,
3650
3699
  ]);
3651
3700
  return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
3652
3701
  }
@@ -4235,11 +4284,27 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
4235
4284
  position: "relative",
4236
4285
  minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
4237
4286
  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 })] }));
4287
+ }, children: [jsxRuntime.jsx(DefaultNodeHeader, { id: id, typeId: typeId, validation: validation, showId: data.showValues }), jsxRuntime.jsx(DefaultNodeContent, { data: data, isConnectable: isConnectable })] }));
4239
4288
  });
4240
4289
  DefaultNode.displayName = "DefaultNode";
4241
- function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate, }) {
4290
+ function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInvalidate, }) {
4242
4291
  const ctx = useWorkbenchContext();
4292
+ const [isEditing, setIsEditing] = React.useState(false);
4293
+ const [editValue, setEditValue] = React.useState("");
4294
+ const inputRef = React.useRef(null);
4295
+ // Use getNodeDisplayName if typeId is provided, otherwise use title prop
4296
+ const displayName = typeId ? ctx.getNodeDisplayName(id) : (title ?? id);
4297
+ const effectiveTypeId = typeId ?? title ?? id;
4298
+ // Get the default display name (without custom name) for comparison
4299
+ const getDefaultDisplayName = React.useCallback(() => {
4300
+ if (!typeId)
4301
+ return title ?? id;
4302
+ const node = ctx.wb.def.nodes.find((n) => n.nodeId === id);
4303
+ if (!node)
4304
+ return id;
4305
+ const desc = ctx.registry.nodes.get(node.typeId);
4306
+ return desc?.displayName || node.typeId;
4307
+ }, [ctx, id, typeId, title]);
4243
4308
  const handleInvalidate = React.useCallback(() => {
4244
4309
  try {
4245
4310
  if (onInvalidate)
@@ -4252,10 +4317,51 @@ function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate,
4252
4317
  }
4253
4318
  catch { }
4254
4319
  }, [ctx, id, onInvalidate]);
4320
+ const handleDoubleClick = React.useCallback((e) => {
4321
+ // Only allow editing if typeId is provided (enables renaming)
4322
+ if (!typeId)
4323
+ return;
4324
+ e.stopPropagation();
4325
+ setIsEditing(true);
4326
+ setEditValue(displayName);
4327
+ }, [typeId, displayName]);
4328
+ const handleSave = React.useCallback(() => {
4329
+ if (!typeId)
4330
+ return;
4331
+ const trimmed = editValue.trim();
4332
+ const defaultDisplayName = getDefaultDisplayName();
4333
+ // If the trimmed value matches the default display name or typeId, clear the custom name
4334
+ ctx.setNodeName(id, trimmed === defaultDisplayName || trimmed === effectiveTypeId
4335
+ ? undefined
4336
+ : trimmed);
4337
+ setIsEditing(false);
4338
+ }, [ctx, id, editValue, getDefaultDisplayName, effectiveTypeId, typeId]);
4339
+ const handleCancel = React.useCallback(() => {
4340
+ setIsEditing(false);
4341
+ setEditValue(displayName);
4342
+ }, [displayName]);
4343
+ const handleKeyDown = React.useCallback((e) => {
4344
+ if (e.key === "Enter") {
4345
+ e.preventDefault();
4346
+ e.stopPropagation();
4347
+ handleSave();
4348
+ }
4349
+ else if (e.key === "Escape") {
4350
+ e.preventDefault();
4351
+ e.stopPropagation();
4352
+ handleCancel();
4353
+ }
4354
+ }, [handleSave, handleCancel]);
4355
+ React.useEffect(() => {
4356
+ if (isEditing && inputRef.current) {
4357
+ inputRef.current.focus();
4358
+ inputRef.current.select();
4359
+ }
4360
+ }, [isEditing]);
4255
4361
  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
4362
  maxHeight: NODE_HEADER_HEIGHT_PX,
4257
4363
  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) => {
4364
+ }, 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
4365
  e.stopPropagation();
4260
4366
  handleInvalidate();
4261
4367
  }, 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")