feat(core): Add optional Error-Output (#7460)

Add an additional optional error output to which all items get sent that
could not be processed.
![Screenshot from 2023-10-18
17-29-15](https://github.com/n8n-io/n8n/assets/6249596/e9732807-ab2b-4662-a5f6-bdff24f7ad55)

Github issue / Community forum post (link here to close automatically):
https://community.n8n.io/t/error-connector-for-nodes/3094

https://community.n8n.io/t/error-handling-at-node-level-detect-node-execution-status/26791

---------

Co-authored-by: OlegIvaniv <me@olegivaniv.com>
This commit is contained in:
Jan Oberhauser
2023-10-30 18:42:47 +01:00
committed by GitHub
parent 442c73e63b
commit 655efeaf66
20 changed files with 1090 additions and 61 deletions

View File

@@ -27,6 +27,7 @@ import * as NodeViewUtils from '@/utils/nodeViewUtils';
import { useHistoryStore } from '@/stores/history.store';
import { useCanvasStore } from '@/stores/canvas.store';
import type { EndpointSpec } from '@jsplumb/common';
import { getStyleTokenValue } from '@/utils';
const createAddInputEndpointSpec = (
connectionName: NodeConnectionType,
@@ -339,13 +340,13 @@ export const nodeBase = defineComponent({
} = {};
const workflow = this.workflowsStore.getCurrentWorkflow();
const outputs = NodeHelpers.getNodeOutputs(workflow, this.data, nodeTypeData) || [];
this.outputs = outputs;
this.outputs = NodeHelpers.getNodeOutputs(workflow, this.data, nodeTypeData) || [];
// TODO: There are still a lot of references of "main" in NodesView and
// other locations. So assume there will be more problems
outputs.forEach((value, i) => {
let maxLabelLength = 0;
const outputConfigurations: INodeOutputConfiguration[] = [];
this.outputs.forEach((value, i) => {
let outputConfiguration: INodeOutputConfiguration;
if (typeof value === 'string') {
outputConfiguration = {
@@ -354,6 +355,24 @@ export const nodeBase = defineComponent({
} else {
outputConfiguration = value;
}
if (nodeTypeData.outputNames?.[i]) {
outputConfiguration.displayName = nodeTypeData.outputNames[i];
}
if (outputConfiguration.displayName) {
maxLabelLength =
outputConfiguration.displayName.length > maxLabelLength
? outputConfiguration.displayName.length
: maxLabelLength;
}
outputConfigurations.push(outputConfiguration);
});
const endpointLabelLength = maxLabelLength < 4 ? 'short' : 'medium';
this.outputs.forEach((value, i) => {
const outputConfiguration = outputConfigurations[i];
const outputName: ConnectionTypes = outputConfiguration.type;
@@ -376,7 +395,7 @@ export const nodeBase = defineComponent({
const rootTypeIndex = rootTypeIndexData[rootCategoryOutputName];
const typeIndex = typeIndexData[outputName];
const outputsOfSameRootType = outputs.filter((outputData) => {
const outputsOfSameRootType = this.outputs.filter((outputData) => {
const thisOutputName: string =
typeof outputData === 'string' ? outputData : outputData.type;
return outputName === NodeConnectionType.Main
@@ -417,7 +436,7 @@ export const nodeBase = defineComponent({
hoverClass: 'dot-output-endpoint-hover',
connectionsDirected: true,
dragAllowedWhenFull: false,
...this.__getOutputConnectionStyle(outputName, nodeTypeData),
...this.__getOutputConnectionStyle(outputName, outputConfiguration, nodeTypeData),
};
const endpoint = this.instance.addEndpoint(
@@ -426,11 +445,12 @@ export const nodeBase = defineComponent({
);
this.__addEndpointTestingData(endpoint, 'output', typeIndex);
if (outputConfiguration.displayName || nodeTypeData.outputNames?.[i]) {
if (outputConfiguration.displayName) {
// Apply output names if they got set
const overlaySpec = NodeViewUtils.getOutputNameOverlay(
outputConfiguration.displayName || nodeTypeData.outputNames[i],
outputConfiguration.displayName,
outputName,
outputConfiguration?.category,
);
endpoint.addOverlay(overlaySpec);
}
@@ -441,7 +461,7 @@ export const nodeBase = defineComponent({
nodeId: this.nodeId,
index: typeIndex,
totalEndpoints: outputsOfSameRootType.length,
nodeType: node.type,
endpointLabelLength,
};
}
@@ -455,9 +475,9 @@ export const nodeBase = defineComponent({
options: {
dimensions: 24,
connectedEndpoint: endpoint,
showOutputLabel: outputs.length === 1,
size: outputs.length >= 3 ? 'small' : 'medium',
nodeType: node.type,
showOutputLabel: this.outputs.length === 1,
size: this.outputs.length >= 3 ? 'small' : 'medium',
endpointLabelLength,
hoverMessage: this.$locale.baseText('nodeBase.clickToAddNodeOrDragToConnect'),
},
},
@@ -475,10 +495,16 @@ export const nodeBase = defineComponent({
nodeId: this.nodeId,
type: outputName,
index: typeIndex,
category: outputConfiguration?.category,
},
cssClass: 'plus-draggable-endpoint',
dragAllowedWhenFull: false,
};
if (outputConfiguration?.category) {
plusEndpointData.cssClass = `${plusEndpointData.cssClass} ${outputConfiguration?.category}`;
}
const plusEndpoint = this.instance.addEndpoint(
this.$refs[this.data.name] as Element,
plusEndpointData,
@@ -538,6 +564,7 @@ export const nodeBase = defineComponent({
},
__getOutputConnectionStyle(
connectionType: ConnectionTypes,
outputConfiguration: INodeOutputConfiguration,
nodeTypeData: INodeTypeDescription,
): EndpointOptions {
const type = 'output';
@@ -557,6 +584,18 @@ export const nodeBase = defineComponent({
});
if (connectionType === NodeConnectionType.Main) {
if (outputConfiguration.category === 'error') {
return {
paintStyle: {
...NodeViewUtils.getOutputEndpointStyle(
nodeTypeData,
this.__getEndpointColor(NodeConnectionType.Main),
),
fill: getStyleTokenValue('--node-error-output-color', true),
},
cssClass: `dot-${type}-endpoint`,
};
}
return {
paintStyle: NodeViewUtils.getOutputEndpointStyle(
nodeTypeData,

View File

@@ -649,6 +649,7 @@ export const workflowHelpers = defineComponent({
'credentials',
'disabled',
'issues',
'onError',
'notes',
'parameters',
'status',
@@ -725,14 +726,16 @@ export const workflowHelpers = defineComponent({
}
}
// Save the disabled property and continueOnFail only when is set
// Save the disabled property, continueOnFail and onError only when is set
if (node.disabled === true) {
nodeData.disabled = true;
}
if (node.continueOnFail === true) {
nodeData.continueOnFail = true;
}
if (node.onError !== 'stopWorkflow') {
nodeData.onError = node.onError;
}
// Save the notes only if when they contain data
if (![undefined, ''].includes(node.notes)) {
nodeData.notes = node.notes;