mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-26 05:04:05 -08:00
fix: Disable pinning for root nodes from canvas (#8848)
This commit is contained in:
parent
bde4c6c7a1
commit
e10fa379d3
|
@ -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();
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in a new issue