mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-28 22:19:41 -08:00
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
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:
parent
af61dbf37f
commit
3a5bd12945
|
@ -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));
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 } }]]);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
48
packages/nodes-base/nodes/Code/test/utils.test.ts
Normal file
48
packages/nodes-base/nodes/Code/test/utils.test.ts
Normal 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]);
|
||||
});
|
||||
});
|
|
@ -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];
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue