From 066908060feee088c47c68a1b2cca7a99798695a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Fri, 31 Jan 2025 12:32:18 +0100 Subject: [PATCH] feat(editor): Relocate workflow ID expression notice (no-changelog) (#12942) --- .../v2/methods/localResourceMapping.ts | 11 +- packages/editor-ui/src/components/RunData.vue | 1 + .../ExecuteWorkflow/ExecuteWorkflow.node.ts | 12 +- packages/workflow/src/WorkflowDataProxy.ts | 6 +- packages/workflow/test/NodeTypes.ts | 225 ++++++++++++++++++ .../workflow/test/WorkflowDataProxy.test.ts | 41 ++++ .../WorkflowDataProxy/rawParameter_run.json | 117 +++++++++ .../rawParameter_workflow.json | 80 +++++++ 8 files changed, 488 insertions(+), 5 deletions(-) create mode 100644 packages/workflow/test/fixtures/WorkflowDataProxy/rawParameter_run.json create mode 100644 packages/workflow/test/fixtures/WorkflowDataProxy/rawParameter_workflow.json diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/localResourceMapping.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/localResourceMapping.ts index 2ca2b534e5..74de9baae2 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/localResourceMapping.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/localResourceMapping.ts @@ -4,14 +4,21 @@ import type { ILocalLoadOptionsFunctions, ResourceMapperFields } from 'n8n-workf export async function loadSubWorkflowInputs( this: ILocalLoadOptionsFunctions, ): Promise { - const { fields, subworkflowInfo } = await loadWorkflowInputMappings.bind(this)(); + const { fields, subworkflowInfo, dataMode } = await loadWorkflowInputMappings.bind(this)(); let emptyFieldsNotice: string | undefined; if (fields.length === 0) { const subworkflowLink = subworkflowInfo?.id ? `sub-workflow’s trigger` : 'sub-workflow’s trigger'; - emptyFieldsNotice = `This sub-workflow will not receive any input when called by your AI node. Define your expected input in the ${subworkflowLink}.`; + switch (dataMode) { + case 'passthrough': + emptyFieldsNotice = `This sub-workflow is set up to receive all input data, without specific inputs the Agent will not be able to pass data to this tool. You can define specific inputs in the ${subworkflowLink}.`; + break; + default: + emptyFieldsNotice = `This sub-workflow will not receive any input when called by your AI node. Define your expected input in the ${subworkflowLink}.`; + break; + } } return { fields, emptyFieldsNotice }; } diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index b2f20dc6fe..0a64d01b5b 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -1471,6 +1471,7 @@ defineExpose({ enterEditMode }); :key="hint.message" :class="$style.hintCallout" :theme="hint.type || 'info'" + data-test-id="node-hint" > diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts index a0ab059ab4..0ce4ab265a 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts @@ -130,7 +130,6 @@ export class ExecuteWorkflow implements INodeType { }, default: '', required: true, - hint: "Note on using an expression here: if this node is set to run once with all items, they will all be sent to the same workflow. That workflow's ID will be calculated by evaluating the expression for the first input item.", }, // ---------------------------------- // source:localFile @@ -270,6 +269,17 @@ export class ExecuteWorkflow implements INodeType { ], }, ], + hints: [ + { + type: 'info', + message: + "Note on using an expression for workflow ID: Since this node is set to run once with all items, they will all be sent to the same workflow. That workflow's ID will be calculated by evaluating the expression for the first input item.", + displayCondition: + '={{ $rawParameter.workflowId.startsWith("=") && $parameter.mode === "once" && $nodeVersion >= 1.2 }}', + whenToDisplay: 'always', + location: 'outputPane', + }, + ], }; methods = { diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index 63353ca10d..600821e44c 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -164,8 +164,9 @@ export class WorkflowDataProxy { * * @private * @param {string} nodeName The name of the node to query data from + * @param {boolean} [resolveValue=true] If the expression value should get resolved */ - private nodeParameterGetter(nodeName: string) { + private nodeParameterGetter(nodeName: string, resolveValue = true) { const that = this; const node = this.workflow.nodes[nodeName]; @@ -223,7 +224,7 @@ export class WorkflowDataProxy { } } - if (typeof returnValue === 'string' && returnValue.charAt(0) === '=') { + if (resolveValue && typeof returnValue === 'string' && returnValue.charAt(0) === '=') { // The found value is an expression so resolve it return that.workflow.expression.getParameterValue( returnValue, @@ -1359,6 +1360,7 @@ export class WorkflowDataProxy { $node: this.nodeGetter(), $self: this.selfGetter(), $parameter: this.nodeParameterGetter(this.activeNodeName), + $rawParameter: this.nodeParameterGetter(this.activeNodeName, false), $prevNode: this.prevNodeGetter(), $runIndex: this.runIndex, $mode: this.mode, diff --git a/packages/workflow/test/NodeTypes.ts b/packages/workflow/test/NodeTypes.ts index 9cd5c548f2..ba1a6ee766 100644 --- a/packages/workflow/test/NodeTypes.ts +++ b/packages/workflow/test/NodeTypes.ts @@ -604,12 +604,237 @@ const manualTriggerNode: LoadedClass = { }, }; +const executeWorkflowNode: LoadedClass = { + type: { + description: { + name: 'n8n-nodes-base.executeWorkflow', + displayName: 'Execute Sub-workflow', + icon: 'fa:sign-in-alt', + iconColor: 'orange-red', + group: ['transform'], + version: [1, 1.1, 1.2], + subtitle: '={{"Workflow: " + $parameter["workflowId"]}}', + description: 'Execute another workflow', + defaults: { name: 'Execute Workflow', color: '#ff6d5a' }, + inputs: [], + outputs: [], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'hidden', + noDataExpression: true, + default: 'call_workflow', + options: [{ name: 'Execute a Sub-Workflow', value: 'call_workflow' }], + }, + { + displayName: + 'This node is out of date. Please upgrade by removing it and adding a new one', + name: 'outdatedVersionWarning', + type: 'notice', + displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } }, + default: '', + }, + { + displayName: 'Source', + name: 'source', + type: 'options', + options: [ + { + name: 'Database', + value: 'database', + description: 'Load the workflow from the database by ID', + }, + { + name: 'Local File', + value: 'localFile', + description: 'Load the workflow from a locally saved file', + }, + { + name: 'Parameter', + value: 'parameter', + description: 'Load the workflow from a parameter', + }, + { name: 'URL', value: 'url', description: 'Load the workflow from an URL' }, + ], + default: 'database', + description: 'Where to get the workflow to execute from', + displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } }, + }, + { + displayName: 'Source', + name: 'source', + type: 'options', + options: [ + { + name: 'Database', + value: 'database', + description: 'Load the workflow from the database by ID', + }, + { + name: 'Define Below', + value: 'parameter', + description: 'Pass the JSON code of a workflow', + }, + ], + default: 'database', + description: 'Where to get the workflow to execute from', + displayOptions: { show: { '@version': [{ _cnd: { gte: 1.2 } }] } }, + }, + { + displayName: 'Workflow ID', + name: 'workflowId', + type: 'string', + displayOptions: { show: { source: ['database'], '@version': [1] } }, + default: '', + required: true, + hint: 'Can be found in the URL of the workflow', + description: + "Note on using an expression here: if this node is set to run once with all items, they will all be sent to the same workflow. That workflow's ID will be calculated by evaluating the expression for the first input item.", + }, + { + displayName: 'Workflow', + name: 'workflowId', + type: 'workflowSelector', + displayOptions: { show: { source: ['database'], '@version': [{ _cnd: { gte: 1.1 } }] } }, + default: '', + required: true, + }, + { + displayName: 'Workflow Path', + name: 'workflowPath', + type: 'string', + displayOptions: { show: { source: ['localFile'] } }, + default: '', + placeholder: '/data/workflow.json', + required: true, + description: 'The path to local JSON workflow file to execute', + }, + { + displayName: 'Workflow JSON', + name: 'workflowJson', + type: 'json', + typeOptions: { rows: 10 }, + displayOptions: { show: { source: ['parameter'] } }, + default: '\n\n\n', + required: true, + description: 'The workflow JSON code to execute', + }, + { + displayName: 'Workflow URL', + name: 'workflowUrl', + type: 'string', + displayOptions: { show: { source: ['url'] } }, + default: '', + placeholder: 'https://example.com/workflow.json', + required: true, + description: 'The URL from which to load the workflow from', + }, + { + displayName: + 'Any data you pass into this node will be output by the Execute Workflow Trigger. More info', + name: 'executeWorkflowNotice', + type: 'notice', + default: '', + displayOptions: { show: { '@version': [{ _cnd: { lte: 1.1 } }] } }, + }, + { + displayName: 'Workflow Inputs', + name: 'workflowInputs', + type: 'resourceMapper', + noDataExpression: true, + default: { mappingMode: 'defineBelow', value: null }, + required: true, + typeOptions: { + loadOptionsDependsOn: ['workflowId.value'], + resourceMapper: { + localResourceMapperMethod: 'loadSubWorkflowInputs', + valuesLabel: 'Workflow Inputs', + mode: 'map', + fieldWords: { singular: 'input', plural: 'inputs' }, + addAllFields: true, + multiKeyMatch: false, + supportAutoMap: false, + showTypeConversionOptions: true, + }, + }, + displayOptions: { + show: { source: ['database'], '@version': [{ _cnd: { gte: 1.2 } }] }, + hide: { workflowId: [''] }, + }, + }, + { + displayName: 'Mode', + name: 'mode', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Run once with all items', + value: 'once', + description: 'Pass all items into a single execution of the sub-workflow', + }, + { + name: 'Run once for each item', + value: 'each', + description: 'Call the sub-workflow individually for each item', + }, + ], + default: 'once', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + default: {}, + placeholder: 'Add option', + options: [ + { + displayName: 'Wait For Sub-Workflow Completion', + name: 'waitForSubWorkflow', + type: 'boolean', + default: true, + description: + 'Whether the main workflow should wait for the sub-workflow to complete its execution before proceeding', + }, + ], + }, + ], + hints: [ + { + type: 'info', + message: + "Note on using an expression for workflow ID: if this node is set to run once with all items, they will all be sent to the same workflow. That workflow's ID will be calculated by evaluating the expression for the first input item.", + displayCondition: + '={{ $rawParameter.workflowId.startsWith("=") && $nodeVersion >= 1.2 }}', + whenToDisplay: 'always', + location: 'outputPane', + }, + ], + codex: { + categories: ['Core Nodes'], + subcategories: { 'Core Nodes': ['Helpers', 'Flow'] }, + alias: ['n8n', 'call', 'sub', 'workflow', 'sub-workflow', 'subworkflow'], + resources: { + primaryDocumentation: [ + { + url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executeworkflow/', + }, + ], + }, + }, + }, + }, + sourcePath: '', +}; + export class NodeTypes implements INodeTypes { nodeTypes: INodeTypeData = { 'n8n-nodes-base.stickyNote': stickyNode, 'n8n-nodes-base.set': setNode, 'test.googleSheets': googleSheetsNode, 'test.set': setNode, + 'n8n-nodes-base.executeWorkflow': executeWorkflowNode, 'test.setMulti': { sourcePath: '', type: { diff --git a/packages/workflow/test/WorkflowDataProxy.test.ts b/packages/workflow/test/WorkflowDataProxy.test.ts index 85c26c79d6..943d765280 100644 --- a/packages/workflow/test/WorkflowDataProxy.test.ts +++ b/packages/workflow/test/WorkflowDataProxy.test.ts @@ -549,4 +549,45 @@ describe('WorkflowDataProxy', () => { expect(() => getFromAIProxy().$fromAI('invalid!')).toThrow(ExpressionError); }); }); + + describe('$rawParameter', () => { + const fixture = loadFixture('rawParameter'); + const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Execute Workflow', 'manual', { + connectionType: NodeConnectionType.Main, + throwOnMissingExecutionData: false, + runIndex: 0, + }); + + test('returns simple raw parameter value', () => { + expect(proxy.$rawParameter.options).toEqual({ + waitForSubWorkflow: '={{ true }}', + }); + }); + + test('returns raw parameter value for resource locator values', () => { + expect(proxy.$rawParameter.workflowId).toEqual('={{ $json.foo }}'); + }); + + test('returns raw parameter value when there is no run data', () => { + const noRunDataProxy = getProxyFromFixture( + fixture.workflow, + { + data: { resultData: { runData: {} } }, + mode: 'manual', + startedAt: new Date(), + status: 'success', + }, + 'Execute Workflow', + 'manual', + { + connectionType: NodeConnectionType.Main, + throwOnMissingExecutionData: false, + runIndex: 0, + }, + ); + expect(noRunDataProxy.$rawParameter.options).toEqual({ + waitForSubWorkflow: '={{ true }}', + }); + }); + }); }); diff --git a/packages/workflow/test/fixtures/WorkflowDataProxy/rawParameter_run.json b/packages/workflow/test/fixtures/WorkflowDataProxy/rawParameter_run.json new file mode 100644 index 0000000000..431dc9efbb --- /dev/null +++ b/packages/workflow/test/fixtures/WorkflowDataProxy/rawParameter_run.json @@ -0,0 +1,117 @@ +{ + "data": { + "startData": {}, + "resultData": { + "runData": { + "_custom": { + "type": "reactive", + "stateTypeName": "Reactive", + "value": { + "Manual trigger": [ + { + "_custom": { + "type": "reactive", + "stateTypeName": "Reactive", + "value": { + "hints": [], + "startTime": 1738314562475, + "executionTime": 1, + "source": [], + "executionStatus": "success", + "data": { "main": [[{ "json": {}, "pairedItem": { "item": 0 } }]] } + } + } + } + ], + "Edit Fields": [ + { + "_custom": { + "type": "reactive", + "stateTypeName": "Reactive", + "value": { + "hints": [], + "startTime": 1738314562477, + "executionTime": 0, + "source": [{ "previousNode": "Manual trigger" }], + "executionStatus": "success", + "data": { + "main": [[{ "json": { "foo": "test" }, "pairedItem": { "item": 0 } }]] + } + } + } + } + ], + "Execute Workflow": [ + { + "hints": [], + "startTime": 1738314562478, + "executionTime": 2, + "source": [{ "previousNode": "Edit Fields" }], + "executionStatus": "error", + "error": { + "level": "error", + "tags": { "packageName": "cli" }, + "extra": { "workflowId": "1.2" }, + "message": "Workflow does not exist.", + "stack": "Error: Workflow does not exist.\n at getWorkflowData (/Users/miloradfilipovic/workspace/n8n/packages/cli/src/workflow-execute-additional-data.ts:124:10)\n at Object.executeWorkflow (/Users/miloradfilipovic/workspace/n8n/packages/cli/src/workflow-execute-additional-data.ts:155:4)\n at ExecuteContext.executeWorkflow (/Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/node-execution-context/base-execute-context.ts:120:18)\n at ExecuteContext.execute (/Users/miloradfilipovic/workspace/n8n/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts:397:50)\n at WorkflowExecute.runNode (/Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/workflow-execute.ts:1097:8)\n at /Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/workflow-execute.ts:1503:27\n at /Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/workflow-execute.ts:2064:11" + } + } + ] + } + } + }, + "pinData": {}, + "lastNodeExecuted": "Execute Workflow", + "error": { + "level": "error", + "tags": { "packageName": "cli" }, + "extra": { "workflowId": "1.2" }, + "message": "Workflow does not exist.", + "stack": "Error: Workflow does not exist.\n at getWorkflowData (/Users/miloradfilipovic/workspace/n8n/packages/cli/src/workflow-execute-additional-data.ts:124:10)\n at Object.executeWorkflow (/Users/miloradfilipovic/workspace/n8n/packages/cli/src/workflow-execute-additional-data.ts:155:4)\n at ExecuteContext.executeWorkflow (/Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/node-execution-context/base-execute-context.ts:120:18)\n at ExecuteContext.execute (/Users/miloradfilipovic/workspace/n8n/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts:397:50)\n at WorkflowExecute.runNode (/Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/workflow-execute.ts:1097:8)\n at /Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/workflow-execute.ts:1503:27\n at /Users/miloradfilipovic/workspace/n8n/packages/core/src/execution-engine/workflow-execute.ts:2064:11" + } + }, + "executionData": { + "contextData": {}, + "nodeExecutionStack": [ + { + "node": { + "parameters": { + "operation": "call_workflow", + "source": "database", + "workflowId": { + "__rl": true, + "mode": "id", + "value": "=1.2", + "cachedResultName": "=1.2" + }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": {}, + "matchingColumns": [], + "schema": [], + "attemptToConvertTypes": false, + "convertFieldsToString": true + }, + "mode": "once", + "options": {} + }, + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.2, + "position": [120, -100], + "id": "62717ac7-614d-4e3f-b2ec-1e28688068c4", + "name": "Execute Workflow" + }, + "data": { "main": [[{ "json": { "foo": "test" }, "pairedItem": { "item": 0 } }]] }, + "source": { "main": [{ "previousNode": "Edit Fields" }] } + } + ], + "metadata": {}, + "waitingExecution": {}, + "waitingExecutionSource": {} + }, + "mode": "manual", + "startedAt": "2024-02-08T15:45:18.848Z", + "stoppedAt": "2024-02-08T15:45:18.862Z", + "status": "success" + } +} diff --git a/packages/workflow/test/fixtures/WorkflowDataProxy/rawParameter_workflow.json b/packages/workflow/test/fixtures/WorkflowDataProxy/rawParameter_workflow.json new file mode 100644 index 0000000000..1270920dc5 --- /dev/null +++ b/packages/workflow/test/fixtures/WorkflowDataProxy/rawParameter_workflow.json @@ -0,0 +1,80 @@ +{ + "nodes": [ + { + "id": "804e5ba7-4b1d-48c2-abfa-a36717a9fa66", + "name": "Manual trigger", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-320, -100], + "parameters": {} + }, + { + "id": "f995b1a2-8a49-4f0c-ae0d-8fb4c600cdef", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [-100, -100], + "parameters": { + "assignments": { + "assignments": [ + { + "id": "f4d80089-a3d7-470f-8c07-dec07e37f339", + "name": "foo", + "value": "={{ test }}", + "type": "string" + } + ] + }, + "options": {} + } + }, + { + "id": "62717ac7-614d-4e3f-b2ec-1e28688068c4", + "name": "Execute Workflow", + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.2, + "position": [120, -100], + "parameters": { + "workflowId": { + "__rl": true, + "value": "={{ $json.foo }}", + "mode": "id" + }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": {}, + "matchingColumns": [], + "schema": [], + "attemptToConvertTypes": false, + "convertFieldsToString": true + }, + "options": { "waitForSubWorkflow": "={{ true }}" } + } + } + ], + "connections": { + "Manual trigger": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields": { + "main": [ + [ + { + "node": "Execute Workflow", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {} +}