fix: Disable pinning for root nodes from canvas (#8848)

This commit is contained in:
Michael Kret 2024-03-11 15:36:03 +02:00 committed by GitHub
parent bde4c6c7a1
commit e10fa379d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 36 additions and 2 deletions

View file

@ -1,11 +1,13 @@
import type { INodeUi } from '@/Interface'; import type { INodeUi } from '@/Interface';
import { useContextMenu } from '@/composables/useContextMenu'; 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 { faker } from '@faker-js/faker';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia'; import { setActivePinia } from 'pinia';
import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { NodeHelpers } from 'n8n-workflow';
const nodeFactory = (data: Partial<INodeUi> = {}): INodeUi => ({ const nodeFactory = (data: Partial<INodeUi> = {}): INodeUi => ({
id: faker.string.uuid(), id: faker.string.uuid(),
@ -20,6 +22,7 @@ const nodeFactory = (data: Partial<INodeUi> = {}): INodeUi => ({
describe('useContextMenu', () => { describe('useContextMenu', () => {
let sourceControlStore: ReturnType<typeof useSourceControlStore>; let sourceControlStore: ReturnType<typeof useSourceControlStore>;
let uiStore: ReturnType<typeof useUIStore>; let uiStore: ReturnType<typeof useUIStore>;
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
const nodes = [nodeFactory(), nodeFactory(), nodeFactory()]; const nodes = [nodeFactory(), nodeFactory(), nodeFactory()];
const selectedNodes = nodes.slice(0, 2); const selectedNodes = nodes.slice(0, 2);
@ -34,10 +37,19 @@ describe('useContextMenu', () => {
); );
sourceControlStore = useSourceControlStore(); sourceControlStore = useSourceControlStore();
uiStore = useUIStore(); uiStore = useUIStore();
workflowsStore = useWorkflowsStore();
vi.spyOn(uiStore, 'isReadOnlyView', 'get').mockReturnValue(false); vi.spyOn(uiStore, 'isReadOnlyView', 'get').mockReturnValue(false);
vi.spyOn(sourceControlStore, 'preferences', 'get').mockReturnValue({ vi.spyOn(sourceControlStore, 'preferences', 'get').mockReturnValue({
branchReadOnly: false, branchReadOnly: false,
} as never); } as never);
vi.spyOn(workflowsStore, 'getCurrentWorkflow').mockReturnValue({
nodes,
getNode: (_: string) => {
return {};
},
} as never);
vi.spyOn(NodeHelpers, 'getNodeInputs').mockReturnValue([]);
}); });
afterEach(() => { afterEach(() => {
@ -80,6 +92,17 @@ describe('useContextMenu', () => {
expect(targetNodes.value).toEqual([sticky]); 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', () => { it('should return the correct actions when right clicking a Node', () => {
const { open, isOpen, actions, targetNodes } = useContextMenu(); const { open, isOpen, actions, targetNodes } = useContextMenu();
const node = nodeFactory(); const node = nodeFactory();

View file

@ -9,6 +9,7 @@ import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import type { IActionDropdownItem } from 'n8n-design-system/src/components/N8nActionDropdown/ActionDropdown.vue'; 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 type { INode, INodeTypeDescription } from 'n8n-workflow';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { getMousePosition } from '../utils/nodeViewUtils'; import { getMousePosition } from '../utils/nodeViewUtils';
@ -158,6 +159,16 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
...selectionActions, ...selectionActions,
]; ];
} else { } 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[] = [ const menuActions: IActionDropdownItem[] = [
!onlyStickies && { !onlyStickies && {
id: 'toggle_activation', id: 'toggle_activation',
@ -173,7 +184,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
? i18n.baseText('contextMenu.unpin', i18nOptions) ? i18n.baseText('contextMenu.unpin', i18nOptions)
: i18n.baseText('contextMenu.pin', i18nOptions), : i18n.baseText('contextMenu.pin', i18nOptions),
shortcut: { keys: ['p'] }, shortcut: { keys: ['p'] },
disabled: isReadOnly.value || !nodes.every(canPinNode), disabled: nodes.some(nonMainInputs) || isReadOnly.value || !nodes.every(canPinNode),
}, },
{ {
id: 'copy', id: 'copy',