mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat(editor): Change default node names depending on node operation and resource (#15954)
This commit is contained in:
@@ -50,7 +50,7 @@ describe('Inline expression editor', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Get many items');
|
||||||
WorkflowPage.actions.openInlineExpressionEditor();
|
WorkflowPage.actions.openInlineExpressionEditor();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ describe('Inline expression editor', () => {
|
|||||||
WorkflowPage.actions.addNodeToCanvas('No Operation');
|
WorkflowPage.actions.addNodeToCanvas('No Operation');
|
||||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Get many items');
|
||||||
WorkflowPage.actions.openInlineExpressionEditor();
|
WorkflowPage.actions.openInlineExpressionEditor();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ describe('Inline expression editor', () => {
|
|||||||
// Run workflow
|
// Run workflow
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
WorkflowPage.actions.executeNode('No Operation, do nothing', { anchor: 'topLeft' });
|
WorkflowPage.actions.executeNode('No Operation, do nothing', { anchor: 'topLeft' });
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Get many items');
|
||||||
WorkflowPage.actions.openInlineExpressionEditor();
|
WorkflowPage.actions.openInlineExpressionEditor();
|
||||||
|
|
||||||
// Previous nodes have run, input can be resolved
|
// Previous nodes have run, input can be resolved
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
|
|
||||||
workflowPage.actions.openNode('Notion');
|
workflowPage.actions.openNode('Append a block');
|
||||||
ndv.getters.credentialInput().should('have.value', 'Credential C1').should('be.disabled');
|
ndv.getters.credentialInput().should('have.value', 'Credential C1').should('be.disabled');
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
});
|
});
|
||||||
@@ -112,7 +112,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
|
|
||||||
workflowPage.actions.openNode('Notion');
|
workflowPage.actions.openNode('Append a block');
|
||||||
ndv.getters
|
ndv.getters
|
||||||
.credentialInput()
|
.credentialInput()
|
||||||
.find('input')
|
.find('input')
|
||||||
@@ -136,7 +136,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
|
|||||||
cy.visit(workflowsPage.url);
|
cy.visit(workflowsPage.url);
|
||||||
workflowsPage.getters.workflowCards().should('have.length', 2);
|
workflowsPage.getters.workflowCards().should('have.length', 2);
|
||||||
workflowsPage.getters.workflowCardContent('Workflow W1').click();
|
workflowsPage.getters.workflowCardContent('Workflow W1').click();
|
||||||
workflowPage.actions.openNode('Notion');
|
workflowPage.actions.openNode('Append a block');
|
||||||
ndv.getters
|
ndv.getters
|
||||||
.credentialInput()
|
.credentialInput()
|
||||||
.find('input')
|
.find('input')
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||||||
expect(interception.request.query).not.to.have.property('projectId');
|
expect(interception.request.query).not.to.have.property('projectId');
|
||||||
expect(interception.request.query).to.have.property('workflowId');
|
expect(interception.request.query).to.have.property('workflowId');
|
||||||
});
|
});
|
||||||
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
|
workflowPage.getters.canvasNodeByName('Append a block').should('be.visible').dblclick();
|
||||||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||||
getVisibleSelect()
|
getVisibleSelect()
|
||||||
.find('li')
|
.find('li')
|
||||||
@@ -182,7 +182,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
|
|
||||||
cy.reload();
|
cy.reload();
|
||||||
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
|
workflowPage.getters.canvasNodeByName('Append a block').should('be.visible').dblclick();
|
||||||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||||
getVisibleSelect()
|
getVisibleSelect()
|
||||||
.find('li')
|
.find('li')
|
||||||
@@ -211,7 +211,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
|
|
||||||
cy.reload();
|
cy.reload();
|
||||||
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
|
workflowPage.getters.canvasNodeByName('Append a block').should('be.visible').dblclick();
|
||||||
workflowPage.getters.nodeCredentialsSelect().first().click();
|
workflowPage.getters.nodeCredentialsSelect().first().click();
|
||||||
getVisibleSelect()
|
getVisibleSelect()
|
||||||
.find('li')
|
.find('li')
|
||||||
@@ -280,7 +280,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
|
|||||||
workflowsPage.getters.workflowCards().first().findChildByTestId('card-content').click();
|
workflowsPage.getters.workflowCards().first().findChildByTestId('card-content').click();
|
||||||
|
|
||||||
// Check if the credential can be changed
|
// Check if the credential can be changed
|
||||||
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
|
workflowPage.getters.canvasNodeByName('Append a block').should('be.visible').dblclick();
|
||||||
ndv.getters.credentialInput().find('input').should('be.enabled');
|
ndv.getters.credentialInput().find('input').should('be.enabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ describe('Node Creator', () => {
|
|||||||
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
|
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
|
||||||
NDVModal.actions.close();
|
NDVModal.actions.close();
|
||||||
WorkflowPage.actions.deleteNode('When clicking ‘Execute workflow’');
|
WorkflowPage.actions.deleteNode('When clicking ‘Execute workflow’');
|
||||||
WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click();
|
WorkflowPage.getters.canvasNodePlusEndpointByName('Create a credential').click();
|
||||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
|
||||||
nodeCreatorFeature.getters.getCreatorItem('n8n').click();
|
nodeCreatorFeature.getters.getCreatorItem('n8n').click();
|
||||||
nodeCreatorFeature.getters.getCategoryItem('Actions').click();
|
nodeCreatorFeature.getters.getCategoryItem('Actions').click();
|
||||||
@@ -324,7 +324,11 @@ describe('Node Creator', () => {
|
|||||||
NDVModal.actions.close();
|
NDVModal.actions.close();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.addNodeBetweenNodes('n8n', 'n8n1', 'Summarize');
|
WorkflowPage.actions.addNodeBetweenNodes(
|
||||||
|
'Create a credential',
|
||||||
|
'Create a credential1',
|
||||||
|
'Summarize',
|
||||||
|
);
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ describe('Editors', () => {
|
|||||||
.type('SELECT * FROM `testTable`', { delay: TYPING_DELAY })
|
.type('SELECT * FROM `testTable`', { delay: TYPING_DELAY })
|
||||||
.type('{esc}');
|
.type('{esc}');
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
workflowPage.actions.openNode('Postgres');
|
workflowPage.actions.openNode('Execute a SQL query');
|
||||||
ndv.getters
|
ndv.getters
|
||||||
.sqlEditorContainer()
|
.sqlEditorContainer()
|
||||||
.find('.cm-content')
|
.find('.cm-content')
|
||||||
.type('{end} LIMIT 10', { delay: TYPING_DELAY })
|
.type('{end} LIMIT 10', { delay: TYPING_DELAY })
|
||||||
.type('{esc}');
|
.type('{esc}');
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
workflowPage.actions.openNode('Postgres');
|
workflowPage.actions.openNode('Execute a SQL query');
|
||||||
ndv.getters.sqlEditorContainer().should('contain', 'SELECT * FROM `testTable` LIMIT 10');
|
ndv.getters.sqlEditorContainer().should('contain', 'SELECT * FROM `testTable` LIMIT 10');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ describe('Editors', () => {
|
|||||||
ndv.actions.setPinnedData([{ table: 'test_table' }]);
|
ndv.actions.setPinnedData([{ table: 'test_table' }]);
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
|
|
||||||
workflowPage.actions.openNode('MySQL');
|
workflowPage.actions.openNode('Execute a SQL query');
|
||||||
ndv.getters
|
ndv.getters
|
||||||
.sqlEditorContainer()
|
.sqlEditorContainer()
|
||||||
.find('.cm-content')
|
.find('.cm-content')
|
||||||
@@ -86,7 +86,7 @@ describe('Editors', () => {
|
|||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
workflowPage.getters.isWorkflowSaved();
|
workflowPage.getters.isWorkflowSaved();
|
||||||
workflowPage.actions.openNode('Postgres');
|
workflowPage.actions.openNode('Execute a SQL query');
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
// Workflow should still be saved
|
// Workflow should still be saved
|
||||||
workflowPage.getters.isWorkflowSaved();
|
workflowPage.getters.isWorkflowSaved();
|
||||||
@@ -100,7 +100,7 @@ describe('Editors', () => {
|
|||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
workflowPage.actions.saveWorkflowOnButtonClick();
|
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
workflowPage.getters.isWorkflowSaved();
|
workflowPage.getters.isWorkflowSaved();
|
||||||
workflowPage.actions.openNode('Postgres');
|
workflowPage.actions.openNode('Execute a SQL query');
|
||||||
ndv.getters
|
ndv.getters
|
||||||
.sqlEditorContainer()
|
.sqlEditorContainer()
|
||||||
.click()
|
.click()
|
||||||
@@ -131,14 +131,14 @@ describe('Editors', () => {
|
|||||||
.paste('SELECT * FROM `secondTable`');
|
.paste('SELECT * FROM `secondTable`');
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
|
|
||||||
workflowPage.actions.openNode('Postgres');
|
workflowPage.actions.openNode('Execute a SQL query');
|
||||||
ndv.actions.clickFloatingNode('Postgres1');
|
ndv.actions.clickFloatingNode('Execute a SQL query1');
|
||||||
ndv.getters
|
ndv.getters
|
||||||
.sqlEditorContainer()
|
.sqlEditorContainer()
|
||||||
.find('.cm-content')
|
.find('.cm-content')
|
||||||
.should('have.text', 'SELECT * FROM `secondTable`');
|
.should('have.text', 'SELECT * FROM `secondTable`');
|
||||||
|
|
||||||
ndv.actions.clickFloatingNode('Postgres');
|
ndv.actions.clickFloatingNode('Execute a SQL query');
|
||||||
ndv.getters
|
ndv.getters
|
||||||
.sqlEditorContainer()
|
.sqlEditorContainer()
|
||||||
.find('.cm-content')
|
.find('.cm-content')
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ describe('AI Assistant Credential Help', () => {
|
|||||||
}).as('chatRequest');
|
}).as('chatRequest');
|
||||||
wf.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
wf.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
wf.actions.addNodeToCanvas(GMAIL_NODE_NAME);
|
wf.actions.addNodeToCanvas(GMAIL_NODE_NAME);
|
||||||
wf.actions.openNode('Gmail');
|
wf.actions.openNode('Add label to message');
|
||||||
openCredentialSelect();
|
openCredentialSelect();
|
||||||
clickCreateNewCredential();
|
clickCreateNewCredential();
|
||||||
aiAssistant.getters.credentialEditAssistantButton().find('button').should('be.visible');
|
aiAssistant.getters.credentialEditAssistantButton().find('button').should('be.visible');
|
||||||
|
|||||||
@@ -86,18 +86,11 @@ describe('NDV', () => {
|
|||||||
cy.get('[class*=hasIssues]').should('have.length', 1);
|
cy.get('[class*=hasIssues]').should('have.length', 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show validation errors only after blur or re-opening of NDV', () => {
|
it('should show validation errors only after blur or re-opening of NDV of a node with operation and resource', () => {
|
||||||
workflowPage.actions.addNodeToCanvas('Manual');
|
workflowPage.actions.addNodeToCanvas('Manual');
|
||||||
workflowPage.actions.addNodeToCanvas('Airtable', true, true, 'Search records');
|
workflowPage.actions.addNodeToCanvas('Airtable', true, true, 'Search records');
|
||||||
ndv.getters.container().should('be.visible');
|
ndv.getters.container().should('be.visible');
|
||||||
cy.get('.has-issues').should('have.length', 0);
|
|
||||||
ndv.getters.parameterInput('table').find('input').eq(1).focus().blur();
|
|
||||||
ndv.getters.parameterInput('base').find('input').eq(1).focus().blur();
|
|
||||||
cy.get('.has-issues').should('have.length', 2);
|
cy.get('.has-issues').should('have.length', 2);
|
||||||
ndv.getters.backToCanvas().click();
|
|
||||||
workflowPage.actions.openNode('Airtable');
|
|
||||||
cy.get('.has-issues').should('have.length', 2);
|
|
||||||
cy.get('[class*=hasIssues]').should('have.length', 1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Correctly failing in V2 - node issues are only shows after execution
|
// Correctly failing in V2 - node issues are only shows after execution
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe('Expression editor modal', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Get many items');
|
||||||
WorkflowPage.actions.openExpressionEditorModal();
|
WorkflowPage.actions.openExpressionEditorModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ describe('Expression editor modal', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Get many items');
|
||||||
WorkflowPage.actions.openExpressionEditorModal();
|
WorkflowPage.actions.openExpressionEditorModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ describe('Expression editor modal', () => {
|
|||||||
WorkflowPage.actions.addNodeToCanvas('No Operation');
|
WorkflowPage.actions.addNodeToCanvas('No Operation');
|
||||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Get many items');
|
||||||
WorkflowPage.actions.openExpressionEditorModal();
|
WorkflowPage.actions.openExpressionEditorModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ describe('Expression editor modal', () => {
|
|||||||
cy.get('body').type('{esc}');
|
cy.get('body').type('{esc}');
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
WorkflowPage.actions.executeNode('No Operation, do nothing', { anchor: 'topLeft' });
|
WorkflowPage.actions.executeNode('No Operation, do nothing', { anchor: 'topLeft' });
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Get many items');
|
||||||
WorkflowPage.actions.openExpressionEditorModal();
|
WorkflowPage.actions.openExpressionEditorModal();
|
||||||
|
|
||||||
// Previous nodes have run, input can be resolved
|
// Previous nodes have run, input can be resolved
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export class WorkflowPage extends BasePage {
|
|||||||
cy.get('body').then((body) => {
|
cy.get('body').then((body) => {
|
||||||
if (body.find('[data-test-id=node-creator]').length > 0) {
|
if (body.find('[data-test-id=node-creator]').length > 0) {
|
||||||
if (action) {
|
if (action) {
|
||||||
cy.contains(action).click();
|
cy.get('[data-keyboard-nav-type="action"]').contains(action).click();
|
||||||
} else {
|
} else {
|
||||||
// Select the first action
|
// Select the first action
|
||||||
if (body.find('[data-keyboard-nav-type="action"]').length > 0) {
|
if (body.find('[data-keyboard-nav-type="action"]').length > 0) {
|
||||||
|
|||||||
@@ -562,6 +562,11 @@ describe('ManualExecutionService', () => {
|
|||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
getTriggerNodes: jest.fn().mockReturnValue([determinedStartNode]),
|
getTriggerNodes: jest.fn().mockReturnValue([determinedStartNode]),
|
||||||
|
nodeTypes: {
|
||||||
|
getByNameAndVersion: jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ description: { name: '', outputs: [] } }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
jest
|
jest
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import {
|
|||||||
filterDisabledNodes,
|
filterDisabledNodes,
|
||||||
recreateNodeExecutionStack,
|
recreateNodeExecutionStack,
|
||||||
WorkflowExecute,
|
WorkflowExecute,
|
||||||
isTool,
|
|
||||||
rewireGraph,
|
rewireGraph,
|
||||||
} from 'n8n-core';
|
} from 'n8n-core';
|
||||||
import { MANUAL_TRIGGER_NODE_TYPE } from 'n8n-workflow';
|
import { MANUAL_TRIGGER_NODE_TYPE, NodeHelpers } from 'n8n-workflow';
|
||||||
import type {
|
import type {
|
||||||
IExecuteData,
|
IExecuteData,
|
||||||
IPinData,
|
IPinData,
|
||||||
@@ -136,8 +135,12 @@ export class ManualExecutionService {
|
|||||||
`Could not find a node named "${data.destinationNode}" in the workflow.`,
|
`Could not find a node named "${data.destinationNode}" in the workflow.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const destinationNodeType = workflow.nodeTypes.getByNameAndVersion(
|
||||||
|
destinationNode.type,
|
||||||
|
destinationNode.typeVersion,
|
||||||
|
);
|
||||||
// Rewire graph to be able to execute the destination tool node
|
// Rewire graph to be able to execute the destination tool node
|
||||||
if (isTool(destinationNode, workflow.nodeTypes)) {
|
if (NodeHelpers.isTool(destinationNodeType.description, destinationNode.parameters)) {
|
||||||
const graph = rewireGraph(
|
const graph = rewireGraph(
|
||||||
destinationNode,
|
destinationNode,
|
||||||
DirectedGraph.fromWorkflow(workflow),
|
DirectedGraph.fromWorkflow(workflow),
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import { mock } from 'jest-mock-extended';
|
|
||||||
import { type INode, type INodeTypes, NodeConnectionTypes } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import { isTool } from '../is-tool';
|
|
||||||
|
|
||||||
describe('isTool', () => {
|
|
||||||
const mockNode = mock<INode>({
|
|
||||||
id: '1',
|
|
||||||
type: 'n8n-nodes-base.openAi',
|
|
||||||
typeVersion: 1,
|
|
||||||
parameters: {},
|
|
||||||
});
|
|
||||||
const mockNodeTypes = mock<INodeTypes>();
|
|
||||||
|
|
||||||
it('should return true for a node with AiTool output', () => {
|
|
||||||
mockNodeTypes.getByNameAndVersion.mockReturnValue({
|
|
||||||
description: {
|
|
||||||
outputs: [NodeConnectionTypes.AiTool],
|
|
||||||
version: 0,
|
|
||||||
defaults: {
|
|
||||||
name: '',
|
|
||||||
color: '',
|
|
||||||
},
|
|
||||||
inputs: [NodeConnectionTypes.Main],
|
|
||||||
properties: [],
|
|
||||||
displayName: '',
|
|
||||||
name: '',
|
|
||||||
group: [],
|
|
||||||
description: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result = isTool(mockNode, mockNodeTypes);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true for a node with AiTool output in NodeOutputConfiguration', () => {
|
|
||||||
mockNodeTypes.getByNameAndVersion.mockReturnValue({
|
|
||||||
description: {
|
|
||||||
outputs: [{ type: NodeConnectionTypes.AiTool }, { type: NodeConnectionTypes.Main }],
|
|
||||||
version: 0,
|
|
||||||
defaults: {
|
|
||||||
name: '',
|
|
||||||
color: '',
|
|
||||||
},
|
|
||||||
inputs: [NodeConnectionTypes.Main],
|
|
||||||
properties: [],
|
|
||||||
displayName: '',
|
|
||||||
name: '',
|
|
||||||
group: [],
|
|
||||||
description: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result = isTool(mockNode, mockNodeTypes);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns true for a vectore store node in retrieve-as-tool mode', () => {
|
|
||||||
mockNode.type = 'n8n-nodes-base.vectorStore';
|
|
||||||
mockNode.parameters = { mode: 'retrieve-as-tool' };
|
|
||||||
mockNodeTypes.getByNameAndVersion.mockReturnValue({
|
|
||||||
description: {
|
|
||||||
outputs: [NodeConnectionTypes.Main],
|
|
||||||
version: 0,
|
|
||||||
defaults: {
|
|
||||||
name: '',
|
|
||||||
color: '',
|
|
||||||
},
|
|
||||||
inputs: [NodeConnectionTypes.Main],
|
|
||||||
properties: [],
|
|
||||||
displayName: '',
|
|
||||||
name: '',
|
|
||||||
group: [],
|
|
||||||
description: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result = isTool(mockNode, mockNodeTypes);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns false for node with no AiTool output', () => {
|
|
||||||
mockNode.type = 'n8n-nodes-base.someTool';
|
|
||||||
mockNode.parameters = {};
|
|
||||||
mockNodeTypes.getByNameAndVersion.mockReturnValue({
|
|
||||||
description: {
|
|
||||||
outputs: [NodeConnectionTypes.Main],
|
|
||||||
version: 0,
|
|
||||||
defaults: {
|
|
||||||
name: '',
|
|
||||||
color: '',
|
|
||||||
},
|
|
||||||
inputs: [NodeConnectionTypes.Main],
|
|
||||||
properties: [],
|
|
||||||
displayName: '',
|
|
||||||
name: '',
|
|
||||||
group: [],
|
|
||||||
description: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const result = isTool(mockNode, mockNodeTypes);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -6,6 +6,5 @@ export { recreateNodeExecutionStack } from './recreate-node-execution-stack';
|
|||||||
export { cleanRunData } from './clean-run-data';
|
export { cleanRunData } from './clean-run-data';
|
||||||
export { handleCycles } from './handle-cycles';
|
export { handleCycles } from './handle-cycles';
|
||||||
export { filterDisabledNodes } from './filter-disabled-nodes';
|
export { filterDisabledNodes } from './filter-disabled-nodes';
|
||||||
export { isTool } from './is-tool';
|
|
||||||
export { rewireGraph } from './rewire-graph';
|
export { rewireGraph } from './rewire-graph';
|
||||||
export { getNextExecutionIndex } from './run-data-utils';
|
export { getNextExecutionIndex } from './run-data-utils';
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { type INode, type INodeTypes, NodeConnectionTypes } from 'n8n-workflow';
|
|
||||||
|
|
||||||
export function isTool(node: INode, nodeTypes: INodeTypes) {
|
|
||||||
const type = nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
|
||||||
|
|
||||||
// Check if node is a vector store in retrieve-as-tool mode
|
|
||||||
if (node.type.includes('vectorStore')) {
|
|
||||||
const mode = node.parameters?.mode;
|
|
||||||
return mode === 'retrieve-as-tool';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for other tool nodes
|
|
||||||
for (const output of type.description.outputs) {
|
|
||||||
if (typeof output === 'string') {
|
|
||||||
return output === NodeConnectionTypes.AiTool;
|
|
||||||
} else if (output?.type && output.type === NodeConnectionTypes.AiTool) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -72,7 +72,6 @@ import {
|
|||||||
handleCycles,
|
handleCycles,
|
||||||
filterDisabledNodes,
|
filterDisabledNodes,
|
||||||
rewireGraph,
|
rewireGraph,
|
||||||
isTool,
|
|
||||||
getNextExecutionIndex,
|
getNextExecutionIndex,
|
||||||
} from './partial-execution-utils';
|
} from './partial-execution-utils';
|
||||||
import { TOOL_EXECUTOR_NODE_NAME } from './partial-execution-utils/rewire-graph';
|
import { TOOL_EXECUTOR_NODE_NAME } from './partial-execution-utils/rewire-graph';
|
||||||
@@ -368,8 +367,12 @@ export class WorkflowExecute {
|
|||||||
|
|
||||||
let graph = DirectedGraph.fromWorkflow(workflow);
|
let graph = DirectedGraph.fromWorkflow(workflow);
|
||||||
|
|
||||||
|
const destinationNodeType = workflow.nodeTypes.getByNameAndVersion(
|
||||||
|
destination.type,
|
||||||
|
destination.typeVersion,
|
||||||
|
);
|
||||||
// Partial execution of nodes as tools
|
// Partial execution of nodes as tools
|
||||||
if (isTool(destination, workflow.nodeTypes)) {
|
if (NodeHelpers.isTool(destinationNodeType.description, destination.parameters)) {
|
||||||
graph = rewireGraph(destination, graph, agentRequest);
|
graph = rewireGraph(destination, graph, agentRequest);
|
||||||
workflow = graph.toWorkflow({ ...workflow });
|
workflow = graph.toWorkflow({ ...workflow });
|
||||||
// Rewire destination node to the virtual agent
|
// Rewire destination node to the virtual agent
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { computed } from 'vue';
|
|||||||
import {
|
import {
|
||||||
CHAIN_LLM_LANGCHAIN_NODE_TYPE,
|
CHAIN_LLM_LANGCHAIN_NODE_TYPE,
|
||||||
NodeConnectionTypes,
|
NodeConnectionTypes,
|
||||||
|
NodeHelpers,
|
||||||
type IDataObject,
|
type IDataObject,
|
||||||
type INodeParameters,
|
type INodeParameters,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
@@ -43,11 +44,14 @@ import { useExternalHooks } from '@/composables/useExternalHooks';
|
|||||||
import { sortNodeCreateElements, transformNodeType } from '../utils';
|
import { sortNodeCreateElements, transformNodeType } from '../utils';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { useCanvasStore } from '@/stores/canvas.store';
|
import { useCanvasStore } from '@/stores/canvas.store';
|
||||||
|
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||||
|
import findLast from 'lodash/findLast';
|
||||||
|
|
||||||
export const useActions = () => {
|
export const useActions = () => {
|
||||||
const nodeCreatorStore = useNodeCreatorStore();
|
const nodeCreatorStore = useNodeCreatorStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const canvasOperations = useCanvasOperations();
|
||||||
const singleNodeOpenSources = [
|
const singleNodeOpenSources = [
|
||||||
NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT,
|
NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT,
|
||||||
NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION,
|
NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION,
|
||||||
@@ -305,7 +309,7 @@ export const useActions = () => {
|
|||||||
return { nodes, connections };
|
return { nodes, connections };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hook into addNode action to set the last node parameters & track the action selected
|
// Hook into addNode action to set the last node parameters, adjust default name and track the action selected
|
||||||
function setAddedNodeActionParameters(
|
function setAddedNodeActionParameters(
|
||||||
action: IUpdateInformation,
|
action: IUpdateInformation,
|
||||||
telemetry?: Telemetry,
|
telemetry?: Telemetry,
|
||||||
@@ -313,10 +317,25 @@ export const useActions = () => {
|
|||||||
) {
|
) {
|
||||||
const { $onAction: onWorkflowStoreAction } = useWorkflowsStore();
|
const { $onAction: onWorkflowStoreAction } = useWorkflowsStore();
|
||||||
const storeWatcher = onWorkflowStoreAction(
|
const storeWatcher = onWorkflowStoreAction(
|
||||||
({ name, after, store: { setLastNodeParameters }, args }) => {
|
({ name, after, store: { setLastNodeParameters, allNodes }, args }) => {
|
||||||
if (name !== 'addNode' || args[0].type !== action.key) return;
|
if (name !== 'addNode' || args[0].type !== action.key) return;
|
||||||
after(() => {
|
after(() => {
|
||||||
|
const node = findLast(allNodes, (n) => n.type === action.key);
|
||||||
|
const nodeType = node && nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||||
|
const wasDefaultName =
|
||||||
|
nodeType && NodeHelpers.isDefaultNodeName(node.name, nodeType, node.parameters ?? {});
|
||||||
|
|
||||||
setLastNodeParameters(action);
|
setLastNodeParameters(action);
|
||||||
|
|
||||||
|
// We update the default name here based on the chosen resource and operation
|
||||||
|
if (wasDefaultName) {
|
||||||
|
const newName = NodeHelpers.makeNodeName(node.parameters, nodeType);
|
||||||
|
// Account for unique-ified nodes with `<name><digit>`
|
||||||
|
if (!node.name.startsWith(newName)) {
|
||||||
|
// setTimeout to allow remaining events trigger by node addition to finish
|
||||||
|
setTimeout(async () => await canvasOperations.renameNode(node.name, newName));
|
||||||
|
}
|
||||||
|
}
|
||||||
if (telemetry) trackActionSelected(action, telemetry, rootView);
|
if (telemetry) trackActionSelected(action, telemetry, rootView);
|
||||||
// Unsubscribe from the store watcher
|
// Unsubscribe from the store watcher
|
||||||
storeWatcher();
|
storeWatcher();
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
|||||||
import { ProjectTypes } from '@/types/projects.types';
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
import { updateDynamicConnections } from '@/utils/nodeSettingsUtils';
|
import { updateDynamicConnections } from '@/utils/nodeSettingsUtils';
|
||||||
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue';
|
||||||
|
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -96,6 +97,7 @@ const telemetry = useTelemetry();
|
|||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
const canvasOperations = useCanvasOperations();
|
||||||
|
|
||||||
const nodeValid = ref(true);
|
const nodeValid = ref(true);
|
||||||
const openPanel = ref<'params' | 'settings'>('params');
|
const openPanel = ref<'params' | 'settings'>('params');
|
||||||
@@ -579,6 +581,15 @@ const valueChanged = (parameterData: IUpdateInformation) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (NodeHelpers.isDefaultNodeName(_node.name, nodeType, node.value?.parameters ?? {})) {
|
||||||
|
const newName = NodeHelpers.makeNodeName(nodeParameters ?? {}, nodeType);
|
||||||
|
// Account for unique-ified nodes with `<name><digit>`
|
||||||
|
if (!_node.name.startsWith(newName)) {
|
||||||
|
// We need a timeout here to support events reacting to the valueChange based on node names
|
||||||
|
setTimeout(async () => await canvasOperations.renameNode(_node.name, newName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const key of Object.keys(nodeParameters as object)) {
|
for (const key of Object.keys(nodeParameters as object)) {
|
||||||
if (nodeParameters && nodeParameters[key] !== null && nodeParameters[key] !== undefined) {
|
if (nodeParameters && nodeParameters[key] !== null && nodeParameters[key] !== undefined) {
|
||||||
setValue(`parameters.${key}`, nodeParameters[key] as string);
|
setValue(`parameters.${key}`, nodeParameters[key] as string);
|
||||||
|
|||||||
@@ -910,7 +910,10 @@ export function useCanvasOperations() {
|
|||||||
options: { viewport?: ViewportBoundaries; forcePosition?: boolean } = {},
|
options: { viewport?: ViewportBoundaries; forcePosition?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
const id = node.id ?? nodeHelpers.assignNodeId(node as INodeUi);
|
const id = node.id ?? nodeHelpers.assignNodeId(node as INodeUi);
|
||||||
const name = node.name ?? (nodeTypeDescription.defaults.name as string);
|
const name =
|
||||||
|
node.name ??
|
||||||
|
nodeHelpers.getDefaultNodeName(node) ??
|
||||||
|
(nodeTypeDescription.defaults.name as string);
|
||||||
const type = nodeTypeDescription.name;
|
const type = nodeTypeDescription.name;
|
||||||
const typeVersion = node.typeVersion;
|
const typeVersion = node.typeVersion;
|
||||||
const position =
|
const position =
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import type {
|
|||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
AddedNode,
|
||||||
ICredentialsResponse,
|
ICredentialsResponse,
|
||||||
INodeUi,
|
INodeUi,
|
||||||
INodeUpdatePropertiesInformation,
|
INodeUpdatePropertiesInformation,
|
||||||
@@ -986,6 +987,21 @@ export function useNodeHelpers() {
|
|||||||
return nodeIssues;
|
return nodeIssues;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDefaultNodeName(node: AddedNode | INode) {
|
||||||
|
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||||
|
if (nodeType === null) return null;
|
||||||
|
const parameters = NodeHelpers.getNodeParameters(
|
||||||
|
nodeType?.properties,
|
||||||
|
node.parameters ?? {},
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
node.typeVersion ? { typeVersion: node.typeVersion } : null,
|
||||||
|
nodeType,
|
||||||
|
);
|
||||||
|
|
||||||
|
return NodeHelpers.makeNodeName(parameters ?? {}, nodeType);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasProxyAuth,
|
hasProxyAuth,
|
||||||
isCustomApiCallSelected,
|
isCustomApiCallSelected,
|
||||||
@@ -1019,5 +1035,6 @@ export function useNodeHelpers() {
|
|||||||
isSingleExecution,
|
isSingleExecution,
|
||||||
getNodeHints,
|
getNodeHints,
|
||||||
nodeIssuesToString,
|
nodeIssuesToString,
|
||||||
|
getDefaultNodeName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1570,6 +1570,35 @@ export function isNodeWithWorkflowSelector(node: INode) {
|
|||||||
return [EXECUTE_WORKFLOW_NODE_TYPE, WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE].includes(node.type);
|
return [EXECUTE_WORKFLOW_NODE_TYPE, WORKFLOW_TOOL_LANGCHAIN_NODE_TYPE].includes(node.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns An object containing either the resolved operation's action if available,
|
||||||
|
* else the resource and operation if both exist.
|
||||||
|
* If neither can be resolved, returns an empty object.
|
||||||
|
*/
|
||||||
|
function resolveResourceAndOperation(
|
||||||
|
nodeParameters: INodeParameters,
|
||||||
|
nodeTypeDescription: INodeTypeDescription,
|
||||||
|
) {
|
||||||
|
const resource = nodeParameters.resource as string;
|
||||||
|
const operation = nodeParameters.operation as string;
|
||||||
|
const nodeTypeOperation = nodeTypeDescription.properties.find(
|
||||||
|
(p) => p.name === 'operation' && p.displayOptions?.show?.resource?.includes(resource),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nodeTypeOperation?.options && isINodePropertyOptionsList(nodeTypeOperation.options)) {
|
||||||
|
const foundOperation = nodeTypeOperation.options.find((option) => option.value === operation);
|
||||||
|
if (foundOperation?.action) {
|
||||||
|
return { action: foundOperation.action };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource && operation) {
|
||||||
|
return { operation, resource };
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a human-readable description for a node based on its parameters and type definition.
|
* Generates a human-readable description for a node based on its parameters and type definition.
|
||||||
*
|
*
|
||||||
@@ -1585,28 +1614,93 @@ export function makeDescription(
|
|||||||
nodeParameters: INodeParameters,
|
nodeParameters: INodeParameters,
|
||||||
nodeTypeDescription: INodeTypeDescription,
|
nodeTypeDescription: INodeTypeDescription,
|
||||||
): string {
|
): string {
|
||||||
let description = '';
|
const { action, operation, resource } = resolveResourceAndOperation(
|
||||||
const resource = nodeParameters.resource as string;
|
nodeParameters,
|
||||||
const operation = nodeParameters.operation as string;
|
nodeTypeDescription,
|
||||||
const nodeTypeOperation = nodeTypeDescription.properties.find(
|
|
||||||
(p) => p.name === 'operation' && p.displayOptions?.show?.resource?.includes(resource),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (nodeTypeOperation?.options && isINodePropertyOptionsList(nodeTypeOperation.options)) {
|
if (action) {
|
||||||
const foundOperation = nodeTypeOperation.options.find((option) => option.value === operation);
|
return `${action} in ${nodeTypeDescription.defaults.name}`;
|
||||||
if (foundOperation?.action) {
|
}
|
||||||
description = `${foundOperation.action} in ${nodeTypeDescription.defaults.name}`;
|
|
||||||
return description;
|
if (resource && operation) {
|
||||||
|
return `${operation} ${resource} in ${nodeTypeDescription.defaults.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeTypeDescription.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTool(
|
||||||
|
nodeTypeDescription: INodeTypeDescription,
|
||||||
|
parameters: INodeParameters,
|
||||||
|
): boolean {
|
||||||
|
// Check if node is a vector store in retrieve-as-tool mode
|
||||||
|
if (nodeTypeDescription.name.includes('vectorStore')) {
|
||||||
|
const mode = parameters.mode;
|
||||||
|
return mode === 'retrieve-as-tool';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for other tool nodes
|
||||||
|
for (const output of nodeTypeDescription.outputs) {
|
||||||
|
if (typeof output === 'string') {
|
||||||
|
return output === NodeConnectionTypes.AiTool;
|
||||||
|
} else if (output?.type && output.type === NodeConnectionTypes.AiTool) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!description && resource && operation) {
|
return false;
|
||||||
description = `${operation} ${resource} in ${nodeTypeDescription.defaults.name}`;
|
}
|
||||||
} else {
|
|
||||||
description = nodeTypeDescription.description;
|
/**
|
||||||
|
* Generates a resource and operation aware node name.
|
||||||
|
*
|
||||||
|
* Appends `in {nodeTypeDisplayName}` if nodeType is a tool
|
||||||
|
*
|
||||||
|
* 1. "{action}" if the operation has a defined action
|
||||||
|
* 2. "{operation} {resource}" if resource and operation exist
|
||||||
|
* 3. The node type's defaults.name field or displayName as a fallback
|
||||||
|
*/
|
||||||
|
export function makeNodeName(
|
||||||
|
nodeParameters: INodeParameters,
|
||||||
|
nodeTypeDescription: INodeTypeDescription,
|
||||||
|
): string {
|
||||||
|
const { action, operation, resource } = resolveResourceAndOperation(
|
||||||
|
nodeParameters,
|
||||||
|
nodeTypeDescription,
|
||||||
|
);
|
||||||
|
|
||||||
|
const postfix = isTool(nodeTypeDescription, nodeParameters)
|
||||||
|
? ` in ${nodeTypeDescription.defaults.name}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
return `${action}${postfix}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return description;
|
if (resource && operation) {
|
||||||
|
const operationProper = operation[0].toUpperCase() + operation.slice(1);
|
||||||
|
return `${operationProper} ${resource}${postfix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeTypeDescription.defaults.name ?? nodeTypeDescription.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the node name is of format `<defaultNodeName>\d*` , which includes auto-renamed nodes
|
||||||
|
*/
|
||||||
|
export function isDefaultNodeName(
|
||||||
|
name: string,
|
||||||
|
nodeType: INodeTypeDescription,
|
||||||
|
parameters: INodeParameters,
|
||||||
|
): boolean {
|
||||||
|
const legacyDefaultName = nodeType.defaults.name ?? nodeType.displayName;
|
||||||
|
const currentDefaultName = makeNodeName(parameters, nodeType);
|
||||||
|
for (const defaultName of [legacyDefaultName, currentDefaultName]) {
|
||||||
|
if (name.startsWith(defaultName) && /^\d*$/.test(name.slice(defaultName.length))) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ import {
|
|||||||
makeDescription,
|
makeDescription,
|
||||||
getUpdatedToolDescription,
|
getUpdatedToolDescription,
|
||||||
getToolDescriptionForNode,
|
getToolDescriptionForNode,
|
||||||
|
isDefaultNodeName,
|
||||||
|
makeNodeName,
|
||||||
|
isTool,
|
||||||
} from '@/node-helpers';
|
} from '@/node-helpers';
|
||||||
import type { Workflow } from '@/workflow';
|
import type { Workflow } from '@/workflow';
|
||||||
|
|
||||||
@@ -5246,4 +5249,366 @@ describe('NodeHelpers', () => {
|
|||||||
expect(result).toBe('This is the default node description');
|
expect(result).toBe('This is the default node description');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('isDefaultNodeName', () => {
|
||||||
|
let mockNodeTypeDescription: INodeTypeDescription;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Arrange a basic mock node type description
|
||||||
|
mockNodeTypeDescription = {
|
||||||
|
displayName: 'Test Node',
|
||||||
|
name: 'testNode',
|
||||||
|
icon: 'fa:test',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'This is a test node',
|
||||||
|
defaults: {
|
||||||
|
name: 'Test Node',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
properties: [],
|
||||||
|
usableAsTool: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['Create a new user', true],
|
||||||
|
['Test Node', true],
|
||||||
|
['Test Node1', true],
|
||||||
|
['Create a new user5', true],
|
||||||
|
['Create a new user in Test Node5', false],
|
||||||
|
['Create a new user 5', false],
|
||||||
|
['Update user', false],
|
||||||
|
['Update user5', false],
|
||||||
|
['TestNode', false],
|
||||||
|
])('should detect default names for input %s', (input, expected) => {
|
||||||
|
// Arrange
|
||||||
|
const name = input;
|
||||||
|
|
||||||
|
mockNodeTypeDescription.properties = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['user'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
action: 'Create a new user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Update',
|
||||||
|
value: 'update',
|
||||||
|
action: 'Update a new user',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'create',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const parameters: INodeParameters = {
|
||||||
|
descriptionType: 'manual',
|
||||||
|
resource: 'user',
|
||||||
|
operation: 'create',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = isDefaultNodeName(name, mockNodeTypeDescription, parameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
});
|
||||||
|
it('should detect default names for tool node types', () => {
|
||||||
|
// Arrange
|
||||||
|
const name = 'Create user in Test Node';
|
||||||
|
mockNodeTypeDescription.outputs = [NodeConnectionTypes.AiTool];
|
||||||
|
|
||||||
|
const parameters: INodeParameters = {
|
||||||
|
resource: 'user',
|
||||||
|
operation: 'create',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = isDefaultNodeName(name, mockNodeTypeDescription, parameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
it('should detect non-default names for tool node types', () => {
|
||||||
|
// Arrange
|
||||||
|
// The default for tools would include ` in Test Node`
|
||||||
|
const name = 'Create user';
|
||||||
|
mockNodeTypeDescription.outputs = [NodeConnectionTypes.AiTool];
|
||||||
|
|
||||||
|
const parameters: INodeParameters = {
|
||||||
|
resource: 'user',
|
||||||
|
operation: 'create',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = isDefaultNodeName(name, mockNodeTypeDescription, parameters);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('makeNodeName', () => {
|
||||||
|
let mockNodeTypeDescription: INodeTypeDescription;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Arrange a basic mock node type description
|
||||||
|
mockNodeTypeDescription = {
|
||||||
|
displayName: 'Test Node',
|
||||||
|
name: 'testNode',
|
||||||
|
icon: 'fa:test',
|
||||||
|
group: ['transform'],
|
||||||
|
version: 1,
|
||||||
|
description: 'This is a test node',
|
||||||
|
defaults: {
|
||||||
|
name: 'Test Node',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
properties: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return action-based name when action is available', () => {
|
||||||
|
// Arrange
|
||||||
|
const nodeParameters: INodeParameters = {
|
||||||
|
resource: 'user',
|
||||||
|
operation: 'create',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockNodeTypeDescription.properties = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['user'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
action: 'Create a new user',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'create',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = makeNodeName(nodeParameters, mockNodeTypeDescription);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Create a new user');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return resource-operation-based name when action is not available', () => {
|
||||||
|
// Arrange
|
||||||
|
const nodeParameters: INodeParameters = {
|
||||||
|
resource: 'user',
|
||||||
|
operation: 'create',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockNodeTypeDescription.properties = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['user'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
// No action property
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'create',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = makeNodeName(nodeParameters, mockNodeTypeDescription);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Create user');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return default name when resource or operation is missing', () => {
|
||||||
|
// Arrange
|
||||||
|
const nodeParameters: INodeParameters = {
|
||||||
|
// No resource or operation
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = makeNodeName(nodeParameters, mockNodeTypeDescription);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Test Node');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle case where nodeTypeOperation is not found', () => {
|
||||||
|
// Arrange
|
||||||
|
const nodeParameters: INodeParameters = {
|
||||||
|
resource: 'user',
|
||||||
|
operation: 'create',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockNodeTypeDescription.properties = [
|
||||||
|
// No matching operation property
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = makeNodeName(nodeParameters, mockNodeTypeDescription);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Create user');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle case where options are not a list of INodePropertyOptions', () => {
|
||||||
|
// Arrange
|
||||||
|
const nodeParameters: INodeParameters = {
|
||||||
|
resource: 'user',
|
||||||
|
operation: 'create',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockNodeTypeDescription.properties = [
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: ['user'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Options are not INodePropertyOptions[]
|
||||||
|
options: [
|
||||||
|
//@ts-expect-error
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
default: 'create',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = makeNodeName(nodeParameters, mockNodeTypeDescription);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Create user');
|
||||||
|
});
|
||||||
|
test('should handle case where node is a tool', () => {
|
||||||
|
// Arrange
|
||||||
|
const nodeParameters: INodeParameters = {
|
||||||
|
resource: 'user',
|
||||||
|
operation: 'create',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockNodeTypeDescription.outputs = [NodeConnectionTypes.AiTool];
|
||||||
|
mockNodeTypeDescription.properties = [
|
||||||
|
// No matching operation property
|
||||||
|
];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = makeNodeName(nodeParameters, mockNodeTypeDescription);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe('Create user in Test Node');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('isTool', () => {
|
||||||
|
it('should return true for a node with AiTool output', () => {
|
||||||
|
const description = {
|
||||||
|
outputs: [NodeConnectionTypes.AiTool],
|
||||||
|
version: 0,
|
||||||
|
defaults: {
|
||||||
|
name: '',
|
||||||
|
color: '',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionTypes.Main],
|
||||||
|
properties: [],
|
||||||
|
displayName: '',
|
||||||
|
group: [],
|
||||||
|
description: '',
|
||||||
|
name: 'n8n-nodes-base.someTool',
|
||||||
|
};
|
||||||
|
const parameters = {};
|
||||||
|
const result = isTool(description, parameters);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for a node with AiTool output in NodeOutputConfiguration', () => {
|
||||||
|
const description = {
|
||||||
|
outputs: [{ type: NodeConnectionTypes.AiTool }, { type: NodeConnectionTypes.Main }],
|
||||||
|
version: 0,
|
||||||
|
defaults: {
|
||||||
|
name: '',
|
||||||
|
color: '',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionTypes.Main],
|
||||||
|
properties: [],
|
||||||
|
displayName: '',
|
||||||
|
group: [],
|
||||||
|
description: '',
|
||||||
|
name: 'n8n-nodes-base.someTool',
|
||||||
|
};
|
||||||
|
const parameters = {};
|
||||||
|
const result = isTool(description, parameters);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for a vector store node in retrieve-as-tool mode', () => {
|
||||||
|
const description = {
|
||||||
|
outputs: [NodeConnectionTypes.Main],
|
||||||
|
version: 0,
|
||||||
|
defaults: {
|
||||||
|
name: '',
|
||||||
|
color: '',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionTypes.Main],
|
||||||
|
properties: [],
|
||||||
|
displayName: '',
|
||||||
|
description: '',
|
||||||
|
group: [],
|
||||||
|
name: 'n8n-nodes-base.vectorStore',
|
||||||
|
};
|
||||||
|
const parameters = { mode: 'retrieve-as-tool' };
|
||||||
|
const result = isTool(description, parameters);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for node with no AiTool output', () => {
|
||||||
|
const description = {
|
||||||
|
outputs: [NodeConnectionTypes.Main],
|
||||||
|
version: 0,
|
||||||
|
defaults: {
|
||||||
|
name: '',
|
||||||
|
color: '',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionTypes.Main],
|
||||||
|
properties: [],
|
||||||
|
displayName: '',
|
||||||
|
group: [],
|
||||||
|
description: '',
|
||||||
|
name: 'n8n-nodes-base.someTool',
|
||||||
|
};
|
||||||
|
const parameters = { mode: 'retrieve-as-tool' };
|
||||||
|
const result = isTool(description, parameters);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user