diff --git a/packages/editor-ui/src/components/AssignmentCollection/Assignment.vue b/packages/editor-ui/src/components/AssignmentCollection/Assignment.vue index 0984d9c7e4..7240dac1e2 100644 --- a/packages/editor-ui/src/components/AssignmentCollection/Assignment.vue +++ b/packages/editor-ui/src/components/AssignmentCollection/Assignment.vue @@ -5,6 +5,7 @@ import ParameterInputFull from '@/components/ParameterInputFull.vue'; import ParameterInputHint from '@/components/ParameterInputHint.vue'; import ParameterIssues from '@/components/ParameterIssues.vue'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; +import { useWorkflowsStore } from '@/stores/workflows.store'; import { isExpression, stringifyExpressionResult } from '@/utils/expressions'; import type { AssignmentValue, INodeProperties, Result } from 'n8n-workflow'; import { computed, ref } from 'vue'; @@ -101,7 +102,12 @@ const hint = computed(() => { result = { ok: false, error }; } - return stringifyExpressionResult(result); + const hasRunData = + !!useWorkflowsStore().workflowExecutionData?.data?.resultData?.runData[ + ndvStore.activeNode?.name ?? '' + ]; + + return stringifyExpressionResult(result, hasRunData); }); const highlightHint = computed(() => Boolean(hint.value && ndvStore.getHoveringItem)); diff --git a/packages/editor-ui/src/components/ParameterInputWrapper.vue b/packages/editor-ui/src/components/ParameterInputWrapper.vue index 5158d1b59a..93534fede9 100644 --- a/packages/editor-ui/src/components/ParameterInputWrapper.vue +++ b/packages/editor-ui/src/components/ParameterInputWrapper.vue @@ -22,6 +22,7 @@ import type { EventBus } from 'n8n-design-system/utils'; import { createEventBus } from 'n8n-design-system/utils'; import { computed } from 'vue'; import { useRouter } from 'vue-router'; +import { useWorkflowsStore } from '@/stores/workflows.store'; type Props = { parameter: INodeProperties; @@ -144,7 +145,11 @@ const evaluatedExpressionValue = computed(() => { }); const evaluatedExpressionString = computed(() => { - return stringifyExpressionResult(evaluatedExpression.value); + const hasRunData = + !!useWorkflowsStore().workflowExecutionData?.data?.resultData?.runData[ + ndvStore.activeNode?.name ?? '' + ]; + return stringifyExpressionResult(evaluatedExpression.value, hasRunData); }); const expressionOutput = computed(() => { diff --git a/packages/editor-ui/src/composables/useExpressionEditor.ts b/packages/editor-ui/src/composables/useExpressionEditor.ts index a63570d101..0911621be3 100644 --- a/packages/editor-ui/src/composables/useExpressionEditor.ts +++ b/packages/editor-ui/src/composables/useExpressionEditor.ts @@ -305,7 +305,11 @@ export const useExpressionEditor = ({ result.resolved = workflowHelpers.resolveExpression('=' + resolvable, undefined, opts); } } catch (error) { - result.resolved = `[${getExpressionErrorMessage(error)}]`; + const hasRunData = + !!workflowsStore.workflowExecutionData?.data?.resultData?.runData[ + ndvStore.activeNode?.name ?? '' + ]; + result.resolved = `[${getExpressionErrorMessage(error, hasRunData)}]`; result.error = true; result.fullError = error; } diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 0f9c28a9cd..101305beee 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -834,6 +834,7 @@ "expressionModalInput.pairedItemConnectionError": "No path back to node", "expressionModalInput.pairedItemInvalidPinnedError": "Unpin node ‘{node}’ and execute", "expressionModalInput.pairedItemError": "Can’t determine which item to use", + "expressionModalInput.pairedItemError.noRunData": "Can't determine which item to use - execute node for more info", "fixedCollectionParameter.choose": "Choose...", "fixedCollectionParameter.currentlyNoItemsExist": "Currently no items exist", "fixedCollectionParameter.deleteItem": "Delete item", diff --git a/packages/editor-ui/src/utils/expressions.ts b/packages/editor-ui/src/utils/expressions.ts index d641a27b53..d572006ad5 100644 --- a/packages/editor-ui/src/utils/expressions.ts +++ b/packages/editor-ui/src/utils/expressions.ts @@ -78,7 +78,7 @@ export const getResolvableState = (error: unknown, ignoreError = false): Resolva return 'invalid'; }; -export const getExpressionErrorMessage = (error: Error): string => { +export const getExpressionErrorMessage = (error: Error, nodeHasRunData = false): string => { if (isNoExecDataExpressionError(error) || isPairedItemIntermediateNodesError(error)) { return i18n.baseText('expressionModalInput.noExecutionData'); } @@ -109,19 +109,24 @@ export const getExpressionErrorMessage = (error: Error): string => { } if (isAnyPairedItemError(error)) { - return i18n.baseText('expressionModalInput.pairedItemError'); + return nodeHasRunData + ? i18n.baseText('expressionModalInput.pairedItemError') + : i18n.baseText('expressionModalInput.pairedItemError.noRunData'); } return error.message; }; -export const stringifyExpressionResult = (result: Result): string => { +export const stringifyExpressionResult = ( + result: Result, + nodeHasRunData = false, +): string => { if (!result.ok) { if (getResolvableState(result.error) !== 'invalid') { return ''; } - return `[${i18n.baseText('parameterInput.error')}: ${getExpressionErrorMessage(result.error)}]`; + return `[${i18n.baseText('parameterInput.error')}: ${getExpressionErrorMessage(result.error, nodeHasRunData)}]`; } if (result.result === null) { diff --git a/packages/nodes-base/nodes/Code/Code.node.ts b/packages/nodes-base/nodes/Code/Code.node.ts index a887bcee20..99604a712a 100644 --- a/packages/nodes-base/nodes/Code/Code.node.ts +++ b/packages/nodes-base/nodes/Code/Code.node.ts @@ -17,7 +17,7 @@ import { JavaScriptSandbox } from './JavaScriptSandbox'; import { JsTaskRunnerSandbox } from './JsTaskRunnerSandbox'; import { PythonSandbox } from './PythonSandbox'; import { getSandboxContext } from './Sandbox'; -import { standardizeOutput } from './utils'; +import { addPostExecutionWarning, standardizeOutput } from './utils'; const { CODE_ENABLE_STDOUT } = process.env; @@ -142,6 +142,8 @@ export class Code implements INodeType { return sandbox; }; + const inputDataItems = this.getInputData(); + // ---------------------------------- // runOnceForAllItems // ---------------------------------- @@ -163,7 +165,7 @@ export class Code implements INodeType { standardizeOutput(item.json); } - return [items]; + return addPostExecutionWarning(items, inputDataItems?.length); } // ---------------------------------- @@ -172,9 +174,7 @@ export class Code implements INodeType { const returnData: INodeExecutionData[] = []; - const items = this.getInputData(); - - for (let index = 0; index < items.length; index++) { + for (let index = 0; index < inputDataItems.length; index++) { const sandbox = getSandbox(index); let result: INodeExecutionData | undefined; try { @@ -201,6 +201,6 @@ export class Code implements INodeType { } } - return [returnData]; + return addPostExecutionWarning(returnData, inputDataItems?.length); } } diff --git a/packages/nodes-base/nodes/Code/test/Code.node.test.ts b/packages/nodes-base/nodes/Code/test/Code.node.test.ts index 4331b3b7e8..3f1ed0eccf 100644 --- a/packages/nodes-base/nodes/Code/test/Code.node.test.ts +++ b/packages/nodes-base/nodes/Code/test/Code.node.test.ts @@ -57,7 +57,8 @@ describe('Code Node unit test', () => { jest.spyOn(NodeVM.prototype, 'run').mockResolvedValueOnce(input); const output = await node.execute.call(thisArg); - expect(output).toEqual([expected]); + + expect([...output]).toEqual([expected]); }), ); }); @@ -109,7 +110,7 @@ describe('Code Node unit test', () => { jest.spyOn(NodeVM.prototype, 'run').mockResolvedValueOnce(input); const output = await node.execute.call(thisArg); - expect(output).toEqual([[{ json: expected?.json, pairedItem: { item: 0 } }]]); + expect([...output]).toEqual([[{ json: expected?.json, pairedItem: { item: 0 } }]]); }), ); }); diff --git a/packages/nodes-base/nodes/Code/test/utils.test.ts b/packages/nodes-base/nodes/Code/test/utils.test.ts new file mode 100644 index 0000000000..b622c86661 --- /dev/null +++ b/packages/nodes-base/nodes/Code/test/utils.test.ts @@ -0,0 +1,48 @@ +import type { INodeExecutionData } from 'n8n-workflow'; +import { NodeExecutionOutput } from 'n8n-workflow'; +import { addPostExecutionWarning } from '../utils'; + +describe('addPostExecutionWarning', () => { + const inputItemsLength = 2; + + it('should return a NodeExecutionOutput warning when returnData length differs from inputItemsLength', () => { + const returnData: INodeExecutionData[] = [{ json: {}, pairedItem: 0 }]; + + const result = addPostExecutionWarning(returnData, inputItemsLength); + + expect(result).toBeInstanceOf(NodeExecutionOutput); + expect((result as NodeExecutionOutput)?.getHints()).toEqual([ + { + message: + 'To make sure expressions after this node work, return the input items that produced each output item. More info', + location: 'outputPane', + }, + ]); + }); + + it('should return a NodeExecutionOutput warning when any item has undefined pairedItem', () => { + const returnData: INodeExecutionData[] = [{ json: {}, pairedItem: 0 }, { json: {} }]; + + const result = addPostExecutionWarning(returnData, inputItemsLength); + + expect(result).toBeInstanceOf(NodeExecutionOutput); + expect((result as NodeExecutionOutput)?.getHints()).toEqual([ + { + message: + 'To make sure expressions after this node work, return the input items that produced each output item. More info', + location: 'outputPane', + }, + ]); + }); + + it('should return returnData array when all items match inputItemsLength and have defined pairedItem', () => { + const returnData: INodeExecutionData[] = [ + { json: {}, pairedItem: 0 }, + { json: {}, pairedItem: 1 }, + ]; + + const result = addPostExecutionWarning(returnData, inputItemsLength); + + expect(result).toEqual([returnData]); + }); +}); diff --git a/packages/nodes-base/nodes/Code/utils.ts b/packages/nodes-base/nodes/Code/utils.ts index 28fa00f829..410da572b9 100644 --- a/packages/nodes-base/nodes/Code/utils.ts +++ b/packages/nodes-base/nodes/Code/utils.ts @@ -1,4 +1,5 @@ -import type { IDataObject } from 'n8n-workflow'; +import type { INodeExecutionData, IDataObject } from 'n8n-workflow'; +import { NodeExecutionOutput } from 'n8n-workflow'; export function isObject(maybe: unknown): maybe is { [key: string]: unknown } { return ( @@ -36,3 +37,26 @@ export function standardizeOutput(output: IDataObject) { standardizeOutputRecursive(output); return output; } + +export const addPostExecutionWarning = ( + returnData: INodeExecutionData[], + inputItemsLength: number, +) => { + if ( + returnData.length !== inputItemsLength || + returnData.some((item) => item.pairedItem === undefined) + ) { + return new NodeExecutionOutput( + [returnData], + [ + { + message: + 'To make sure expressions after this node work, return the input items that produced each output item. More info', + location: 'outputPane', + }, + ], + ); + } + + return [returnData]; +};