diff --git a/packages/editor-ui/src/components/Error/NodeErrorView.vue b/packages/editor-ui/src/components/Error/NodeErrorView.vue index 6496f7abc07f6..f893a326477e7 100644 --- a/packages/editor-ui/src/components/Error/NodeErrorView.vue +++ b/packages/editor-ui/src/components/Error/NodeErrorView.vue @@ -22,6 +22,7 @@ import { sanitizeHtml } from '@/utils/htmlUtils'; import { useAIStore } from '@/stores/ai.store'; import { MAX_DISPLAY_DATA_SIZE } from '@/constants'; import VueMarkdown from 'vue-markdown-render'; +import type { BaseTextKey } from '@/plugins/i18n'; const props = defineProps({ error: { @@ -188,6 +189,21 @@ function getErrorDescription(): string { }), ); } + + if (props.error.context?.descriptionKey) { + const interpolate = { + nodeCause: props.error.context.nodeCause as string, + runIndex: (props.error.context.runIndex as string) ?? '0', + itemIndex: (props.error.context.itemIndex as string) ?? '0', + }; + return sanitizeHtml( + i18n.baseText( + `nodeErrorView.description.${props.error.context.descriptionKey as string}` as BaseTextKey, + { interpolate }, + ), + ); + } + if (!props.error.context?.descriptionTemplate) { return sanitizeHtml(props.error.description ?? ''); } @@ -397,7 +413,7 @@ function copySuccess() {
@@ -661,6 +677,19 @@ function copySuccess() { &__header-description { padding: 0 var(--spacing-s) var(--spacing-3xs) var(--spacing-s); font-size: var(--font-size-xs); + + ul { + padding: var(--spacing-s) 0; + padding-left: var(--spacing-l); + } + + code { + font-size: var(--font-size-xs); + color: var(--color-text-base); + background: var(--color-background-base); + padding: var(--spacing-5xs); + border-radius: var(--border-radius-base); + } } &__debugging { diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 2d59fd8cd8d01..86f4c4360f722 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -664,6 +664,8 @@ export const ALLOWED_HTML_TAGS = [ 'a', 'br', 'i', + 'ul', + 'li', 'em', 'small', 'details', diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 0d6455d76f3ed..aac976ad5ef66 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1033,6 +1033,17 @@ "nodeErrorView.theErrorCauseIsTooLargeToBeDisplayed": "The error cause is too large to be displayed", "nodeErrorView.time": "Time", "nodeErrorView.inputPanel.previousNodeError.title": "Error running node '{nodeName}'", + "nodeErrorView.description.pairedItemInvalidInfo": "An expression here won't work because it uses.item
and n8n can't figure out the matching item. This is because the node '{nodeCause}' returned incorrect matching information (for item {itemIndex} of run {runIndex}). .first()
, .last()
or .all()[index]
instead of .item
.",
+ "nodeErrorView.description.pairedItemNoInfo": "An expression here won't work because it uses .item
and n8n can't figure out the matching item. The node '{nodeCause}' didn't return enough information.",
+ "nodeErrorView.description.pairedItemNoInfoCodeNode": "An expression here won't work because it uses .item
and n8n can't figure out the matching item. You can either: .first()
, .last()
or .all()[index]
instead of .item
.item
and n8n can't figure out the matching item. (There are multiple possible matches) .first()
, .last()
or .all()[index]
instead of .item
or reference a different node.",
+ "nodeErrorView.description.pairedItemMultipleMatchesCodeNode": "The code here won't work because it uses .item
and n8n can't figure out the matching item. (There are multiple possible matches) .first()
, .last()
or .all()[index]
instead of .item
or reference a different node.",
+ "nodeErrorView.description.pairedItemPinned": "The item-matching data in that node may be stale. It is needed by an expression in this node that uses .item
.",
"nodeErrorView.debugError.button": "Ask AI ✨",
"nodeErrorView.debugError.loading": "Asking AI.. ✨",
"nodeErrorView.debugError.feedback.reload": "Regenerate answer",
diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts
index a5f2bb7fde86d..c4b043b67fdc3 100644
--- a/packages/workflow/src/WorkflowDataProxy.ts
+++ b/packages/workflow/src/WorkflowDataProxy.ts
@@ -271,9 +271,11 @@ export class WorkflowDataProxy {
}
if (!that.workflow.getNode(nodeName)) {
- throw new ExpressionError(`"${nodeName}" node doesn't exist`, {
+ throw new ExpressionError("Referenced node doesn't exist", {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
+ nodeCause: nodeName,
+ descriptionKey: 'nodeNotFound',
});
}
@@ -281,10 +283,11 @@ export class WorkflowDataProxy {
!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName) &&
!that.workflow.getPinDataOfNode(nodeName)
) {
- throw new ExpressionError(`no data, execute "${nodeName}" node first`, {
+ throw new ExpressionError('Referenced node is unexecuted', {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
type: 'no_node_execution_data',
+ descriptionKey: 'noNodeExecutionData',
nodeCause: nodeName,
});
}
@@ -369,7 +372,12 @@ export class WorkflowDataProxy {
name = name.toString();
if (!node) {
- throw new ExpressionError(`"${nodeName}" node doesn't exist`);
+ throw new ExpressionError("Referenced node doesn't exist", {
+ runIndex: that.runIndex,
+ itemIndex: that.itemIndex,
+ nodeCause: nodeName,
+ descriptionKey: 'nodeNotFound',
+ });
}
if (['binary', 'data', 'json'].includes(name)) {
@@ -380,8 +388,7 @@ export class WorkflowDataProxy {
throw new ExpressionError('No execution data available', {
messageTemplate:
'No execution data available to expression under ‘%%PARAMETER%%’',
- description:
- 'This node has no input data. Please make sure this node is connected to another node.',
+ descriptionKey: 'noInputConnection',
nodeCause: nodeName,
runIndex: that.runIndex,
itemIndex: that.itemIndex,
@@ -589,9 +596,11 @@ export class WorkflowDataProxy {
const nodeName = name.toString();
if (that.workflow.getNode(nodeName) === null) {
- throw new ExpressionError(`"${nodeName}" node doesn't exist`, {
+ throw new ExpressionError("Referenced node doesn't exist", {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
+ nodeCause: nodeName,
+ descriptionKey: 'nodeNotFound',
});
}
@@ -666,10 +675,9 @@ export class WorkflowDataProxy {
if (!context) {
context = {};
}
- message = `‘Node ${nodeName}‘ must be unpinned to execute`;
+ message = `Unpin '${nodeName}' to execute`;
context.messageTemplate = undefined;
- context.description = `To fetch the data for the expression, you must unpin the node '${nodeName}' and execute the workflow again.`;
- context.descriptionTemplate = `To fetch the data for the expression under '%%PARAMETER%%', you must unpin the node '${nodeName}' and execute the workflow again.`;
+ context.descriptionKey = 'pairedItemPinned';
}
if (context.moreInfoLink && (pinData || isScriptingNode(nodeName, that.workflow))) {
@@ -688,6 +696,48 @@ export class WorkflowDataProxy {
});
};
+ const createInvalidPairedItemError = ({ nodeName }: { nodeName: string }) => {
+ return createExpressionError("Can't get data for expression", {
+ messageTemplate: 'Expression info invalid',
+ functionality: 'pairedItem',
+ functionOverrides: {
+ message: "Can't get data",
+ },
+ nodeCause: nodeName,
+ descriptionKey: 'pairedItemInvalidInfo',
+ type: 'paired_item_invalid_info',
+ });
+ };
+
+ const createMissingPairedItemError = (nodeCause: string) => {
+ return createExpressionError("Can't get data for expression", {
+ messageTemplate: 'Info for expression missing from previous node',
+ functionality: 'pairedItem',
+ functionOverrides: {
+ message: "Can't get data",
+ },
+ nodeCause,
+ descriptionKey: isScriptingNode(nodeCause, that.workflow)
+ ? 'pairedItemNoInfoCodeNode'
+ : 'pairedItemNoInfo',
+ causeDetailed: `Missing pairedItem data (node '${nodeCause}' probably didn't supply it)`,
+ type: 'paired_item_no_info',
+ });
+ };
+
+ const createNoConnectionError = (nodeCause: string) => {
+ return createExpressionError('Invalid expression', {
+ messageTemplate: 'No path back to referenced node',
+ functionality: 'pairedItem',
+ descriptionKey: isScriptingNode(nodeCause, that.workflow)
+ ? 'pairedItemNoConnectionCodeNode'
+ : 'pairedItemNoConnection',
+ type: 'paired_item_no_connection',
+ moreInfoLink: true,
+ nodeCause,
+ });
+ };
+
const getPairedItem = (
destinationNodeName: string,
incomingSourceData: ISourceData | null,
@@ -736,42 +786,15 @@ export class WorkflowDataProxy {
const source = taskData?.source ?? [];
if (pairedItem.item >= previousNodeOutputData.length) {
- throw createExpressionError('Can’t get data for expression', {
- messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
- functionality: 'pairedItem',
- functionOverrides: {
- message: 'Can’t get data',
- },
- nodeCause: nodeBeforeLast,
- description: `In node ‘${nodeBeforeLast!}’, output item ${
- currentPairedItem.item || 0
- } ${
- sourceData.previousNodeRun
- ? `of run ${(sourceData.previousNodeRun || 0).toString()} `
- : ''
- }points to an input item on node ‘${
- sourceData.previousNode
- }‘ that doesn’t exist.`,
- type: 'paired_item_invalid_info',
- moreInfoLink: true,
+ throw createInvalidPairedItemError({
+ nodeName: sourceData.previousNode,
});
}
const itemPreviousNode: INodeExecutionData = previousNodeOutputData[pairedItem.item];
if (itemPreviousNode.pairedItem === undefined) {
- throw createExpressionError('Can’t get data for expression', {
- messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
- functionality: 'pairedItem',
- functionOverrides: {
- message: 'Can’t get data',
- },
- nodeCause: sourceData.previousNode,
- description: `To fetch the data from other nodes that this expression needs, more information is needed from the node ‘${sourceData.previousNode}’`,
- causeDetailed: `Missing pairedItem data (node ‘${sourceData.previousNode}’ probably didn’t supply it)`,
- type: 'paired_item_no_info',
- moreInfoLink: true,
- });
+ throw createMissingPairedItemError(sourceData.previousNode);
}
if (Array.isArray(itemPreviousNode.pairedItem)) {
@@ -804,13 +827,17 @@ export class WorkflowDataProxy {
}
throw createExpressionError('Invalid expression', {
- messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
+ messageTemplate: `Multiple matching items for expression [item ${
+ currentPairedItem.item || 0
+ }]`,
functionality: 'pairedItem',
functionOverrides: {
- description: `The code uses data in the node ‘${destinationNodeName}’ but there is more than one matching item in that node`,
- message: 'Invalid code',
+ message: `Multiple matching items for code [item ${currentPairedItem.item || 0}]`,
},
- description: `The expression uses data in the node ‘${destinationNodeName}’ but there is more than one matching item in that node`,
+ nodeCause: destinationNodeName,
+ descriptionKey: isScriptingNode(destinationNodeName, that.workflow)
+ ? 'pairedItemMultipleMatchesCodeNode'
+ : 'pairedItemMultipleMatches',
type: 'paired_item_multiple_matches',
});
}
@@ -833,17 +860,7 @@ export class WorkflowDataProxy {
if (itemInput >= source.length) {
if (source.length === 0) {
// A trigger node got reached, so looks like that that item can not be resolved
- throw createExpressionError('Invalid expression', {
- messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
- functionality: 'pairedItem',
- functionOverrides: {
- description: `The code uses data in the node ‘${destinationNodeName}’ but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
- message: 'Invalid code',
- },
- description: `The expression uses data in the node ‘${destinationNodeName}’ but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
- type: 'paired_item_no_connection',
- moreInfoLink: true,
- });
+ throw createNoConnectionError(destinationNodeName);
}
throw createExpressionError('Can’t get data for expression', {
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
@@ -905,24 +922,8 @@ export class WorkflowDataProxy {
}
if (pairedItem.item >= taskData.data!.main[previousNodeOutput]!.length) {
- throw createExpressionError('Can’t get data for expression', {
- messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
- functionality: 'pairedItem',
- functionOverrides: {
- message: 'Can’t get data',
- },
- nodeCause: nodeBeforeLast,
- description: `In node ‘${nodeBeforeLast!}’, output item ${
- currentPairedItem.item || 0
- } ${
- sourceData.previousNodeRun
- ? `of run ${(sourceData.previousNodeRun || 0).toString()} `
- : ''
- }points to an input item on node ‘${
- sourceData.previousNode
- }‘ that doesn’t exist.`,
- type: 'paired_item_invalid_info',
- moreInfoLink: true,
+ throw createInvalidPairedItemError({
+ nodeName: sourceData.previousNode,
});
}
@@ -937,7 +938,12 @@ export class WorkflowDataProxy {
const referencedNode = that.workflow.getNode(nodeName);
if (referencedNode === null) {
- throw createExpressionError(`"${nodeName}" node doesn't exist`);
+ throw createExpressionError("Referenced node doesn't exist", {
+ runIndex: that.runIndex,
+ itemIndex: that.itemIndex,
+ nodeCause: nodeName,
+ descriptionKey: 'nodeNotFound',
+ });
}
const ensureNodeExecutionData = () => {
@@ -945,8 +951,11 @@ export class WorkflowDataProxy {
!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) &&
!that.workflow.getPinDataOfNode(nodeName)
) {
- throw createExpressionError(`no data, execute "${nodeName}" node first`, {
+ throw createExpressionError('Referenced node is unexecuted', {
+ runIndex: that.runIndex,
+ itemIndex: that.itemIndex,
type: 'no_node_execution_data',
+ descriptionKey: 'noNodeExecutionData',
nodeCause: nodeName,
});
}
@@ -989,17 +998,7 @@ export class WorkflowDataProxy {
}
const parentNodes = that.workflow.getParentNodes(contextNode);
if (!parentNodes.includes(nodeName)) {
- throw createExpressionError('Invalid expression', {
- messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
- functionality: 'pairedItem',
- functionOverrides: {
- description: `The code uses data in the node ‘${nodeName}’ but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
- message: `No path back to node ‘${nodeName}’`,
- },
- description: `The expression uses data in the node ‘${nodeName}’ but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
- nodeCause: nodeName,
- type: 'paired_item_no_connection',
- });
+ throw createNoConnectionError(nodeName);
}
ensureNodeExecutionData();
@@ -1037,17 +1036,7 @@ export class WorkflowDataProxy {
const pairedItem = input.pairedItem as IPairedItemData;
if (pairedItem === undefined) {
- throw createExpressionError('Can’t get data for expression', {
- messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
- functionality: 'pairedItem',
- functionOverrides: {
- description: `To fetch the data from other nodes that this code needs, more information is needed from the node ‘${that.activeNodeName}‘`,
- message: 'Can’t get data',
- },
- description: `To fetch the data from other nodes that this expression needs, more information is needed from the node ‘${that.activeNodeName}‘`,
- causeDetailed: `Missing pairedItem data (node ‘${that.activeNodeName}‘ probably didn’t supply it)`,
- itemIndex,
- });
+ throw createMissingPairedItemError(that.activeNodeName);
}
if (!that.executeData?.source) {
diff --git a/packages/workflow/src/errors/expression.error.ts b/packages/workflow/src/errors/expression.error.ts
index 96a93376bb316..ac73010957153 100644
--- a/packages/workflow/src/errors/expression.error.ts
+++ b/packages/workflow/src/errors/expression.error.ts
@@ -5,6 +5,7 @@ export interface ExpressionErrorOptions {
cause?: Error;
causeDetailed?: string;
description?: string;
+ descriptionKey?: string;
descriptionTemplate?: string;
functionality?: 'pairedItem';
itemIndex?: number;
@@ -38,6 +39,7 @@ export class ExpressionError extends ExecutionBaseError {
const allowedKeys = [
'causeDetailed',
'descriptionTemplate',
+ 'descriptionKey',
'functionality',
'itemIndex',
'messageTemplate',
diff --git a/packages/workflow/test/WorkflowDataProxy.test.ts b/packages/workflow/test/WorkflowDataProxy.test.ts
index 7137af6d57737..98f0a3959c1db 100644
--- a/packages/workflow/test/WorkflowDataProxy.test.ts
+++ b/packages/workflow/test/WorkflowDataProxy.test.ts
@@ -162,7 +162,7 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
- expect(exprError.message).toEqual('"does not exist" node doesn\'t exist');
+ expect(exprError.message).toEqual("Referenced node doesn't exist");
done();
}
});
@@ -193,7 +193,7 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
- expect(exprError.message).toEqual('no data, execute "Impossible" node first');
+ expect(exprError.message).toEqual('Referenced node is unexecuted');
expect(exprError.context.type).toEqual('no_node_execution_data');
done();
}
@@ -221,7 +221,7 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
- expect(exprError.message).toEqual('no data, execute "Impossible if" node first');
+ expect(exprError.message).toEqual('Referenced node is unexecuted');
expect(exprError.context.type).toEqual('no_node_execution_data');
done();
}
@@ -263,7 +263,7 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
- expect(exprError.message).toEqual('Can’t get data for expression');
+ expect(exprError.message).toEqual("Can't get data for expression");
expect(exprError.context.type).toEqual('paired_item_no_info');
done();
}
@@ -277,7 +277,7 @@ describe('WorkflowDataProxy', () => {
} catch (error) {
expect(error).toBeInstanceOf(ExpressionError);
const exprError = error as ExpressionError;
- expect(exprError.message).toEqual('Can’t get data for expression');
+ expect(exprError.message).toEqual("Can't get data for expression");
expect(exprError.context.type).toEqual('paired_item_invalid_info');
done();
}