From a4de491665db0899c475fbbf73e714e76fd5a9ea Mon Sep 17 00:00:00 2001 From: snuziale <6452957+snuziale@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:54:39 -0700 Subject: [PATCH] fix(apollo-react): keep a cleared node name empty instead of the manifest label [MST-17199] Clearing a BaseNode's label inline deleted display.label, so resolveDisplay fell back to the manifest label and the node showed its type name again. Store label: '' instead; resolveDisplay already keeps it and NodeLabel shows the empty-label placeholder. An empty subLabel is still dropped. Behavior change: clearing a name no longer resets it to the type name. A consumer that wants the reset must delete display.label itself. ContainerNode keeps its reset-on-clear behavior for now. Co-Authored-By: Claude Opus 5.5 --- .../components/BaseNode/BaseNode.test.tsx | 41 ++++++++++++++++++- .../canvas/components/BaseNode/BaseNode.tsx | 18 ++++---- .../canvas/utils/manifest-resolver.test.ts | 7 ++++ 3 files changed, 55 insertions(+), 11 deletions(-) diff --git a/packages/apollo-react/src/canvas/components/BaseNode/BaseNode.test.tsx b/packages/apollo-react/src/canvas/components/BaseNode/BaseNode.test.tsx index a55b66f1f8..e9cbe4d8d0 100644 --- a/packages/apollo-react/src/canvas/components/BaseNode/BaseNode.test.tsx +++ b/packages/apollo-react/src/canvas/components/BaseNode/BaseNode.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import type { Node, NodeProps } from '@uipath/apollo-react/canvas/xyflow/react'; import { Position } from '@uipath/apollo-react/canvas/xyflow/react'; import { describe, expect, it, vi } from 'vitest'; @@ -12,6 +12,7 @@ const DEFAULT_MANIFEST = { // Hoisted mocks — available inside vi.mock factories const { mockUpdateNode, + mockUpdateNodeData, mockGetNode, mockNodeState, mockHandleConfigs, @@ -26,6 +27,7 @@ const { mockReadOnlyNodeIds, } = vi.hoisted(() => ({ mockUpdateNode: vi.fn(), + mockUpdateNodeData: vi.fn(), mockGetNode: vi.fn(), // Simulates React Flow's stored node: updateNode writes height here, getNode reads it. mockNodeState: { current: { height: undefined as number | undefined } }, @@ -64,7 +66,7 @@ vi.mock('@uipath/apollo-react/canvas/xyflow/react', async (importOriginal) => ({ useStore: () => mockIsConnecting.current, useUpdateNodeInternals: () => vi.fn(), useReactFlow: () => ({ - updateNodeData: vi.fn(), + updateNodeData: mockUpdateNodeData, updateNode: mockUpdateNode, getNode: mockGetNode, }), @@ -186,6 +188,7 @@ const makeHandles = (position: Position, count: number): HandleGroupManifest[] = describe('BaseNode', () => { afterEach(() => { mockUpdateNode.mockClear(); + mockUpdateNodeData.mockClear(); mockGetNode.mockClear(); mockNodeState.current = { height: undefined }; mockUseButtonHandles.mockClear(); @@ -736,4 +739,38 @@ describe('BaseNode', () => { expect(opts.showAddButton).toBe(false); }); }); + + // NodeLabel is module-mocked above, so the inline edit is driven through the + // onChange it receives, as useInlineLabelEditing would call it on commit. + describe('Inline label editing', () => { + const commitLabel = (values: { label: string; subLabel: string }) => + act(() => mockNodeLabel.mock.calls.at(-1)?.[0]?.onChange(values)); + + it('stores an empty label when the name is cleared', () => { + render( + + ); + commitLabel({ label: '', subLabel: '' }); + expect(mockUpdateNodeData).toHaveBeenCalledWith('script1', { display: { label: '' } }); + }); + + it('drops an empty subLabel', () => { + render(); + commitLabel({ label: 'A', subLabel: '' }); + expect(mockUpdateNodeData).toHaveBeenCalledWith('test-node', { display: { label: 'A' } }); + }); + + it('keeps the other display fields', () => { + render( + + ); + commitLabel({ label: 'B', subLabel: 'Details' }); + expect(mockUpdateNodeData).toHaveBeenCalledWith('test-node', { + display: { label: 'B', subLabel: 'Details', icon: 'mail', color: 'red' }, + }); + }); + }); }); diff --git a/packages/apollo-react/src/canvas/components/BaseNode/BaseNode.tsx b/packages/apollo-react/src/canvas/components/BaseNode/BaseNode.tsx index ad455bcbe2..2cb6c153fc 100644 --- a/packages/apollo-react/src/canvas/components/BaseNode/BaseNode.tsx +++ b/packages/apollo-react/src/canvas/components/BaseNode/BaseNode.tsx @@ -439,15 +439,15 @@ const BaseNodeComponent = (props: NodeProps>) => { const handleBlur = useCallback(() => setIsFocused(false), []); const handleLabelChange = useCallback( - (values: { label: string; subLabel: string }) => { - const newDisplay = { ...data.display }; - // Ensure all label fields are updated or removed appropriately. - for (const labelKey of Object.keys(values) as (keyof typeof values)[]) { - if (values[labelKey]) { - newDisplay[labelKey] = values[labelKey]; - } else { - delete newDisplay[labelKey]; - } + ({ label, subLabel }: { label: string; subLabel: string }) => { + // A cleared label is stored as '' so the node stays unnamed; an unset + // label would fall back to the manifest label. An empty subLabel is + // dropped so a label-only edit doesn't add `subLabel: ''` to the node. + const newDisplay = { ...data.display, label }; + if (subLabel) { + newDisplay.subLabel = subLabel; + } else { + delete newDisplay.subLabel; } updateNodeData(id, { display: newDisplay, diff --git a/packages/apollo-react/src/canvas/utils/manifest-resolver.test.ts b/packages/apollo-react/src/canvas/utils/manifest-resolver.test.ts index 0d24b2d38d..bd8821a762 100644 --- a/packages/apollo-react/src/canvas/utils/manifest-resolver.test.ts +++ b/packages/apollo-react/src/canvas/utils/manifest-resolver.test.ts @@ -122,6 +122,13 @@ describe('resolveDisplay', () => { ); expect(result.label).toBe('Notify Ops'); }); + + it('keeps an empty instance.label instead of falling back to the manifest', () => { + // A cleared name is stored as label: '', meaning "unnamed". Only an unset + // label falls back to the manifest. + const result = resolveDisplay({ label: 'Script', icon: 'code' }, { display: { label: '' } }); + expect(result.label).toBe(''); + }); }); // ============================================================================