feat(editor): Add Python to Code actions (#18668)

This commit is contained in:
Iván Ovejero
2025-08-26 14:29:50 +02:00
committed by GitHub
parent b73f2393b4
commit 38f25d74eb
16 changed files with 185 additions and 59 deletions

View File

@@ -40,6 +40,8 @@ export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
export const CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code';
export const CODE_NODE_DISPLAY_NAME = 'Code in JavaScript';
export const CODE_NODE_ACTION = CODE_NODE_DISPLAY_NAME;
export const SET_NODE_NAME = 'Set';
export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields';
export const LOOP_OVER_ITEMS_NODE_NAME = 'Loop Over Items';

View File

@@ -1,10 +1,10 @@
import { getCanvasNodes } from '../composables/workflow';
import {
SCHEDULE_TRIGGER_NODE_NAME,
CODE_NODE_NAME,
SET_NODE_NAME,
MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
CODE_NODE_DISPLAY_NAME,
} from '../constants';
import { NDV } from '../pages/ndv';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
@@ -19,9 +19,9 @@ describe('Undo/Redo', () => {
it('should undo/redo deleting node using context menu', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addCodeNodeToCanvas();
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_NAME, {
WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_DISPLAY_NAME, {
method: 'right-click',
anchor: 'topLeft',
});
@@ -37,8 +37,8 @@ describe('Undo/Redo', () => {
it('should undo/redo deleting node using keyboard shortcut', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).click();
WorkflowPage.actions.addCodeNodeToCanvas();
WorkflowPage.getters.canvasNodeByName(CODE_NODE_DISPLAY_NAME).click();
cy.get('body').type('{backspace}');
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
@@ -52,9 +52,9 @@ describe('Undo/Redo', () => {
it('should undo/redo deleting node between two connected nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addCodeNodeToCanvas();
WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).click();
WorkflowPage.getters.canvasNodeByName(CODE_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.zoomToFit();
cy.get('body').type('{backspace}');
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
@@ -69,7 +69,7 @@ describe('Undo/Redo', () => {
it('should undo/redo deleting whole workflow', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addCodeNodeToCanvas();
cy.get('body').type('{esc}');
cy.get('body').type('{esc}');
WorkflowPage.actions.hitDeleteAllNodes();
@@ -85,7 +85,7 @@ describe('Undo/Redo', () => {
it('should undo/redo moving nodes', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addCodeNodeToCanvas();
WorkflowPage.actions.zoomToFit();
@@ -131,8 +131,8 @@ describe('Undo/Redo', () => {
it('should undo/redo deleting a connection using context menu', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.deleteNodeBetweenNodes(SCHEDULE_TRIGGER_NODE_NAME, CODE_NODE_NAME);
WorkflowPage.actions.addCodeNodeToCanvas();
WorkflowPage.actions.deleteNodeBetweenNodes(SCHEDULE_TRIGGER_NODE_NAME, CODE_NODE_DISPLAY_NAME);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.nodeConnections().should('have.length', 1);
@@ -142,8 +142,8 @@ describe('Undo/Redo', () => {
it('should undo/redo disabling a node using context menu', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.disableNode(CODE_NODE_NAME);
WorkflowPage.actions.addCodeNodeToCanvas();
WorkflowPage.actions.disableNode(CODE_NODE_DISPLAY_NAME);
WorkflowPage.getters.disabledNodes().should('have.length', 1);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.disabledNodes().should('have.length', 0);
@@ -153,7 +153,7 @@ describe('Undo/Redo', () => {
it('should undo/redo disabling a node using keyboard shortcut', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addCodeNodeToCanvas();
WorkflowPage.getters.canvasNodes().last().click();
WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 1);
@@ -165,7 +165,7 @@ describe('Undo/Redo', () => {
it('should undo/redo disabling multiple nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addCodeNodeToCanvas();
cy.get('body').type('{esc}');
cy.get('body').type('{esc}');
WorkflowPage.actions.hitSelectAll();
@@ -179,8 +179,8 @@ describe('Undo/Redo', () => {
it('should undo/redo duplicating a node', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.duplicateNode(CODE_NODE_NAME);
WorkflowPage.actions.addCodeNodeToCanvas();
WorkflowPage.actions.duplicateNode(CODE_NODE_DISPLAY_NAME);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.actions.hitRedo();

View File

@@ -6,6 +6,7 @@ import {
EDIT_FIELDS_SET_NODE_NAME,
SWITCH_NODE_NAME,
MERGE_NODE_NAME,
CODE_NODE_DISPLAY_NAME,
} from './../constants';
import {
clickContextMenuAction,
@@ -165,7 +166,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_NAME, {
WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_DISPLAY_NAME, {
method: 'right-click',
anchor: 'topLeft',
});
@@ -176,7 +177,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
it('should delete node using keyboard shortcut', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).click();
WorkflowPage.getters.canvasNodeByName(CODE_NODE_DISPLAY_NAME).click();
cy.get('body').type('{backspace}');
WorkflowPage.getters.canvasNodes().should('have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 0);
@@ -188,7 +189,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
WorkflowPage.getters.canvasNodes().should('have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 2);
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).click();
WorkflowPage.getters.canvasNodeByName(CODE_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.zoomToFit();
cy.get('body').type('{backspace}');
WorkflowPage.getters.canvasNodes().should('have.length', 2);
@@ -318,7 +319,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 1);
WorkflowPage.actions.disableNode(CODE_NODE_NAME);
WorkflowPage.actions.disableNode(CODE_NODE_DISPLAY_NAME);
WorkflowPage.getters.disabledNodes().should('have.length', 0);
});
@@ -394,7 +395,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.duplicateNode(CODE_NODE_NAME);
WorkflowPage.actions.duplicateNode(CODE_NODE_DISPLAY_NAME);
WorkflowPage.getters.canvasNodes().should('have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 1);

View File

@@ -390,7 +390,7 @@ describe('NDV', () => {
});
it('should not push NDV header out with a lot of code in Code node editor', () => {
workflowPage.actions.addInitialNodeToCanvas('Code', { keepNdvOpen: true });
workflowPage.actions.addInitialCodeNodeToCanvas({ keepNdvOpen: true });
ndv.getters.parameterInput('jsCode').get('.cm-content').type('{selectall}').type('{backspace}');
cy.fixture('Dummy_javascript.txt').then((code) => {
ndv.getters.parameterInput('jsCode').get('.cm-content').paste(code);
@@ -400,7 +400,7 @@ describe('NDV', () => {
it('should allow editing code in fullscreen in the code editors', () => {
// Code (JavaScript)
workflowPage.actions.addInitialNodeToCanvas('Code', { keepNdvOpen: true });
workflowPage.actions.addInitialCodeNodeToCanvas({ keepNdvOpen: true });
ndv.actions.openCodeEditorFullscreen();
ndv.getters.codeEditorFullscreen().type('{selectall}').type('{backspace}').type('foo()');
@@ -725,8 +725,7 @@ describe('NDV', () => {
});
it('should properly show node execution indicator', () => {
workflowPage.actions.addInitialNodeToCanvas('Code');
workflowPage.actions.openNode('Code');
workflowPage.actions.addInitialCodeNodeToCanvas();
// Should not show run info before execution
ndv.getters.nodeRunSuccessIndicator().should('not.exist');
ndv.getters.nodeRunErrorIndicator().should('not.exist');
@@ -737,8 +736,7 @@ describe('NDV', () => {
});
it('should properly show node execution indicator for multiple nodes', () => {
workflowPage.actions.addInitialNodeToCanvas('Code');
workflowPage.actions.openNode('Code');
workflowPage.actions.addInitialCodeNodeToCanvas();
ndv.actions.typeIntoParameterInput('jsCode', 'testets');
ndv.getters.backToCanvas().click();
workflowPage.actions.executeWorkflow();

View File

@@ -1,7 +1,7 @@
import { BasePage } from './base';
import { NodeCreator } from './features/node-creator';
import { clickContextMenuAction, getCanvasPane, openContextMenu } from '../composables/workflow';
import { META_KEY } from '../constants';
import { CODE_NODE_ACTION, CODE_NODE_NAME, META_KEY } from '../constants';
import type { OpenContextMenuOptions } from '../types';
import { getVisibleSelect } from '../utils';
import { getUniqueWorkflowName } from '../utils/workflowUtils';
@@ -179,6 +179,12 @@ export class WorkflowPage extends BasePage {
win.preventNodeViewBeforeUnload = preventNodeViewUnload;
});
},
addInitialCodeNodeToCanvas(opts: { keepNdvOpen: boolean } = { keepNdvOpen: false }) {
this.addInitialNodeToCanvas(CODE_NODE_NAME, {
action: CODE_NODE_ACTION,
keepNdvOpen: opts.keepNdvOpen,
});
},
addInitialNodeToCanvas: (
nodeDisplayName: string,
opts?: { keepNdvOpen?: boolean; action?: string; isTrigger?: boolean },
@@ -202,6 +208,9 @@ export class WorkflowPage extends BasePage {
cy.get('body').type('{esc}');
}
},
addCodeNodeToCanvas(plusButtonClick = true, preventNdvClose?: boolean) {
this.addNodeToCanvas(CODE_NODE_NAME, plusButtonClick, preventNdvClose, CODE_NODE_ACTION);
},
addNodeToCanvas: (
nodeDisplayName: string,
plusButtonClick = true,

View File

@@ -733,6 +733,7 @@ export type NodeTypeSelectedPayload = {
parameters?: {
resource?: string;
operation?: string;
language?: string;
};
actionName?: string;
};

View File

@@ -185,6 +185,11 @@ export const useActions = () => {
actionName: actionData.name,
};
if (typeof actionData.value.language === 'string') {
result.parameters = { language: actionData.value.language };
return result;
}
if (
typeof actionData.value.resource === 'string' ||
typeof actionData.value.operation === 'string'

View File

@@ -54,6 +54,25 @@ const customNodeActionsParsers: {
}),
);
},
['n8n-nodes-base.code']: (matchedProperty, nodeTypeDescription) => {
if (matchedProperty.name !== 'language') return;
const languageOptions = matchedProperty.options as INodePropertyOptions[] | undefined;
if (!languageOptions) return;
return languageOptions.map(
(option): ActionTypeDescription => ({
...getNodeTypeBase(nodeTypeDescription),
actionKey: `language_${option.value}`,
displayName: `Code in ${option.name}`,
description: `Run custom ${option.name} code`,
displayOptions: matchedProperty.displayOptions,
values: {
language: option.value,
},
}),
);
},
};
function getNodeTypeBase(nodeTypeDescription: INodeTypeDescription, label?: string) {
@@ -79,6 +98,21 @@ function getNodeTypeBase(nodeTypeDescription: INodeTypeDescription, label?: stri
function operationsCategory(nodeTypeDescription: INodeTypeDescription): ActionTypeDescription[] {
if (nodeTypeDescription.properties.find((property) => property.name === 'resource')) return [];
if (nodeTypeDescription.name === 'n8n-nodes-base.code') {
const languageProperty = nodeTypeDescription.properties.find(
(property) =>
property.name === 'language' && property.displayOptions?.show?.['@version']?.[0] === 2,
);
if (languageProperty) {
const customParsedItems = customNodeActionsParsers[nodeTypeDescription.name]?.(
languageProperty,
nodeTypeDescription,
);
if (customParsedItems) return customParsedItems;
}
}
const matchedProperty = nodeTypeDescription.properties.find(
(property) => property.name?.toLowerCase() === 'operation',
);

View File

@@ -41,10 +41,12 @@ const getV2LanguageProperty = (): INodeProperties => {
{
name: 'JavaScript',
value: 'javaScript',
action: 'Code in JavaScript',
},
{
name: 'Python (Beta)',
value: 'python',
action: 'Code in Python (Beta)',
},
];
@@ -52,6 +54,7 @@ const getV2LanguageProperty = (): INodeProperties => {
options.push({
name: 'Python (Native) (Beta)',
value: 'pythonNative',
action: 'Code in Python (Native) (Beta)',
});
}

View File

@@ -9,6 +9,7 @@ export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
export const CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code';
export const CODE_NODE_DISPLAY_NAME = 'Code in JavaScript';
export const SET_NODE_NAME = 'Set';
export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields (Set)';
export const LOOP_OVER_ITEMS_NODE_NAME = 'Loop Over Items';

View File

@@ -473,4 +473,9 @@ export class CanvasPage extends BasePage {
await this.page.goto('/workflow/new');
}
async addNodeWithSubItem(searchText: string, subItemText: string): Promise<void> {
await this.addNode(searchText);
await this.nodeCreatorSubItem(subItemText).click();
}
}

View File

@@ -1,4 +1,9 @@
import { MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../../config/constants';
import {
MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
CODE_NODE_NAME,
CODE_NODE_DISPLAY_NAME,
} from '../../config/constants';
import { test, expect } from '../../fixtures/base';
test.describe('Canvas Node Actions', () => {
@@ -52,8 +57,9 @@ test.describe('Canvas Node Actions', () => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.fillNodeCreatorSearchBar('Code');
await n8n.canvas.fillNodeCreatorSearchBar(CODE_NODE_NAME);
await n8n.page.keyboard.press('Enter');
await n8n.canvas.nodeCreatorSubItem(CODE_NODE_DISPLAY_NAME).click();
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
@@ -63,7 +69,11 @@ test.describe('Canvas Node Actions', () => {
test('should add disconnected node when nothing selected', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.deselectAll();
await n8n.canvas.addNode('Code', { closeNDV: true });
await n8n.canvas.clickNodeCreatorPlusButton();
await n8n.canvas.fillNodeCreatorSearchBar(CODE_NODE_NAME);
await n8n.page.keyboard.press('Enter');
await n8n.canvas.nodeCreatorSubItem(CODE_NODE_DISPLAY_NAME).click();
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeConnections()).toHaveCount(0);
});

View File

@@ -3,6 +3,7 @@ import {
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
CODE_NODE_NAME,
HTTP_REQUEST_NODE_NAME,
CODE_NODE_DISPLAY_NAME,
} from '../../config/constants';
import { test, expect } from '../../fixtures/base';
@@ -22,6 +23,7 @@ test.describe('Canvas Actions', () => {
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.fillNodeCreatorSearchBar(CODE_NODE_NAME);
await n8n.page.keyboard.press('Enter');
await n8n.canvas.nodeCreatorSubItem(CODE_NODE_DISPLAY_NAME).click();
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
@@ -32,12 +34,10 @@ test.describe('Canvas Actions', () => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.fillNodeCreatorSearchBar(CODE_NODE_NAME);
const sourceElement = n8n.canvas
.nodeCreatorNodeItems()
.filter({ hasText: CODE_NODE_NAME })
.first();
await sourceElement.dragTo(n8n.canvas.canvasPane(), { targetPosition: { x: 100, y: 100 } });
await n8n.page.keyboard.press('Enter');
await n8n.canvas
.nodeCreatorSubItem(CODE_NODE_DISPLAY_NAME)
.dragTo(n8n.canvas.canvasPane(), { targetPosition: { x: 100, y: 100 } });
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeConnections()).toHaveCount(1);
@@ -61,7 +61,7 @@ test.describe('Canvas Actions', () => {
test('should add disconnected node if nothing is selected', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.deselectAll();
await n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(CODE_NODE_NAME, { action: CODE_NODE_DISPLAY_NAME, closeNDV: true });
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeConnections()).toHaveCount(0);
@@ -70,14 +70,14 @@ test.describe('Canvas Actions', () => {
test('should add node between two connected nodes', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
await n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(CODE_NODE_NAME, { action: CODE_NODE_DISPLAY_NAME, closeNDV: true });
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeConnections()).toHaveCount(1);
await n8n.canvas.addNodeBetweenNodes(
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
CODE_NODE_NAME,
CODE_NODE_DISPLAY_NAME,
HTTP_REQUEST_NODE_NAME,
);
@@ -96,8 +96,11 @@ test.describe('Canvas Actions', () => {
test('should delete connections by clicking on the delete button', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
await n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true });
await n8n.canvas.deleteConnectionBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME);
await n8n.canvas.addNode(CODE_NODE_NAME, { action: CODE_NODE_DISPLAY_NAME, closeNDV: true });
await n8n.canvas.deleteConnectionBetweenNodes(
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
CODE_NODE_DISPLAY_NAME,
);
await expect(n8n.canvas.nodeConnections()).toHaveCount(0);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
@@ -116,9 +119,9 @@ test.describe('Canvas Actions', () => {
test('should disable and enable node', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(CODE_NODE_NAME, { action: CODE_NODE_DISPLAY_NAME, closeNDV: true });
const disableButton = n8n.canvas.nodeDisableButton(CODE_NODE_NAME);
const disableButton = n8n.canvas.nodeDisableButton(CODE_NODE_DISPLAY_NAME);
await disableButton.click();
await expect(n8n.canvas.disabledNodes()).toHaveCount(1);
@@ -130,8 +133,8 @@ test.describe('Canvas Actions', () => {
test('should delete node', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true });
await n8n.canvas.deleteNodeByName(CODE_NODE_NAME);
await n8n.canvas.addNode(CODE_NODE_NAME, { action: CODE_NODE_DISPLAY_NAME, closeNDV: true });
await n8n.canvas.deleteNodeByName(CODE_NODE_DISPLAY_NAME);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
await expect(n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME)).toBeVisible();
@@ -140,9 +143,9 @@ test.describe('Canvas Actions', () => {
test('should copy selected nodes', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(CODE_NODE_NAME, { action: CODE_NODE_DISPLAY_NAME, closeNDV: true });
await n8n.canvasComposer.selectAllAndCopy();
await n8n.canvas.nodeByName(CODE_NODE_NAME).click();
await n8n.canvas.nodeByName(CODE_NODE_DISPLAY_NAME).click();
await n8n.canvasComposer.copySelectedNodesWithToast();
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
@@ -150,7 +153,7 @@ test.describe('Canvas Actions', () => {
test('should select/deselect all nodes', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(CODE_NODE_NAME, { action: CODE_NODE_DISPLAY_NAME, closeNDV: true });
await n8n.canvas.selectAll();
await expect(n8n.canvas.selectedNodes()).toHaveCount(2);
@@ -162,7 +165,7 @@ test.describe('Canvas Actions', () => {
test('should select nodes using arrow keys', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
await n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(CODE_NODE_NAME, { action: CODE_NODE_DISPLAY_NAME, closeNDV: true });
await n8n.canvas.getCanvasNodes().first().waitFor();
await n8n.canvas.navigateNodesWithArrows('left');
@@ -177,7 +180,7 @@ test.describe('Canvas Actions', () => {
test('should select nodes using shift and arrow keys', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
await n8n.canvas.addNode(CODE_NODE_NAME, { closeNDV: true });
await n8n.canvas.addNode(CODE_NODE_NAME, { action: CODE_NODE_DISPLAY_NAME, closeNDV: true });
await n8n.canvas.getCanvasNodes().first().waitFor();
await n8n.canvas.extendSelectionWithArrows('left');

View File

@@ -1,6 +1,10 @@
import { nanoid } from 'nanoid';
import { CODE_NODE_NAME, MANUAL_TRIGGER_NODE_NAME } from '../../config/constants';
import {
CODE_NODE_DISPLAY_NAME,
CODE_NODE_NAME,
MANUAL_TRIGGER_NODE_NAME,
} from '../../config/constants';
import { test, expect } from '../../fixtures/base';
test.describe('Code node', () => {
@@ -9,7 +13,7 @@ test.describe('Code node', () => {
await n8n.goHome();
await n8n.workflows.clickAddWorkflowButton();
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNode(CODE_NODE_NAME);
await n8n.canvas.addNodeWithSubItem(CODE_NODE_NAME, CODE_NODE_DISPLAY_NAME);
});
test('should show correct placeholders switching modes', async ({ n8n }) => {
@@ -53,17 +57,17 @@ test.describe('Code node', () => {
await n8n.ndv.getCodeEditor().fill("console.log('code node 1')");
await n8n.ndv.close();
await n8n.canvas.addNode(CODE_NODE_NAME);
await n8n.canvas.addNodeWithSubItem(CODE_NODE_NAME, CODE_NODE_DISPLAY_NAME);
await n8n.ndv.getCodeEditor().fill("console.log('code node 2')");
await n8n.ndv.close();
await n8n.canvas.openNode(CODE_NODE_NAME);
await n8n.canvas.openNode(CODE_NODE_DISPLAY_NAME);
await n8n.ndv.clickFloatingNode('Code1');
await n8n.ndv.clickFloatingNode(CODE_NODE_DISPLAY_NAME + '1');
await expect(n8n.ndv.getCodeEditor()).toContainText("console.log('code node 2')");
await n8n.ndv.clickFloatingNode('Code');
await n8n.ndv.clickFloatingNode(CODE_NODE_DISPLAY_NAME);
await expect(n8n.ndv.getCodeEditor()).toContainText("console.log('code node 1')");
});
@@ -112,7 +116,10 @@ return []
await n8n.goHome();
await n8n.workflows.clickAddWorkflowButton();
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNode(CODE_NODE_NAME);
await n8n.canvas.clickNodeCreatorPlusButton();
await n8n.canvas.fillNodeCreatorSearchBar(CODE_NODE_NAME);
await n8n.page.keyboard.press('Enter');
await n8n.canvas.clickNodeCreatorItemName(CODE_NODE_DISPLAY_NAME);
});
test('tab should exist if experiment selected and be selectable', async ({ n8n }) => {

View File

@@ -1586,6 +1586,15 @@ function resolveResourceAndOperation(
nodeParameters: INodeParameters,
nodeTypeDescription: INodeTypeDescription,
) {
if (nodeTypeDescription.name === 'n8n-nodes-base.code') {
const language = nodeParameters.language as string;
const langProp = nodeTypeDescription.properties.find((p) => p.name === 'language');
if (langProp?.options && isINodePropertyOptionsList(langProp.options)) {
const found = langProp.options.find((o) => o.value === language);
if (found?.action) return { action: found.action };
}
}
const resource = nodeParameters.resource as string;
const operation = nodeParameters.operation as string;
const nodeTypeOperation = nodeTypeDescription.properties.find(

View File

@@ -5630,6 +5630,44 @@ describe('NodeHelpers', () => {
// Assert
expect(result).toBe('Create user in Test Node');
});
test.each([
['javaScript', 'Code in JavaScript'],
['python', 'Code in Python (Beta)'],
['pythonNative', 'Code in Python (Native) (Beta)'],
])(
'should return action-based name for Code node with %s language',
(language, expectedAction) => {
mockNodeTypeDescription.name = 'n8n-nodes-base.code';
mockNodeTypeDescription.properties = [
{
displayName: 'Language',
name: 'language',
type: 'options',
options: [
{
name: 'JavaScript',
value: 'javaScript',
action: 'Code in JavaScript',
},
{
name: 'Python (Beta)',
value: 'python',
action: 'Code in Python (Beta)',
},
{
name: 'Python (Native) (Beta)',
value: 'pythonNative',
action: 'Code in Python (Native) (Beta)',
},
],
default: 'javaScript',
},
];
const result = makeNodeName({ language }, mockNodeTypeDescription);
expect(result).toBe(expectedAction);
},
);
});
describe('isTool', () => {
it('should return true for a node with AiTool output', () => {