diff --git a/cypress/constants.ts b/cypress/constants.ts index 6f7e7b978d317..cbbf838530133 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -61,6 +61,7 @@ export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model' export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory'; export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser'; export const WEBHOOK_NODE_NAME = 'Webhook'; +export const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Workflow'; export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl'; diff --git a/cypress/e2e/45-workflow-selector-parameter.cy.ts b/cypress/e2e/45-workflow-selector-parameter.cy.ts new file mode 100644 index 0000000000000..a6dc23e6c2926 --- /dev/null +++ b/cypress/e2e/45-workflow-selector-parameter.cy.ts @@ -0,0 +1,82 @@ +import { EXECUTE_WORKFLOW_NODE_NAME } from '../constants'; +import { WorkflowPage as WorkflowPageClass, NDV } from '../pages'; +import { getVisiblePopper } from '../utils'; + +const workflowPage = new WorkflowPageClass(); +const ndv = new NDV(); + +describe('Workflow Selector Parameter', () => { + beforeEach(() => { + cy.resetDatabase(); + cy.signinAsOwner(); + ['Get_Weather', 'Search_DB'].forEach((workflowName) => { + workflowPage.actions.visit(); + cy.createFixtureWorkflow(`Test_Subworkflow_${workflowName}.json`, workflowName); + workflowPage.actions.saveWorkflowOnButtonClick(); + }); + workflowPage.actions.visit(); + workflowPage.actions.addInitialNodeToCanvas(EXECUTE_WORKFLOW_NODE_NAME, { + keepNdvOpen: true, + action: 'Call Another Workflow', + }); + }); + it('should render sub-workflows list', () => { + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + + getVisiblePopper() + .should('have.length', 1) + .findChildByTestId('rlc-item') + .should('have.length', 2); + }); + + it('should show required parameter warning', () => { + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + ndv.getters.parameterInputIssues('workflowId').should('exist'); + }); + + it('should filter sub-workflows list', () => { + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + ndv.getters.resourceLocatorSearch('workflowId').type('Weather'); + + getVisiblePopper() + .should('have.length', 1) + .findChildByTestId('rlc-item') + .should('have.length', 1) + .click(); + + ndv.getters + .resourceLocatorInput('workflowId') + .find('input') + .should('have.value', 'Get_Weather'); + }); + + it('should render sub-workflow links correctly', () => { + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + + getVisiblePopper().findChildByTestId('rlc-item').first().click(); + + ndv.getters.resourceLocatorInput('workflowId').find('a').should('exist'); + cy.getByTestId('radio-button-expression').eq(1).click(); + ndv.getters.resourceLocatorInput('workflowId').find('a').should('not.exist'); + }); + + it('should switch to ID mode on expression', () => { + ndv.getters.resourceLocator('workflowId').should('be.visible'); + ndv.getters.resourceLocatorInput('workflowId').click(); + + getVisiblePopper().findChildByTestId('rlc-item').first().click(); + ndv.getters + .resourceLocatorModeSelector('workflowId') + .find('input') + .should('have.value', 'From list'); + cy.getByTestId('radio-button-expression').eq(1).click(); + ndv.getters + .resourceLocatorModeSelector('workflowId') + .find('input') + .should('have.value', 'By ID'); + }); +}); diff --git a/cypress/fixtures/Test_Subworkflow_Get_Weather.json b/cypress/fixtures/Test_Subworkflow_Get_Weather.json new file mode 100644 index 0000000000000..3829aca879c3a --- /dev/null +++ b/cypress/fixtures/Test_Subworkflow_Get_Weather.json @@ -0,0 +1,53 @@ +{ + "name": "Get Weather", + "nodes": [ + { + "parameters": {}, + "id": "82eed1ba-179b-4f8f-8a85-b45f0d4e5857", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1, + "position": [ + 560, + 340 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "6ad8dc55-20f3-45af-a724-c7ecac90d338", + "name": "response", + "value": "Weather is sunny", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "8f3e00f6-fc92-4aba-817b-93d206158bda", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 780, + 340 + ] + } + ], + "pinData": {}, + "connections": { + "Execute Workflow Trigger": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + } +} diff --git a/cypress/fixtures/Test_Subworkflow_Search_DB.json b/cypress/fixtures/Test_Subworkflow_Search_DB.json new file mode 100644 index 0000000000000..990aee120d4de --- /dev/null +++ b/cypress/fixtures/Test_Subworkflow_Search_DB.json @@ -0,0 +1,64 @@ +{ + "name": "Search DB", + "nodes": [ + { + "parameters": {}, + "id": "64465f9b-63de-43f9-8d90-b5b2eb7a2dc7", + "name": "Execute Workflow Trigger", + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1, + "position": [ + 640, + 380 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "6ad8dc55-20f3-45af-a724-c7ecac90d338", + "name": "response", + "value": "10 results found", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "b580fd2b-00c8-4a52-8acb-024f204c0947", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [ + 860, + 380 + ] + } + ], + "pinData": {}, + "connections": { + "Execute Workflow Trigger": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "6026f7a4-f5dc-4c27-9f83-3a02fc6e33ae", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "BFFhCdBZmNSkx4qf", + "tags": [] +} \ No newline at end of file diff --git a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts index 5b2d852178932..6b446149fca3a 100644 --- a/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/retrievers/RetrieverWorkflow/RetrieverWorkflow.node.ts @@ -9,6 +9,7 @@ import type { INodeType, INodeTypeDescription, SupplyData, + INodeParameterResourceLocator, } from 'n8n-workflow'; import { BaseRetriever, type BaseRetrieverInput } from '@langchain/core/retrievers'; @@ -41,7 +42,7 @@ export class RetrieverWorkflow implements INodeType { name: 'retrieverWorkflow', icon: 'fa:box-open', group: ['transform'], - version: 1, + version: [1, 1.1], description: 'Use an n8n Workflow as Retriever', defaults: { name: 'Workflow Retriever', @@ -105,12 +106,26 @@ export class RetrieverWorkflow implements INodeType { displayOptions: { show: { source: ['database'], + '@version': [{ _cnd: { eq: 1 } }], }, }, default: '', required: true, description: 'The workflow to execute', }, + { + displayName: 'Workflow', + name: 'workflowId', + type: 'workflowSelector', + displayOptions: { + show: { + source: ['database'], + '@version': [{ _cnd: { gte: 1.1 } }], + }, + }, + default: '', + required: true, + }, // ---------------------------------- // source:parameter @@ -301,11 +316,21 @@ export class RetrieverWorkflow implements INodeType { const workflowInfo: IExecuteWorkflowInfo = {}; if (source === 'database') { - // Read workflow from database - workflowInfo.id = this.executeFunctions.getNodeParameter( - 'workflowId', - itemIndex, - ) as string; + const nodeVersion = this.executeFunctions.getNode().typeVersion; + if (nodeVersion === 1) { + workflowInfo.id = this.executeFunctions.getNodeParameter( + 'workflowId', + itemIndex, + ) as string; + } else { + const { value } = this.executeFunctions.getNodeParameter( + 'workflowId', + itemIndex, + {}, + ) as INodeParameterResourceLocator; + workflowInfo.id = value as string; + } + baseMetadata.workflowId = workflowInfo.id; } else if (source === 'parameter') { // Read workflow from parameter diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts index 5ed96cbd60123..0b00e17ac46ee 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/ToolWorkflow.node.ts @@ -8,6 +8,7 @@ import type { SupplyData, ExecutionError, IDataObject, + INodeParameterResourceLocator, } from 'n8n-workflow'; import { NodeConnectionType, NodeOperationError, jsonParse } from 'n8n-workflow'; import type { SetField, SetNodeOptions } from 'n8n-nodes-base/dist/nodes/Set/v2/helpers/interfaces'; @@ -32,7 +33,7 @@ export class ToolWorkflow implements INodeType { name: 'toolWorkflow', icon: 'fa:network-wired', group: ['transform'], - version: [1, 1.1], + version: [1, 1.1, 1.2], description: 'Uses another n8n workflow as a tool. Allows packaging any n8n node(s) as a tool.', defaults: { name: 'Call n8n Workflow Tool', @@ -142,6 +143,7 @@ export class ToolWorkflow implements INodeType { displayOptions: { show: { source: ['database'], + '@version': [{ _cnd: { lte: 1.1 } }], }, }, default: '', @@ -150,6 +152,20 @@ export class ToolWorkflow implements INodeType { hint: 'Can be found in the URL of the workflow', }, + { + displayName: 'Workflow', + name: 'workflowId', + type: 'workflowSelector', + displayOptions: { + show: { + source: ['database'], + '@version': [{ _cnd: { gte: 1.2 } }], + }, + }, + default: '', + required: true, + }, + // ---------------------------------- // source:parameter // ---------------------------------- @@ -368,7 +384,17 @@ export class ToolWorkflow implements INodeType { const workflowInfo: IExecuteWorkflowInfo = {}; if (source === 'database') { // Read workflow from database - workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string; + const nodeVersion = this.getNode().typeVersion; + if (nodeVersion <= 1.1) { + workflowInfo.id = this.getNodeParameter('workflowId', itemIndex) as string; + } else { + const { value } = this.getNodeParameter( + 'workflowId', + itemIndex, + {}, + ) as INodeParameterResourceLocator; + workflowInfo.id = value as string; + } } else if (source === 'parameter') { // Read workflow from parameter const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string; diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 533ad6c49d3cb..b55e7b7c6d3b1 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -15,7 +15,7 @@