feat(Code Node): Warning if pairedItem absent or could not be auto mapped (#11737)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions

Co-authored-by: Shireen Missi <shireen@n8n.io>
This commit is contained in:
Michael Kret 2024-11-27 18:31:36 +02:00 committed by GitHub
parent af61dbf37f
commit 3a5bd12945
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 110 additions and 16 deletions

View file

@ -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));

View file

@ -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(() => {

View file

@ -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;
}

View file

@ -834,6 +834,7 @@
"expressionModalInput.pairedItemConnectionError": "No path back to node",
"expressionModalInput.pairedItemInvalidPinnedError": "Unpin node {node} and execute",
"expressionModalInput.pairedItemError": "Cant 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",

View file

@ -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<unknown, Error>): string => {
export const stringifyExpressionResult = (
result: Result<unknown, Error>,
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) {

View file

@ -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);
}
}

View file

@ -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 } }]]);
}),
);
});

View file

@ -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. <a target="_blank" href="https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-code-node/">More info</a>',
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. <a target="_blank" href="https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-code-node/">More info</a>',
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]);
});
});

View file

@ -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. <a target="_blank" href="https://docs.n8n.io/data/data-mapping/data-item-linking/item-linking-code-node/">More info</a>',
location: 'outputPane',
},
],
);
}
return [returnData];
};