From 220ff49ab523befe4a07ebd4fd19bcd227f440de Mon Sep 17 00:00:00 2001 From: Daria Date: Thu, 18 Jun 2026 12:17:04 +0300 Subject: [PATCH] fix(editor): Clear selection box when collapsing a node group (no-changelog) (#32396) --- .../canvas/components/Canvas.test.ts | 44 +++++++++++++++++++ .../workflows/canvas/components/Canvas.vue | 15 ++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.test.ts b/packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.test.ts index d39e4afc98e..a004a929b80 100644 --- a/packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.test.ts +++ b/packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.test.ts @@ -28,6 +28,7 @@ import { canvasEventBus } from '@/features/workflows/canvas/canvas.eventBus'; import { createEventBus } from '@n8n/utils/event-bus'; import { usePostHog } from '@/app/stores/posthog.store'; import { GROUP_PADDING_Y_BOTTOM, GROUP_PADDING_Y_TOP } from '../stores/canvasNodeGroups.constants'; +import { NodeGroupViewKey, type CanvasNodeGroupView } from '../composables/useCanvasNodeGroupView'; let workflowDocumentStore: ReturnType; @@ -224,6 +225,49 @@ describe('Canvas', () => { ); }); + it('should clear selected members when their group is collapsed', async () => { + vi.spyOn(usePostHog(), 'isFeatureEnabled').mockImplementation( + (name) => name === CANVAS_NODES_GROUPING_EXPERIMENT.name, + ); + vi.spyOn(workflowDocumentStore, 'getGroupById').mockReturnValue({ + id: 'g1', + name: 'Group 1', + nodeIds: ['node-1'], + }); + + let collapsed = false; + const nodeGroupView = { + isGroupCollapsed: () => collapsed, + toggleCollapsed: () => { + collapsed = !collapsed; + }, + getVisualOffsetForNode: () => ({ x: 0, y: 0 }), + getVisualOffsetForComponent: () => ({ x: 0, y: 0 }), + syncLayoutComponents: () => {}, + settleManualNodePositions: (events: unknown) => events, + commitMovedPushSourceEffects: () => [], + } as unknown as CanvasNodeGroupView; + + const node = createCanvasNodeElement({ id: 'node-1' }); + const groupNode = createCanvasGroupNode({ selectable: true }); + const eventBus = createEventBus(); + + const { container, getByTestId } = renderComponent({ + props: { nodes: [node, groupNode], eventBus }, + global: { provide: { [NodeGroupViewKey as symbol]: nodeGroupView } }, + }); + + await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(2)); + + const { getSelectedNodes } = useVueFlow(canvasId); + eventBus.emit('nodes:selectAll'); + await waitFor(() => expect(getSelectedNodes.value.map(({ id }) => id)).toContain('node-1')); + + await fireEvent.click(getByTestId('canvas-node-group-toggle')); + + await waitFor(() => expect(getSelectedNodes.value.map(({ id }) => id)).not.toContain('node-1')); + }); + it('should expand a selected collapsed group to its members when copying', async () => { vi.spyOn(usePostHog(), 'isFeatureEnabled').mockImplementation( (name) => name === CANVAS_NODES_GROUPING_EXPERIMENT.name, diff --git a/packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue b/packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue index f482e96c28a..c6517d717fd 100644 --- a/packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue +++ b/packages/frontend/editor-ui/src/features/workflows/canvas/components/Canvas.vue @@ -618,8 +618,19 @@ function onSelectionDrag(event: NodeDragEvent) { function onCanvasGroupToggle(groupId: string) { injectedNodeGroupView?.toggleCollapsed(groupId); - // Expanding makes the title bar non-selectable, so drop any selection lingering on it. - if (injectedNodeGroupView && !injectedNodeGroupView.isGroupCollapsed(groupId)) { + if (!injectedNodeGroupView) return; + + if (injectedNodeGroupView.isGroupCollapsed(groupId)) { + // Collapsing hides the members, so drop them from the selection to clear the lingering box. + const memberNodeIds = workflowDocumentStore.value.getGroupById(groupId)?.nodeIds ?? []; + const selectedMembers = memberNodeIds + .map((nodeId) => findNode(nodeId)) + .filter((node): node is NonNullable => node?.selected ?? false); + if (selectedMembers.length > 0) { + removeSelectedNodes(selectedMembers); + } + } else { + // Expanding makes the title bar non-selectable, so drop any selection lingering on it. const groupNode = findNode(`${CANVAS_NODE_GROUP_ID_PREFIX}${groupId}`); if (groupNode) { removeSelectedNodes([groupNode]);