mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
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:
parent
442c73e63b
commit
655efeaf66
|
@ -34,9 +34,9 @@ properties:
|
|||
type: number
|
||||
waitBetweenTries:
|
||||
type: number
|
||||
continueOnFail:
|
||||
type: boolean
|
||||
example: false
|
||||
onError:
|
||||
type: string
|
||||
example: 'stopWorkflow'
|
||||
position:
|
||||
type: array
|
||||
items:
|
||||
|
|
|
@ -2289,7 +2289,13 @@ export function getNodeParameter(
|
|||
*
|
||||
*/
|
||||
export function continueOnFail(node: INode): boolean {
|
||||
return get(node, 'continueOnFail', false);
|
||||
const onError = get(node, 'onError', undefined);
|
||||
|
||||
if (onError === undefined) {
|
||||
return get(node, 'continueOnFail', false);
|
||||
}
|
||||
|
||||
return ['continueRegularOutput', 'continueErrorOutput'].includes(onError);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,6 +9,7 @@ import PCancelable from 'p-cancelable';
|
|||
import type {
|
||||
ExecutionError,
|
||||
ExecutionStatus,
|
||||
GenericValue,
|
||||
IConnection,
|
||||
IDataObject,
|
||||
IExecuteData,
|
||||
|
@ -36,6 +37,8 @@ import {
|
|||
IRunExecutionData,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
WorkflowExecuteMode,
|
||||
NodeHelpers,
|
||||
NodeConnectionType,
|
||||
} from 'n8n-workflow';
|
||||
import get from 'lodash/get';
|
||||
import * as NodeExecuteFunctions from './NodeExecuteFunctions';
|
||||
|
@ -1041,6 +1044,7 @@ export class WorkflowExecute {
|
|||
node: executionNode.name,
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
|
||||
const runNodeData = await workflow.runNode(
|
||||
executionData,
|
||||
this.runExecutionData,
|
||||
|
@ -1051,6 +1055,112 @@ export class WorkflowExecute {
|
|||
);
|
||||
nodeSuccessData = runNodeData.data;
|
||||
|
||||
if (nodeSuccessData && executionData.node.onError === 'continueErrorOutput') {
|
||||
// If errorOutput is activated check all the output items for error data.
|
||||
// If any is found, route them to the last output as that will be the
|
||||
// error output.
|
||||
|
||||
const nodeType = workflow.nodeTypes.getByNameAndVersion(
|
||||
executionData.node.type,
|
||||
executionData.node.typeVersion,
|
||||
);
|
||||
const outputs = NodeHelpers.getNodeOutputs(
|
||||
workflow,
|
||||
executionData.node,
|
||||
nodeType.description,
|
||||
);
|
||||
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
||||
const mainOutputTypes = outputTypes.filter(
|
||||
(output) => output === NodeConnectionType.Main,
|
||||
);
|
||||
|
||||
const errorItems: INodeExecutionData[] = [];
|
||||
const successItems: INodeExecutionData[] = [];
|
||||
|
||||
// Create a WorkflowDataProxy instance that we can get the data of the
|
||||
// item which did error
|
||||
const executeFunctions = NodeExecuteFunctions.getExecuteFunctions(
|
||||
workflow,
|
||||
this.runExecutionData,
|
||||
runIndex,
|
||||
[],
|
||||
executionData.data,
|
||||
executionData.node,
|
||||
this.additionalData,
|
||||
executionData,
|
||||
this.mode,
|
||||
);
|
||||
const dataProxy = executeFunctions.getWorkflowDataProxy(0);
|
||||
|
||||
// Loop over all outputs except the error output as it would not contain data by default
|
||||
for (
|
||||
let outputIndex = 0;
|
||||
outputIndex < mainOutputTypes.length - 1;
|
||||
outputIndex++
|
||||
) {
|
||||
successItems.length = 0;
|
||||
const items = nodeSuccessData.length ? nodeSuccessData[0] : [];
|
||||
|
||||
while (items.length) {
|
||||
const item = items.pop();
|
||||
if (item === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let errorData: GenericValue | undefined;
|
||||
if (item.error) {
|
||||
errorData = item.error;
|
||||
item.error = undefined;
|
||||
} else if (item.json.error && Object.keys(item.json).length === 1) {
|
||||
errorData = item.json.error;
|
||||
}
|
||||
|
||||
if (errorData) {
|
||||
const pairedItemData =
|
||||
item.pairedItem && typeof item.pairedItem === 'object'
|
||||
? Array.isArray(item.pairedItem)
|
||||
? item.pairedItem[0]
|
||||
: item.pairedItem
|
||||
: undefined;
|
||||
|
||||
if (executionData!.source === null || pairedItemData === undefined) {
|
||||
// Source data is missing for some reason so we can not figure out the item
|
||||
errorItems.push(item);
|
||||
} else {
|
||||
const pairedItemInputIndex = pairedItemData.input || 0;
|
||||
|
||||
const sourceData =
|
||||
executionData!.source[NodeConnectionType.Main][pairedItemInputIndex];
|
||||
|
||||
const constPairedItem = dataProxy.$getPairedItem(
|
||||
sourceData!.previousNode,
|
||||
sourceData,
|
||||
pairedItemData,
|
||||
);
|
||||
|
||||
if (constPairedItem === null) {
|
||||
errorItems.push(item);
|
||||
} else {
|
||||
errorItems.push({
|
||||
...item,
|
||||
json: {
|
||||
...constPairedItem.json,
|
||||
...item.json,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
successItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
nodeSuccessData[outputIndex] = successItems;
|
||||
}
|
||||
|
||||
nodeSuccessData[mainOutputTypes.length - 1] = errorItems;
|
||||
}
|
||||
|
||||
if (runNodeData.closeFunction) {
|
||||
// Explanation why we do this can be found in n8n-workflow/Workflow.ts -> runNode
|
||||
|
||||
|
@ -1180,7 +1290,12 @@ export class WorkflowExecute {
|
|||
taskData.error = executionError;
|
||||
taskData.executionStatus = 'error';
|
||||
|
||||
if (executionData.node.continueOnFail === true) {
|
||||
if (
|
||||
executionData.node.continueOnFail === true ||
|
||||
['continueRegularOutput', 'continueErrorOutput'].includes(
|
||||
executionData.node.onError || '',
|
||||
)
|
||||
) {
|
||||
// Workflow should continue running even if node errors
|
||||
if (executionData.data.hasOwnProperty('main') && executionData.data.main.length > 0) {
|
||||
// Simply get the input data of the node if it has any and pass it through
|
||||
|
|
734
packages/core/test/workflows/error_outputs.json
Normal file
734
packages/core/test/workflows/error_outputs.json
Normal file
|
@ -0,0 +1,734 @@
|
|||
{
|
||||
"name": "Error Output - Test Workflow",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "c41b46f0-3e76-4655-b5ea-4d15af58c138",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-680, 460]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "originalName",
|
||||
"stringValue": "={{ $('Mock Data').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "247f4118-d80f-49ab-8d9a-0cdbbb9271df",
|
||||
"name": "Success",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [200, 860]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "originalName",
|
||||
"stringValue": "={{ $('Mock Data').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "311e3650-d89c-405a-9c8d-c238f48a8a5a",
|
||||
"name": "Error",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [200, 1040]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return [\n {\n \"id\": \"23423532\",\n \"name\": \"Jay Gatsby\",\n \"email\": \"gatsby@west-egg.com\",\n \"notes\": \"Keeps asking about a green light??\",\n \"country\": \"US\",\n \"created\": \"1925-04-10\"\n },\n {\n \"id\": \"23423533\",\n \"name\": \"José Arcadio Buendía\",\n \"email\": \"jab@macondo.co\",\n \"notes\": \"Lots of people named after him. Very confusing\",\n \"country\": \"CO\",\n \"created\": \"1967-05-05\"\n },\n {\n \"id\": \"23423534\",\n \"name\": \"Max Sendak\",\n \"email\": \"info@in-and-out-of-weeks.org\",\n \"notes\": \"Keeps rolling his terrible eyes\",\n \"country\": \"US\",\n \"created\": \"1963-04-09\"\n },\n {\n \"id\": \"23423535\",\n \"name\": \"Zaphod Beeblebrox\",\n \"email\": \"captain@heartofgold.com\",\n \"notes\": \"Felt like I was talking to more than one person\",\n \"country\": null,\n \"created\": \"1979-10-12\"\n },\n {\n \"id\": \"23423536\",\n \"name\": \"Edmund Pevensie\",\n \"email\": \"edmund@narnia.gov\",\n \"notes\": \"Passionate sailor\",\n \"country\": \"UK\",\n \"created\": \"1950-10-16\"\n }\n]"
|
||||
},
|
||||
"id": "179d4fe7-1ae7-4957-a77d-12c3ca6d141b",
|
||||
"name": "Mock Data",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [-460, 460]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## On Error: Continue (using error output)",
|
||||
"height": 414,
|
||||
"width": 564
|
||||
},
|
||||
"id": "1ec2a8b6-54e2-4319-90b3-30b387855b36",
|
||||
"name": "Sticky Note",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [-160, 780]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## Continue On Fail (deprecated)",
|
||||
"height": 279,
|
||||
"width": 564
|
||||
},
|
||||
"id": "49a2b7d9-8bd1-4cdf-9649-2d93668b0f8f",
|
||||
"name": "Sticky Note1",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [-160, 140]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "originalName",
|
||||
"stringValue": "={{ $('Mock Data').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "9852f1d9-95b4-4ef7-bb18-8f0bab81a0bc",
|
||||
"name": "Combined",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [180, 240]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "// Add a new field called 'myNewField' to the JSON of the item\n$input.item.json.myNewField = 1;\n\nif ($input.item.json.country === 'US') {\n throw new Error('This is an error');\n}\n\nreturn $input.item;"
|
||||
},
|
||||
"id": "40d4dba3-3db7-4eb5-aa27-e76f955a5e09",
|
||||
"name": "Throw Error",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [-140, 960],
|
||||
"errorOutput": true,
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## On Error: Continue",
|
||||
"height": 279,
|
||||
"width": 564
|
||||
},
|
||||
"id": "8eb3dd54-c1dd-4167-abfa-c06d044c63f3",
|
||||
"name": "Sticky Note2",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [-160, 460]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "// Add a new field called 'myNewField' to the JSON of the item\n$input.item.json.myNewField = 1;\n\nif ($input.item.json.country === 'US') {\n throw new Error('This is an error');\n}\n\nreturn $input.item;"
|
||||
},
|
||||
"id": "19a3d6ac-e610-4296-9b7a-9ed19d242bdb",
|
||||
"name": "Throw Error2",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [-120, 560],
|
||||
"onError": "continueRegularOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "originalName",
|
||||
"stringValue": "={{ $('Mock Data').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "5f803fdc-7d88-4c12-8886-6092cfbc03c6",
|
||||
"name": "Combined1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [180, 560]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "// Add a new field called 'myNewField' to the JSON of the item\n$input.item.json.myNewField = 1;\n\nif ($input.item.json.country === 'US') {\n throw new Error('This is an error');\n}\n\nreturn $input.item;"
|
||||
},
|
||||
"id": "c2696c1f-1abd-4549-9ad9-e62017dc14b8",
|
||||
"name": "Throw Error1",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [-120, 240],
|
||||
"continueOnFail": true
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "originalName",
|
||||
"stringValue": "={{ $('Mock Data').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "01740d7e-e572-408a-9fae-729068803113",
|
||||
"name": "Success1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [200, 1320]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## On Error: Continue (using error output) + Make sure error data gets removed",
|
||||
"height": 509.71047006830065,
|
||||
"width": 1183.725293692246
|
||||
},
|
||||
"id": "ed409181-4847-4d65-af45-f45078a6343e",
|
||||
"name": "Sticky Note3",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [-160, 1240]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "runOnceForEachItem",
|
||||
"jsCode": "// Add a new field called 'myNewField' to the JSON of the item\n$input.item.json.myNewField = 1;\n\nif ($input.item.json.country === 'US') {\n throw new Error('This is an error');\n}\n\nreturn $input.item;"
|
||||
},
|
||||
"id": "93d03f38-b928-4b4b-832a-3f1a5deebb2d",
|
||||
"name": "Throw Error3",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [-140, 1420],
|
||||
"errorOutput": true,
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "c92a6ce5-41ea-4fb9-a07b-c4e98f905b12",
|
||||
"name": "Edit Fields",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [420, 1500],
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "originalName",
|
||||
"stringValue": "={{ $('Mock Data').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "ab838cc1-0987-4b41-bdc5-fe17f38e0691",
|
||||
"name": "Success2",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [700, 1360]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "originalName",
|
||||
"stringValue": "={{ $('Mock Data').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "22e04172-19b9-4735-9dd0-a3e2fa3bf000",
|
||||
"name": "Error2",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [700, 1580]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "originalName",
|
||||
"stringValue": "={{ $('Mock Data').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "69e7257a-1ba8-46ba-9394-d38d65b2e567",
|
||||
"name": "Error1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [200, 1500]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Error": [
|
||||
{
|
||||
"json": {
|
||||
"id": "23423534",
|
||||
"name": "Max Sendak",
|
||||
"email": "info@in-and-out-of-weeks.org",
|
||||
"notes": "Keeps rolling his terrible eyes",
|
||||
"country": "US",
|
||||
"created": "1963-04-09",
|
||||
"error": "This is an error [line 5, for item 2]",
|
||||
"originalName": "Max Sendak"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423532",
|
||||
"name": "Jay Gatsby",
|
||||
"email": "gatsby@west-egg.com",
|
||||
"notes": "Keeps asking about a green light??",
|
||||
"country": "US",
|
||||
"created": "1925-04-10",
|
||||
"error": "This is an error [line 5, for item 0]",
|
||||
"originalName": "Jay Gatsby"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"Success": [
|
||||
{
|
||||
"json": {
|
||||
"id": "23423536",
|
||||
"name": "Edmund Pevensie",
|
||||
"email": "edmund@narnia.gov",
|
||||
"notes": "Passionate sailor",
|
||||
"country": "UK",
|
||||
"created": "1950-10-16",
|
||||
"myNewField": 1,
|
||||
"originalName": "Edmund Pevensie"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423535",
|
||||
"name": "Zaphod Beeblebrox",
|
||||
"email": "captain@heartofgold.com",
|
||||
"notes": "Felt like I was talking to more than one person",
|
||||
"country": null,
|
||||
"created": "1979-10-12",
|
||||
"myNewField": 1,
|
||||
"originalName": "Zaphod Beeblebrox"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423533",
|
||||
"name": "José Arcadio Buendía",
|
||||
"email": "jab@macondo.co",
|
||||
"notes": "Lots of people named after him. Very confusing",
|
||||
"country": "CO",
|
||||
"created": "1967-05-05",
|
||||
"myNewField": 1,
|
||||
"originalName": "José Arcadio Buendía"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"Combined": [
|
||||
{
|
||||
"json": {
|
||||
"error": "This is an error [line 5, for item 0]",
|
||||
"originalName": "Jay Gatsby"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423533",
|
||||
"name": "José Arcadio Buendía",
|
||||
"email": "jab@macondo.co",
|
||||
"notes": "Lots of people named after him. Very confusing",
|
||||
"country": "CO",
|
||||
"created": "1967-05-05",
|
||||
"myNewField": 1,
|
||||
"originalName": "José Arcadio Buendía"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"error": "This is an error [line 5, for item 2]",
|
||||
"originalName": "Max Sendak"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423535",
|
||||
"name": "Zaphod Beeblebrox",
|
||||
"email": "captain@heartofgold.com",
|
||||
"notes": "Felt like I was talking to more than one person",
|
||||
"country": null,
|
||||
"created": "1979-10-12",
|
||||
"myNewField": 1,
|
||||
"originalName": "Zaphod Beeblebrox"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423536",
|
||||
"name": "Edmund Pevensie",
|
||||
"email": "edmund@narnia.gov",
|
||||
"notes": "Passionate sailor",
|
||||
"country": "UK",
|
||||
"created": "1950-10-16",
|
||||
"myNewField": 1,
|
||||
"originalName": "Edmund Pevensie"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"Combined1": [
|
||||
{
|
||||
"json": {
|
||||
"error": "This is an error [line 5, for item 0]",
|
||||
"originalName": "Jay Gatsby"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423533",
|
||||
"name": "José Arcadio Buendía",
|
||||
"email": "jab@macondo.co",
|
||||
"notes": "Lots of people named after him. Very confusing",
|
||||
"country": "CO",
|
||||
"created": "1967-05-05",
|
||||
"myNewField": 1,
|
||||
"originalName": "José Arcadio Buendía"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"error": "This is an error [line 5, for item 2]",
|
||||
"originalName": "Max Sendak"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423535",
|
||||
"name": "Zaphod Beeblebrox",
|
||||
"email": "captain@heartofgold.com",
|
||||
"notes": "Felt like I was talking to more than one person",
|
||||
"country": null,
|
||||
"created": "1979-10-12",
|
||||
"myNewField": 1,
|
||||
"originalName": "Zaphod Beeblebrox"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423536",
|
||||
"name": "Edmund Pevensie",
|
||||
"email": "edmund@narnia.gov",
|
||||
"notes": "Passionate sailor",
|
||||
"country": "UK",
|
||||
"created": "1950-10-16",
|
||||
"myNewField": 1,
|
||||
"originalName": "Edmund Pevensie"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"Success1": [
|
||||
{
|
||||
"json": {
|
||||
"id": "23423536",
|
||||
"name": "Edmund Pevensie",
|
||||
"email": "edmund@narnia.gov",
|
||||
"notes": "Passionate sailor",
|
||||
"country": "UK",
|
||||
"created": "1950-10-16",
|
||||
"myNewField": 1,
|
||||
"originalName": "Edmund Pevensie"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423535",
|
||||
"name": "Zaphod Beeblebrox",
|
||||
"email": "captain@heartofgold.com",
|
||||
"notes": "Felt like I was talking to more than one person",
|
||||
"country": null,
|
||||
"created": "1979-10-12",
|
||||
"myNewField": 1,
|
||||
"originalName": "Zaphod Beeblebrox"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423533",
|
||||
"name": "José Arcadio Buendía",
|
||||
"email": "jab@macondo.co",
|
||||
"notes": "Lots of people named after him. Very confusing",
|
||||
"country": "CO",
|
||||
"created": "1967-05-05",
|
||||
"myNewField": 1,
|
||||
"originalName": "José Arcadio Buendía"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"Error1": [
|
||||
{
|
||||
"json": {
|
||||
"id": "23423534",
|
||||
"name": "Max Sendak",
|
||||
"email": "info@in-and-out-of-weeks.org",
|
||||
"notes": "Keeps rolling his terrible eyes",
|
||||
"country": "US",
|
||||
"created": "1963-04-09",
|
||||
"error": "This is an error [line 5, for item 2]",
|
||||
"originalName": "Max Sendak"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423532",
|
||||
"name": "Jay Gatsby",
|
||||
"email": "gatsby@west-egg.com",
|
||||
"notes": "Keeps asking about a green light??",
|
||||
"country": "US",
|
||||
"created": "1925-04-10",
|
||||
"error": "This is an error [line 5, for item 0]",
|
||||
"originalName": "Jay Gatsby"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"Success2": [
|
||||
{
|
||||
"json": {
|
||||
"id": "23423532",
|
||||
"name": "Jay Gatsby",
|
||||
"email": "gatsby@west-egg.com",
|
||||
"notes": "Keeps asking about a green light??",
|
||||
"country": "US",
|
||||
"created": "1925-04-10",
|
||||
"error": "This is an error [line 5, for item 0]",
|
||||
"originalName": "Jay Gatsby"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "23423534",
|
||||
"name": "Max Sendak",
|
||||
"email": "info@in-and-out-of-weeks.org",
|
||||
"notes": "Keeps rolling his terrible eyes",
|
||||
"country": "US",
|
||||
"created": "1963-04-09",
|
||||
"error": "This is an error [line 5, for item 2]",
|
||||
"originalName": "Max Sendak"
|
||||
},
|
||||
"pairedItem": {
|
||||
"item": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Mock Data",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Mock Data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Throw Error",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Throw Error2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Throw Error1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Throw Error3",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Throw Error": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Success",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Error",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Throw Error2": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Combined1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Throw Error1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Combined",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Throw Error3": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Success1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Error1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Edit Fields": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Success2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Error2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Error1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "e73e1eda-293c-4ee2-87b9-923873241774",
|
||||
"id": "UgoluWRMeg7fPLCB",
|
||||
"meta": {
|
||||
"instanceId": "021d3c82ba2d3bc090cbf4fc81c9312668bcc34297e022bb3438c5c88a43a5ff"
|
||||
},
|
||||
"tags": []
|
||||
}
|
|
@ -1297,6 +1297,15 @@ export default defineComponent({
|
|||
stroke: var(--color-foreground-xdark);
|
||||
}
|
||||
|
||||
&.error {
|
||||
path {
|
||||
fill: var(--node-error-output-color);
|
||||
}
|
||||
rect {
|
||||
stroke: var(--node-error-output-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
margin-left: calc((var(--stalk-size) + var(--plus-endpoint-box-size-small) / 2));
|
||||
g {
|
||||
|
@ -1427,6 +1436,10 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
.node-output-endpoint-label.node-connection-category-error {
|
||||
color: var(--node-error-output-color);
|
||||
}
|
||||
|
||||
.node-output-endpoint-label {
|
||||
margin-left: calc(var(--endpoint-size-small) + var(--spacing-2xs));
|
||||
|
||||
|
@ -1436,9 +1449,9 @@ export default defineComponent({
|
|||
margin-left: 0;
|
||||
}
|
||||
|
||||
// Switch node allows for dynamic connection labels
|
||||
// Some nodes allow for dynamic connection labels
|
||||
// so we need to make sure the label does not overflow
|
||||
&[data-endpoint-node-type='n8n-nodes-base.switch'] {
|
||||
&[data-endpoint-label-length='medium'] {
|
||||
max-width: calc(var(--stalk-size) - (var(--endpoint-size-small)));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -1495,7 +1508,8 @@ export default defineComponent({
|
|||
.ep-success--without-label {
|
||||
--stalk-size: var(--stalk-success-size-without-label);
|
||||
}
|
||||
[data-endpoint-node-type='n8n-nodes-base.switch'] {
|
||||
|
||||
[data-endpoint-label-length='medium'] {
|
||||
--stalk-size: var(--stalk-switch-size);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -106,6 +106,7 @@
|
|||
@valueChanged="valueChanged"
|
||||
@execute="onNodeExecute"
|
||||
@stopExecution="onStopExecution"
|
||||
@redrawRequired="redrawRequired = true"
|
||||
@activate="onWorkflowActivate"
|
||||
/>
|
||||
<a
|
||||
|
@ -199,6 +200,7 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
settingsEventBus: createEventBus(),
|
||||
redrawRequired: false,
|
||||
runInputIndex: -1,
|
||||
runOutputIndex: -1,
|
||||
isLinkingEnabled: true,
|
||||
|
@ -633,7 +635,8 @@ export default defineComponent({
|
|||
|
||||
if (
|
||||
typeof this.activeNodeType?.outputs === 'string' ||
|
||||
typeof this.activeNodeType?.inputs === 'string'
|
||||
typeof this.activeNodeType?.inputs === 'string' ||
|
||||
this.redrawRequired
|
||||
) {
|
||||
// TODO: We should keep track of if it actually changed and only do if required
|
||||
// Whenever a node with custom inputs and outputs gets closed redraw it in case
|
||||
|
|
|
@ -371,7 +371,7 @@ export default defineComponent({
|
|||
alwaysOutputData: false,
|
||||
executeOnce: false,
|
||||
notesInFlow: false,
|
||||
continueOnFail: false,
|
||||
onError: 'stopWorkflow',
|
||||
retryOnFail: false,
|
||||
maxTries: 3,
|
||||
waitBetweenTries: 1000,
|
||||
|
@ -440,12 +440,39 @@ export default defineComponent({
|
|||
description: this.$locale.baseText('nodeSettings.waitBetweenTries.description'),
|
||||
},
|
||||
{
|
||||
displayName: this.$locale.baseText('nodeSettings.continueOnFail.displayName'),
|
||||
name: 'continueOnFail',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayName: this.$locale.baseText('nodeSettings.onError.displayName'),
|
||||
name: 'onError',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: this.$locale.baseText('nodeSettings.onError.options.stopWorkflow.displayName'),
|
||||
value: 'stopWorkflow',
|
||||
description: this.$locale.baseText(
|
||||
'nodeSettings.onError.options.stopWorkflow.description',
|
||||
),
|
||||
},
|
||||
{
|
||||
name: this.$locale.baseText(
|
||||
'nodeSettings.onError.options.continueRegularOutput.displayName',
|
||||
),
|
||||
value: 'continueRegularOutput',
|
||||
description: this.$locale.baseText(
|
||||
'nodeSettings.onError.options.continueRegularOutput.description',
|
||||
),
|
||||
},
|
||||
{
|
||||
name: this.$locale.baseText(
|
||||
'nodeSettings.onError.options.continueErrorOutput.displayName',
|
||||
),
|
||||
value: 'continueErrorOutput',
|
||||
description: this.$locale.baseText(
|
||||
'nodeSettings.onError.options.continueErrorOutput.description',
|
||||
),
|
||||
},
|
||||
],
|
||||
default: 'stopWorkflow',
|
||||
noDataExpression: true,
|
||||
description: this.$locale.baseText('nodeSettings.continueOnFail.description'),
|
||||
description: this.$locale.baseText('nodeSettings.onError.description'),
|
||||
},
|
||||
{
|
||||
displayName: this.$locale.baseText('nodeSettings.notes.displayName'),
|
||||
|
@ -633,6 +660,11 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
if (parameterData.name === 'onError') {
|
||||
// If that parameter changes, we need to redraw the connections, as the error output may need to be added or removed
|
||||
this.$emit('redrawRequired');
|
||||
}
|
||||
|
||||
if (parameterData.name === 'name') {
|
||||
// Name of node changed so we have to set also the new node name as active
|
||||
|
||||
|
@ -880,10 +912,18 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
if (this.node.continueOnFail) {
|
||||
foundNodeSettings.push('continueOnFail');
|
||||
foundNodeSettings.push('onError');
|
||||
this.nodeValues = {
|
||||
...this.nodeValues,
|
||||
continueOnFail: this.node.continueOnFail,
|
||||
onError: 'continueRegularOutput',
|
||||
};
|
||||
}
|
||||
|
||||
if (this.node.onError) {
|
||||
foundNodeSettings.push('onError');
|
||||
this.nodeValues = {
|
||||
...this.nodeValues,
|
||||
onError: this.node.onError,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -174,6 +174,7 @@
|
|||
var(--node-type-ai_vectorStore-color-s),
|
||||
var(--node-type-background-l)
|
||||
);
|
||||
--node-error-output-color: #991818;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
|
|
|
@ -941,8 +941,14 @@
|
|||
"nodeSettings.alwaysOutputData.description": "If active, will output a single, empty item when the output would have been empty. Use to prevent the workflow finishing on this node.",
|
||||
"nodeSettings.alwaysOutputData.displayName": "Always Output Data",
|
||||
"nodeSettings.clickOnTheQuestionMarkIcon": "Click the '?' icon to open this node on n8n.io",
|
||||
"nodeSettings.continueOnFail.description": "If active, the workflow continues even if this node's execution fails. When this occurs, the node passes along input data from previous nodes - so your workflow should account for unexpected output data.",
|
||||
"nodeSettings.continueOnFail.displayName": "Continue On Fail",
|
||||
"nodeSettings.onError.description": "Action to take when the node execution fails",
|
||||
"nodeSettings.onError.displayName": "On Error",
|
||||
"nodeSettings.onError.options.continueRegularOutput.description": "Pass error message as item in regular output",
|
||||
"nodeSettings.onError.options.continueRegularOutput.displayName": "Continue",
|
||||
"nodeSettings.onError.options.continueErrorOutput.description": "Pass item to an extra `error` output",
|
||||
"nodeSettings.onError.options.continueErrorOutput.displayName": "Continue (using error output)",
|
||||
"nodeSettings.onError.options.stopWorkflow.description": "Halt execution and fail workflow",
|
||||
"nodeSettings.onError.options.stopWorkflow.displayName": "Stop Workflow",
|
||||
"nodeSettings.docs": "Docs",
|
||||
"nodeSettings.executeOnce.description": "If active, the node executes only once, with data from the first item it receives",
|
||||
"nodeSettings.executeOnce.displayName": "Execute Once",
|
||||
|
|
|
@ -14,7 +14,7 @@ interface N8nPlusEndpointParams extends EndpointRepresentationParams {
|
|||
dimensions: number;
|
||||
connectedEndpoint: Endpoint;
|
||||
hoverMessage: string;
|
||||
nodeType: string;
|
||||
endpointLabelLength: 'small' | 'medium';
|
||||
size: 'small' | 'medium';
|
||||
showOutputLabel: boolean;
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
|||
options: {
|
||||
id: PlusStalkOverlay,
|
||||
attributes: {
|
||||
'data-endpoint-node-type': this.params.nodeType,
|
||||
'data-endpoint-label-length': this.params.endpointLabelLength,
|
||||
},
|
||||
create: () => {
|
||||
const stalk = createElement('div', {}, `${PlusStalkOverlay} ${this.params.size}`);
|
||||
|
@ -69,7 +69,7 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
|||
id: HoverMessageOverlay,
|
||||
location: 0.5,
|
||||
attributes: {
|
||||
'data-endpoint-node-type': this.params.nodeType,
|
||||
'data-endpoint-label-length': this.params.endpointLabelLength,
|
||||
},
|
||||
create: () => {
|
||||
const hoverMessage = createElement('p', {}, `${HoverMessageOverlay} ${this.params.size}`);
|
||||
|
@ -199,7 +199,8 @@ export const N8nPlusEndpointHandler: EndpointHandler<N8nPlusEndpoint, ComputedN8
|
|||
ep.w = w;
|
||||
ep.h = h;
|
||||
|
||||
ep.canvas?.setAttribute('data-endpoint-node-type', ep.params.nodeType);
|
||||
ep.canvas?.setAttribute('data-endpoint-label-length', ep.params.endpointLabelLength);
|
||||
|
||||
ep.addClass('plus-endpoint');
|
||||
return [x, y, w, h, ep.params.dimensions];
|
||||
},
|
||||
|
|
|
@ -112,7 +112,11 @@ export const CONNECTOR_PAINT_STYLE_DATA: PaintStyle = {
|
|||
stroke: getStyleTokenValue('--color-foreground-dark', true),
|
||||
};
|
||||
|
||||
export const getConnectorColor = (type: ConnectionTypes): string => {
|
||||
export const getConnectorColor = (type: ConnectionTypes, category?: string): string => {
|
||||
if (category === 'error') {
|
||||
return '--node-error-output-color';
|
||||
}
|
||||
|
||||
if (type === NodeConnectionType.Main) {
|
||||
return '--node-type-main-color';
|
||||
}
|
||||
|
@ -121,7 +125,10 @@ export const getConnectorColor = (type: ConnectionTypes): string => {
|
|||
};
|
||||
|
||||
export const getConnectorPaintStylePull = (connection: Connection): PaintStyle => {
|
||||
const connectorColor = getConnectorColor(connection.parameters.type as ConnectionTypes);
|
||||
const connectorColor = getConnectorColor(
|
||||
connection.parameters.type as ConnectionTypes,
|
||||
connection.parameters.category,
|
||||
);
|
||||
const additionalStyles: PaintStyle = {};
|
||||
if (connection.parameters.type !== NodeConnectionType.Main) {
|
||||
additionalStyles.dashstyle = '5 3';
|
||||
|
@ -134,15 +141,21 @@ export const getConnectorPaintStylePull = (connection: Connection): PaintStyle =
|
|||
};
|
||||
|
||||
export const getConnectorPaintStyleDefault = (connection: Connection): PaintStyle => {
|
||||
const connectorColor = getConnectorColor(connection.parameters.type as ConnectionTypes);
|
||||
const connectorColor = getConnectorColor(
|
||||
connection.parameters.type as ConnectionTypes,
|
||||
connection.parameters.category,
|
||||
);
|
||||
return {
|
||||
...CONNECTOR_PAINT_STYLE_DEFAULT,
|
||||
...(connectorColor ? { stroke: getStyleTokenValue(connectorColor, true) } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
export const getConnectorPaintStyleData = (connection: Connection): PaintStyle => {
|
||||
const connectorColor = getConnectorColor(connection.parameters.type as ConnectionTypes);
|
||||
export const getConnectorPaintStyleData = (
|
||||
connection: Connection,
|
||||
category?: string,
|
||||
): PaintStyle => {
|
||||
const connectorColor = getConnectorColor(connection.parameters.type as ConnectionTypes, category);
|
||||
return {
|
||||
...CONNECTOR_PAINT_STYLE_DATA,
|
||||
...(connectorColor ? { stroke: getStyleTokenValue(connectorColor, true) } : {}),
|
||||
|
@ -292,6 +305,7 @@ export const getOutputEndpointStyle = (
|
|||
export const getOutputNameOverlay = (
|
||||
labelText: string,
|
||||
outputName: ConnectionTypes,
|
||||
category?: string,
|
||||
): OverlaySpec => ({
|
||||
type: 'Custom',
|
||||
options: {
|
||||
|
@ -302,11 +316,16 @@ export const getOutputNameOverlay = (
|
|||
label.innerHTML = labelText;
|
||||
label.classList.add('node-output-endpoint-label');
|
||||
|
||||
label.setAttribute('data-endpoint-node-type', ep?.__meta?.nodeType);
|
||||
if (ep?.__meta?.endpointLabelLength) {
|
||||
label.setAttribute('data-endpoint-label-length', ep?.__meta?.endpointLabelLength);
|
||||
}
|
||||
if (outputName !== NodeConnectionType.Main) {
|
||||
label.classList.add('node-output-endpoint-label--data');
|
||||
label.classList.add(`node-connection-type-${getScope(outputName)}`);
|
||||
}
|
||||
if (category) {
|
||||
label.classList.add(`node-connection-category-${category}`);
|
||||
}
|
||||
return label;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1978,21 +1978,25 @@ export default defineComponent({
|
|||
lastSelectedNode.type,
|
||||
lastSelectedNode.typeVersion,
|
||||
);
|
||||
|
||||
if (sourceNodeType) {
|
||||
const offsets = [
|
||||
[-100, 100],
|
||||
[-140, 0, 140],
|
||||
[-240, -100, 100, 240],
|
||||
];
|
||||
|
||||
const sourceNodeOutputs = NodeHelpers.getNodeOutputs(
|
||||
workflow,
|
||||
lastSelectedNode,
|
||||
sourceNodeType,
|
||||
);
|
||||
const sourceNodeOutputTypes = NodeHelpers.getConnectionTypes(sourceNodeOutputs);
|
||||
|
||||
const sourceNodeOutputMainOutputs = sourceNodeOutputTypes.filter(
|
||||
(output) => output === NodeConnectionType.Main,
|
||||
);
|
||||
|
||||
if (sourceNodeOutputMainOutputs.length > 1) {
|
||||
const offset = offsets[sourceNodeOutputMainOutputs.length - 2];
|
||||
const sourceOutputIndex = lastSelectedConnection.__meta
|
||||
|
@ -2005,9 +2009,9 @@ export default defineComponent({
|
|||
|
||||
let outputs: Array<ConnectionTypes | INodeOutputConfiguration> = [];
|
||||
try {
|
||||
// It fails when the outputs are an expression. As those node have
|
||||
// It fails when the outputs are an expression. As those nodes have
|
||||
// normally no outputs by default and the only reason we need the
|
||||
// outputs here is to calculate the position it is fine to assume
|
||||
// outputs here is to calculate the position, it is fine to assume
|
||||
// that they have no outputs and are so treated as a regular node
|
||||
// with only "main" outputs.
|
||||
outputs = NodeHelpers.getNodeOutputs(workflow, newNodeData, nodeTypeData);
|
||||
|
@ -2571,7 +2575,9 @@ export default defineComponent({
|
|||
);
|
||||
if (sourceInfo.type !== NodeConnectionType.Main) {
|
||||
// Not "main" connections get a different connection style
|
||||
info.connection.setPaintStyle(getConnectorPaintStyleData(info.connection));
|
||||
info.connection.setPaintStyle(
|
||||
getConnectorPaintStyleData(info.connection, info.sourceEndpoint.parameters.category),
|
||||
);
|
||||
endpointArrow?.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -160,7 +160,12 @@ export class Code implements INodeType {
|
|||
result = await sandbox.runCodeEachItem();
|
||||
} catch (error) {
|
||||
if (!this.continueOnFail()) throw error;
|
||||
returnData.push({ json: { error: error.message } });
|
||||
returnData.push({
|
||||
json: { error: error.message },
|
||||
pairedItem: {
|
||||
item: index,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (result) {
|
||||
|
|
|
@ -16,7 +16,7 @@ export interface SandboxContext extends IWorkflowDataProxyData {
|
|||
helpers: IExecuteFunctions['helpers'];
|
||||
}
|
||||
|
||||
export const REQUIRED_N8N_ITEM_KEYS = new Set(['json', 'binary', 'pairedItem']);
|
||||
export const REQUIRED_N8N_ITEM_KEYS = new Set(['json', 'binary', 'pairedItem', 'error']);
|
||||
|
||||
export function getSandboxContext(this: IExecuteFunctions, index: number): SandboxContext {
|
||||
return {
|
||||
|
|
|
@ -920,6 +920,7 @@ export interface INodeCredentials {
|
|||
[key: string]: INodeCredentialsDetails;
|
||||
}
|
||||
|
||||
export type OnError = 'continueErrorOutput' | 'continueRegularOutput' | 'stopWorkflow';
|
||||
export interface INode {
|
||||
id: string;
|
||||
name: string;
|
||||
|
@ -934,6 +935,7 @@ export interface INode {
|
|||
waitBetweenTries?: number;
|
||||
alwaysOutputData?: boolean;
|
||||
executeOnce?: boolean;
|
||||
onError?: OnError;
|
||||
continueOnFail?: boolean;
|
||||
parameters: INodeParameters;
|
||||
credentials?: INodeCredentials;
|
||||
|
@ -1546,6 +1548,7 @@ export interface INodeInputConfiguration {
|
|||
}
|
||||
|
||||
export interface INodeOutputConfiguration {
|
||||
category?: string;
|
||||
displayName?: string;
|
||||
required?: boolean;
|
||||
type: ConnectionTypes;
|
||||
|
@ -1653,6 +1656,11 @@ export interface IWorkflowDataProxyData {
|
|||
$thisItemIndex: number;
|
||||
$now: any;
|
||||
$today: any;
|
||||
$getPairedItem: (
|
||||
destinationNodeName: string,
|
||||
incomingSourceData: ISourceData | null,
|
||||
pairedItem: IPairedItemData,
|
||||
) => INodeExecutionData | null;
|
||||
constructor: any;
|
||||
}
|
||||
|
||||
|
|
|
@ -1045,21 +1045,48 @@ export function getNodeOutputs(
|
|||
node: INode,
|
||||
nodeTypeData: INodeTypeDescription,
|
||||
): Array<ConnectionTypes | INodeOutputConfiguration> {
|
||||
let outputs: Array<ConnectionTypes | INodeOutputConfiguration> = [];
|
||||
|
||||
if (Array.isArray(nodeTypeData.outputs)) {
|
||||
return nodeTypeData.outputs;
|
||||
outputs = nodeTypeData.outputs;
|
||||
} else {
|
||||
// Calculate the outputs dynamically
|
||||
try {
|
||||
outputs = (workflow.expression.getSimpleParameterValue(
|
||||
node,
|
||||
nodeTypeData.outputs,
|
||||
'internal',
|
||||
{},
|
||||
) || []) as ConnectionTypes[];
|
||||
} catch (e) {
|
||||
throw new Error(`Could not calculate outputs dynamically for node "${node.name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the outputs dynamically
|
||||
try {
|
||||
return (workflow.expression.getSimpleParameterValue(
|
||||
node,
|
||||
nodeTypeData.outputs,
|
||||
'internal',
|
||||
{},
|
||||
) || []) as ConnectionTypes[];
|
||||
} catch (e) {
|
||||
throw new Error(`Could not calculate outputs dynamically for node "${node.name}"`);
|
||||
if (node.onError === 'continueErrorOutput') {
|
||||
// Copy the data to make sure that we do not change the data of the
|
||||
// node type and so change the displayNames for all nodes in the flow
|
||||
outputs = deepCopy(outputs);
|
||||
if (outputs.length === 1) {
|
||||
// Set the displayName to "Success"
|
||||
if (typeof outputs[0] === 'string') {
|
||||
outputs[0] = {
|
||||
type: outputs[0],
|
||||
};
|
||||
}
|
||||
outputs[0].displayName = 'Success';
|
||||
}
|
||||
return [
|
||||
...outputs,
|
||||
{
|
||||
category: 'error',
|
||||
type: 'main',
|
||||
displayName: 'Error',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -121,8 +121,9 @@ export class RoutingNode {
|
|||
|
||||
// TODO: Think about how batching could be handled for REST APIs which support it
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let thisArgs: IExecuteSingleFunctions | undefined;
|
||||
try {
|
||||
const thisArgs = nodeExecuteFunctions.getExecuteSingleFunctions(
|
||||
thisArgs = nodeExecuteFunctions.getExecuteSingleFunctions(
|
||||
this.workflow,
|
||||
this.runExecutionData,
|
||||
runIndex,
|
||||
|
@ -209,7 +210,7 @@ export class RoutingNode {
|
|||
|
||||
returnData.push(...responseData);
|
||||
} catch (error) {
|
||||
if (get(this.node, 'continueOnFail', false)) {
|
||||
if (thisArgs !== undefined && thisArgs.continueOnFail()) {
|
||||
returnData.push({ json: {}, error: error.message });
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -1211,6 +1211,7 @@ export class WorkflowDataProxy {
|
|||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
Duration,
|
||||
...that.additionalKeys,
|
||||
$getPairedItem: getPairedItem,
|
||||
|
||||
// deprecated
|
||||
$jmespath: jmespathWrapper,
|
||||
|
|
Loading…
Reference in a new issue