From e10fa379d3212fd9fd964d8468add07b257af7e1 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:36:03 +0200 Subject: [PATCH] fix: Disable pinning for root nodes from canvas (#8848) --- .../__tests__/useContextMenu.test.ts | 25 ++++++++++++++++++- .../src/composables/useContextMenu.ts | 13 +++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/composables/__tests__/useContextMenu.test.ts b/packages/editor-ui/src/composables/__tests__/useContextMenu.test.ts index 6e3374be64..a36cd0e4a1 100644 --- a/packages/editor-ui/src/composables/__tests__/useContextMenu.test.ts +++ b/packages/editor-ui/src/composables/__tests__/useContextMenu.test.ts @@ -1,11 +1,13 @@ import type { INodeUi } from '@/Interface'; import { useContextMenu } from '@/composables/useContextMenu'; -import { NO_OP_NODE_TYPE, STICKY_NODE_TYPE, STORES } from '@/constants'; +import { BASIC_CHAIN_NODE_TYPE, NO_OP_NODE_TYPE, STICKY_NODE_TYPE, STORES } from '@/constants'; import { faker } from '@faker-js/faker'; import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useUIStore } from '@/stores/ui.store'; +import { useWorkflowsStore } from '@/stores/workflows.store'; +import { NodeHelpers } from 'n8n-workflow'; const nodeFactory = (data: Partial = {}): INodeUi => ({ id: faker.string.uuid(), @@ -20,6 +22,7 @@ const nodeFactory = (data: Partial = {}): INodeUi => ({ describe('useContextMenu', () => { let sourceControlStore: ReturnType; let uiStore: ReturnType; + let workflowsStore: ReturnType; const nodes = [nodeFactory(), nodeFactory(), nodeFactory()]; const selectedNodes = nodes.slice(0, 2); @@ -34,10 +37,19 @@ describe('useContextMenu', () => { ); sourceControlStore = useSourceControlStore(); uiStore = useUIStore(); + workflowsStore = useWorkflowsStore(); vi.spyOn(uiStore, 'isReadOnlyView', 'get').mockReturnValue(false); vi.spyOn(sourceControlStore, 'preferences', 'get').mockReturnValue({ branchReadOnly: false, } as never); + vi.spyOn(workflowsStore, 'getCurrentWorkflow').mockReturnValue({ + nodes, + getNode: (_: string) => { + return {}; + }, + } as never); + + vi.spyOn(NodeHelpers, 'getNodeInputs').mockReturnValue([]); }); afterEach(() => { @@ -80,6 +92,17 @@ describe('useContextMenu', () => { expect(targetNodes.value).toEqual([sticky]); }); + it('should disable pinning for node that has other inputs then "main"', () => { + const { open, isOpen, actions, targetNodes } = useContextMenu(); + const basicChain = nodeFactory({ type: BASIC_CHAIN_NODE_TYPE }); + vi.spyOn(NodeHelpers, 'getConnectionTypes').mockReturnValue(['main', 'ai_languageModel']); + open(mockEvent, { source: 'node-right-click', node: basicChain }); + + expect(isOpen.value).toBe(true); + expect(actions.value.find((action) => action.id === 'toggle_pin')?.disabled).toBe(true); + expect(targetNodes.value).toEqual([basicChain]); + }); + it('should return the correct actions when right clicking a Node', () => { const { open, isOpen, actions, targetNodes } = useContextMenu(); const node = nodeFactory(); diff --git a/packages/editor-ui/src/composables/useContextMenu.ts b/packages/editor-ui/src/composables/useContextMenu.ts index 012235bd15..d84398329a 100644 --- a/packages/editor-ui/src/composables/useContextMenu.ts +++ b/packages/editor-ui/src/composables/useContextMenu.ts @@ -9,6 +9,7 @@ import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useUIStore } from '@/stores/ui.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import type { IActionDropdownItem } from 'n8n-design-system/src/components/N8nActionDropdown/ActionDropdown.vue'; +import { NodeHelpers, NodeConnectionType } from 'n8n-workflow'; import type { INode, INodeTypeDescription } from 'n8n-workflow'; import { computed, ref, watch } from 'vue'; import { getMousePosition } from '../utils/nodeViewUtils'; @@ -158,6 +159,16 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) = ...selectionActions, ]; } else { + const nonMainInputs = (node: INode) => { + const workflow = workflowsStore.getCurrentWorkflow(); + const workflowNode = workflow.getNode(node.name); + const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion); + const inputs = NodeHelpers.getNodeInputs(workflow, workflowNode!, nodeType!); + const inputNames = NodeHelpers.getConnectionTypes(inputs); + + return !!inputNames.find((inputName) => inputName !== NodeConnectionType.Main); + }; + const menuActions: IActionDropdownItem[] = [ !onlyStickies && { id: 'toggle_activation', @@ -173,7 +184,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) = ? i18n.baseText('contextMenu.unpin', i18nOptions) : i18n.baseText('contextMenu.pin', i18nOptions), shortcut: { keys: ['p'] }, - disabled: isReadOnly.value || !nodes.every(canPinNode), + disabled: nodes.some(nonMainInputs) || isReadOnly.value || !nodes.every(canPinNode), }, { id: 'copy',