diff --git a/cypress/e2e/12-canvas.cy.ts b/cypress/e2e/12-canvas.cy.ts index b12ed884ca..01386d8e0c 100644 --- a/cypress/e2e/12-canvas.cy.ts +++ b/cypress/e2e/12-canvas.cy.ts @@ -158,7 +158,7 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.zoomToFit(); - cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150]); + cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true }); WorkflowPage.getters .canvasNodes() .last() diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index e6bd6b9f5c..64818faa14 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -515,6 +515,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { }, executionData: { contextData: {}, + metadata: {}, nodeExecutionStack, waitingExecution: {}, waitingExecutionSource: {}, diff --git a/packages/cli/src/GenericHelpers.ts b/packages/cli/src/GenericHelpers.ts index ef9e82a9fd..63edec827f 100644 --- a/packages/cli/src/GenericHelpers.ts +++ b/packages/cli/src/GenericHelpers.ts @@ -141,6 +141,7 @@ export async function createErrorExecution( }, executionData: { contextData: {}, + metadata: {}, nodeExecutionStack: [ { node, diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index ae78cd2dbc..22b67be6e9 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -249,6 +249,7 @@ export class Server extends AbstractServer { urlBaseWebhook, urlBaseEditor: instanceBaseUrl, versionCli: '', + isBetaRelease: config.getEnv('generic.isBetaRelease'), oauthCallbackUrls: { oauth1: `${instanceBaseUrl}/${this.restEndpoint}/oauth1-credential/callback`, oauth2: `${instanceBaseUrl}/${this.restEndpoint}/oauth2-credential/callback`, diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 5c94de7f12..5c38fb992f 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -32,6 +32,7 @@ import type { import { ErrorReporterProxy as ErrorReporter, LoggerProxy as Logger, + NodeOperationError, Workflow, WorkflowHooks, } from 'n8n-workflow'; @@ -46,6 +47,7 @@ import type { IWorkflowExecuteProcess, IWorkflowExecutionDataProcess, IWorkflowErrorData, + IPushDataType, ExecutionPayload, } from '@/Interfaces'; import { NodeTypes } from '@/NodeTypes'; @@ -69,6 +71,41 @@ import { restoreBinaryDataId } from './executionLifecycleHooks/restoreBinaryData const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType'); +export function objectToError(errorObject: unknown, workflow: Workflow): Error { + // TODO: Expand with other error types + if (errorObject instanceof Error) { + // If it's already an Error instance, return it as is. + return errorObject; + } else if (errorObject && typeof errorObject === 'object' && 'message' in errorObject) { + // If it's an object with a 'message' property, create a new Error instance. + let error: Error | undefined; + if ('node' in errorObject) { + const node = workflow.getNode((errorObject.node as { name: string }).name); + if (node) { + error = new NodeOperationError( + node, + errorObject as unknown as Error, + errorObject as object, + ); + } + } + + if (error === undefined) { + error = new Error(errorObject.message as string); + } + + if ('stack' in errorObject) { + // If there's a 'stack' property, set it on the new Error instance. + error.stack = errorObject.stack as string; + } + + return error; + } else { + // If it's neither an Error nor an object with a 'message' property, create a generic Error. + return new Error('An error occurred'); + } +} + /** * Checks if there was an error and if errorWorkflow or a trigger is defined. If so it collects * all the data and executes it @@ -369,6 +406,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx }, executionData: { contextData: {}, + metadata: {}, nodeExecutionStack: [], waitingExecution: {}, waitingExecutionSource: {}, @@ -709,6 +747,7 @@ export async function getRunData( }, executionData: { contextData: {}, + metadata: {}, nodeExecutionStack, waitingExecution: {}, waitingExecutionSource: {}, @@ -743,7 +782,7 @@ export async function getWorkflowData( workflowData = await WorkflowsService.get({ id: workflowInfo.id }, { relations }); - if (workflowData === undefined) { + if (workflowData === undefined || workflowData === null) { throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`); } } else { @@ -910,11 +949,14 @@ async function executeWorkflow( executionId, fullExecutionData, ); - throw { - ...error, - stack: error.stack, - message: error.message, - }; + throw objectToError( + { + ...error, + stack: error.stack, + message: error.message, + }, + workflow, + ); } await externalHooks.run('workflow.postExecute', [data, workflowData, executionId]); @@ -932,10 +974,13 @@ async function executeWorkflow( // Workflow did fail const { error } = data.data.resultData; // eslint-disable-next-line @typescript-eslint/no-throw-literal - throw { - ...error, - stack: error!.stack, - }; + throw objectToError( + { + ...error, + stack: error!.stack, + }, + workflow, + ); } export function setExecutionStatus(status: ExecutionStatus) { @@ -951,8 +996,7 @@ export function setExecutionStatus(status: ExecutionStatus) { }); } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function sendMessageToUI(source: string, messages: any[]) { +export function sendDataToUI(type: string, data: IDataObject | IDataObject[]) { const { sessionId } = this; if (sessionId === undefined) { return; @@ -961,14 +1005,7 @@ export function sendMessageToUI(source: string, messages: any[]) { // Push data to session which started workflow try { const pushInstance = Container.get(Push); - pushInstance.send( - 'sendConsoleMessage', - { - source: `[Node: "${source}"]`, - messages, - }, - sessionId, - ); + pushInstance.send(type as IPushDataType, data, sessionId); } catch (error) { Logger.warn(`There was a problem sending message to UI: ${error.message}`); } diff --git a/packages/cli/src/WorkflowHelpers.ts b/packages/cli/src/WorkflowHelpers.ts index 093f713e4e..103641e88c 100644 --- a/packages/cli/src/WorkflowHelpers.ts +++ b/packages/cli/src/WorkflowHelpers.ts @@ -74,6 +74,7 @@ export function generateFailedExecutionFromError( }, executionData: { contextData: {}, + metadata: {}, nodeExecutionStack: [ { node, @@ -252,6 +253,7 @@ export async function executeErrorWorkflow( }, executionData: { contextData: {}, + metadata: {}, nodeExecutionStack, waitingExecution: {}, waitingExecutionSource: {}, diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index cea0f9e98d..cbfc8fb356 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -327,7 +327,7 @@ export class WorkflowRunner { executionId, }); - additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({ + additionalData.sendDataToUI = WorkflowExecuteAdditionalData.sendDataToUI.bind({ sessionId: data.sessionId, }); @@ -344,8 +344,7 @@ export class WorkflowRunner { } else if ( data.runData === undefined || data.startNodes === undefined || - data.startNodes.length === 0 || - data.destinationNode === undefined + data.startNodes.length === 0 ) { Logger.debug(`Execution ID ${executionId} will run executing all nodes.`, { executionId }); // Execute all nodes @@ -736,11 +735,11 @@ export class WorkflowRunner { if (responsePromise) { responsePromise.resolve(WebhookHelpers.decodeWebhookResponse(message.data.response)); } - } else if (message.type === 'sendMessageToUI') { + } else if (message.type === 'sendDataToUI') { // eslint-disable-next-line @typescript-eslint/no-unsafe-call - WorkflowExecuteAdditionalData.sendMessageToUI.bind({ sessionId: data.sessionId })( - message.data.source, - message.data.message, + WorkflowExecuteAdditionalData.sendDataToUI.bind({ sessionId: data.sessionId })( + message.data.type, + message.data.data, ); } else if (message.type === 'processError') { clearTimeout(executionTimeout); diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index 071d8c982b..a69e22bda6 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -189,13 +189,13 @@ class WorkflowRunnerProcess { executionId: inputData.executionId, }); // eslint-disable-next-line @typescript-eslint/no-explicit-any - additionalData.sendMessageToUI = async (source: string, message: any) => { + additionalData.sendDataToUI = async (type: string, data: IDataObject | IDataObject[]) => { if (workflowRunner.data!.executionMode !== 'manual') { return; } try { - await sendToParentProcess('sendMessageToUI', { source, message }); + await sendToParentProcess('sendDataToUI', { type, data }); } catch (error) { ErrorReporter.error(error); this.logger.error( @@ -291,8 +291,7 @@ class WorkflowRunnerProcess { if ( this.data.runData === undefined || this.data.startNodes === undefined || - this.data.startNodes.length === 0 || - this.data.destinationNode === undefined + this.data.startNodes.length === 0 ) { // Execute all nodes diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index ec4f2e5567..b8052444fd 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -431,6 +431,13 @@ export const schema = { format: ['main', 'webhook', 'worker'] as const, default: 'main', }, + + isBetaRelease: { + doc: 'If it is a beta release', + format: 'Boolean', + default: false, + env: 'IS_BETA_RELEASE', + }, }, // How n8n can be reached (Editor & REST-API) diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index b432edc675..e85d4330f0 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -39,6 +39,8 @@ import pick from 'lodash/pick'; import { extension, lookup } from 'mime-types'; import type { BinaryHelperFunctions, + ConnectionTypes, + ExecutionError, FieldType, FileSystemHelperFunctions, FunctionsBase, @@ -66,6 +68,8 @@ import type { INodeCredentialDescription, INodeCredentialsDetails, INodeExecutionData, + INodeInputConfiguration, + INodeOutputConfiguration, INodeProperties, INodePropertyCollection, INodePropertyOptions, @@ -75,6 +79,7 @@ import type { IPollFunctions, IRunExecutionData, ISourceData, + ITaskData, ITaskDataConnections, ITriggerFunctions, IWebhookData, @@ -106,6 +111,7 @@ import { isObjectEmpty, isResourceMapperValue, validateFieldType, + ExecutionBaseError, } from 'n8n-workflow'; import type { Token } from 'oauth-1.0a'; import clientOAuth1 from 'oauth-1.0a'; @@ -2253,6 +2259,9 @@ export function getNodeParameter( timezone, additionalKeys, executeData, + false, + {}, + options?.contextNode?.name, ); cleanupParameterData(returnData); } catch (e) { @@ -2380,6 +2389,106 @@ export function getWebhookDescription( return undefined; } +// TODO: Change options to an object +const addExecutionDataFunctions = async ( + type: 'input' | 'output', + nodeName: string, + data: INodeExecutionData[][] | ExecutionBaseError, + runExecutionData: IRunExecutionData, + connectionType: ConnectionTypes, + additionalData: IWorkflowExecuteAdditionalData, + sourceNodeName: string, + sourceNodeRunIndex: number, + currentNodeRunIndex: number, +): Promise => { + if (connectionType === 'main') { + throw new Error(`Setting the ${type} is not supported for the main connection!`); + } + + let taskData: ITaskData | undefined; + if (type === 'input') { + taskData = { + startTime: new Date().getTime(), + executionTime: 0, + executionStatus: 'running', + source: [null], + }; + } else { + // At the moment we expect that there is always an input sent before the output + taskData = get( + runExecutionData, + ['resultData', 'runData', nodeName, currentNodeRunIndex], + undefined, + ); + if (taskData === undefined) { + return; + } + } + taskData = taskData!; + + if (data instanceof Error) { + // TODO: Or "failed", what is the difference + taskData.executionStatus = 'error'; + taskData.error = data; + } else { + if (type === 'output') { + taskData.executionStatus = 'success'; + } + taskData.data = { + [connectionType]: data, + } as ITaskDataConnections; + } + + if (type === 'input') { + if (!(data instanceof Error)) { + taskData.inputOverride = { + [connectionType]: data, + } as ITaskDataConnections; + } + + if (!runExecutionData.resultData.runData.hasOwnProperty(nodeName)) { + runExecutionData.resultData.runData[nodeName] = []; + } + + runExecutionData.resultData.runData[nodeName][currentNodeRunIndex] = taskData; + if (additionalData.sendDataToUI) { + additionalData.sendDataToUI('nodeExecuteBefore', { + executionId: additionalData.executionId, + nodeName, + }); + } + } else { + // Outputs + taskData.executionTime = new Date().getTime() - taskData.startTime; + + if (additionalData.sendDataToUI) { + additionalData.sendDataToUI('nodeExecuteAfter', { + executionId: additionalData.executionId, + nodeName, + data: taskData, + }); + } + + let sourceTaskData = get(runExecutionData, `executionData.metadata[${sourceNodeName}]`); + + if (!sourceTaskData) { + runExecutionData.executionData!.metadata[sourceNodeName] = []; + sourceTaskData = runExecutionData.executionData!.metadata[sourceNodeName]; + } + + if (!sourceTaskData[sourceNodeRunIndex]) { + sourceTaskData[sourceNodeRunIndex] = { + subRun: [], + }; + } + + sourceTaskData[sourceNodeRunIndex]!.subRun!.push({ + node: nodeName, + runIndex: currentNodeRunIndex, + }); + } +}; + const getCommonWorkflowFunctions = ( workflow: Workflow, node: INode, @@ -2787,6 +2896,192 @@ export function getExecuteFunctions( getContext(type: string): IContextObject { return NodeHelpers.getContext(runExecutionData, type, node); }, + async getInputConnectionData( + inputName: ConnectionTypes, + itemIndex: number, + // TODO: Not implemented yet, and maybe also not needed + inputIndex?: number, + ): Promise { + const node = this.getNode(); + const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); + + const inputs = NodeHelpers.getNodeInputs(workflow, node, nodeType.description); + + let inputConfiguration = inputs.find((input) => { + if (typeof input === 'string') { + return input === inputName; + } + return input.type === inputName; + }); + + if (inputConfiguration === undefined) { + throw new Error(`The node "${node.name}" does not have an input of type "${inputName}"`); + } + + if (typeof inputConfiguration === 'string') { + inputConfiguration = { + type: inputConfiguration, + } as INodeInputConfiguration; + } + + const parentNodes = workflow.getParentNodes(node.name, inputName, 1); + if (parentNodes.length === 0) { + return inputConfiguration.maxConnections === 1 ? undefined : []; + } + + const constParentNodes = parentNodes + .map((nodeName) => { + return workflow.getNode(nodeName) as INode; + }) + .filter((connectedNode) => connectedNode.disabled !== true) + .map(async (connectedNode) => { + const nodeType = workflow.nodeTypes.getByNameAndVersion( + connectedNode.type, + connectedNode.typeVersion, + ); + + if (!nodeType.supplyData) { + throw new Error( + `The node "${connectedNode.name}" does not have a "supplyData" method defined!`, + ); + } + + const context = Object.assign({}, this); + + context.getNodeParameter = ( + parameterName: string, + itemIndex: number, + fallbackValue?: any, + options?: IGetNodeParameterOptions, + ) => { + return getNodeParameter( + workflow, + runExecutionData, + runIndex, + connectionInputData, + connectedNode, + parameterName, + itemIndex, + mode, + additionalData.timezone, + getAdditionalKeys(additionalData, mode, runExecutionData), + executeData, + fallbackValue, + { ...(options || {}), contextNode: node }, + ) as any; + }; + + // TODO: Check what else should be overwritten + context.getNode = () => { + return deepCopy(connectedNode); + }; + + context.getCredentials = async (key: string) => { + try { + return await getCredentials( + workflow, + connectedNode, + key, + additionalData, + mode, + runExecutionData, + runIndex, + connectionInputData, + itemIndex, + ); + } catch (error) { + // Display the error on the node which is causing it + + let currentNodeRunIndex = 0; + if (runExecutionData.resultData.runData.hasOwnProperty(node.name)) { + currentNodeRunIndex = runExecutionData.resultData.runData[node.name].length; + } + + await addExecutionDataFunctions( + 'input', + connectedNode.name, + error, + runExecutionData, + inputName, + additionalData, + node.name, + runIndex, + currentNodeRunIndex, + ); + + throw error; + } + }; + + try { + return await nodeType.supplyData.call(context); + } catch (error) { + if (!(error instanceof ExecutionBaseError)) { + error = new NodeOperationError(connectedNode, error, { + itemIndex, + }); + } + + let currentNodeRunIndex = 0; + if (runExecutionData.resultData.runData.hasOwnProperty(node.name)) { + currentNodeRunIndex = runExecutionData.resultData.runData[node.name].length; + } + + // Display the error on the node which is causing it + await addExecutionDataFunctions( + 'input', + connectedNode.name, + error, + runExecutionData, + inputName, + additionalData, + node.name, + runIndex, + currentNodeRunIndex, + ); + + // Display on the calling node which node has the error + throw new NodeOperationError( + connectedNode, + `Error on node "${connectedNode.name}" which is connected via input "${inputName}"`, + { + itemIndex, + }, + ); + } + }); + + // Validate the inputs + const nodes = await Promise.all(constParentNodes); + + if (inputConfiguration.required && nodes.length === 0) { + throw new NodeOperationError(node, `A ${inputName} processor node must be connected!`); + } + if ( + inputConfiguration.maxConnections !== undefined && + nodes.length > inputConfiguration.maxConnections + ) { + throw new NodeOperationError( + node, + `Only ${inputConfiguration.maxConnections} ${inputName} processor nodes are/is allowed to be connected!`, + ); + } + + return inputConfiguration.maxConnections === 1 + ? (nodes || [])[0]?.response + : nodes.map((node) => node.response); + }, + getNodeOutputs(): INodeOutputConfiguration[] { + const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); + return NodeHelpers.getNodeOutputs(workflow, node, nodeType.description).map((output) => { + if (typeof output === 'string') { + return { + type: output, + }; + } + return output; + }); + }, getInputData: (inputIndex = 0, inputName = 'main') => { if (!inputData.hasOwnProperty(inputName)) { // Return empty array because else it would throw error when nothing is connected to input @@ -2863,7 +3158,7 @@ export function getExecuteFunctions( return; } try { - if (additionalData.sendMessageToUI) { + if (additionalData.sendDataToUI) { args = args.map((arg) => { // prevent invalid dates from being logged as null if (arg.isLuxonDateTime && arg.invalidReason) return { ...arg }; @@ -2875,7 +3170,10 @@ export function getExecuteFunctions( return arg; }); - additionalData.sendMessageToUI(node.name, args); + additionalData.sendDataToUI('sendConsoleMessage', { + source: `[Node: "${node.name}"]`, + messages: args, + }); } } catch (error) { Logger.warn(`There was a problem sending message to UI: ${error.message}`); @@ -2884,6 +3182,60 @@ export function getExecuteFunctions( async sendResponse(response: IExecuteResponsePromiseData): Promise { await additionalData.hooks?.executeHookFunctions('sendResponse', [response]); }, + + addInputData( + connectionType: ConnectionTypes, + data: INodeExecutionData[][] | ExecutionError, + ): { index: number } { + const nodeName = this.getNode().name; + let currentNodeRunIndex = 0; + if (runExecutionData.resultData.runData.hasOwnProperty(nodeName)) { + currentNodeRunIndex = runExecutionData.resultData.runData[nodeName].length; + } + + addExecutionDataFunctions( + 'input', + this.getNode().name, + data, + runExecutionData, + connectionType, + additionalData, + node.name, + runIndex, + currentNodeRunIndex, + ).catch((error) => { + Logger.warn( + `There was a problem logging input data of node "${this.getNode().name}": ${ + error.message + }`, + ); + }); + + return { index: currentNodeRunIndex }; + }, + addOutputData( + connectionType: ConnectionTypes, + currentNodeRunIndex: number, + data: INodeExecutionData[][] | ExecutionError, + ): void { + addExecutionDataFunctions( + 'output', + this.getNode().name, + data, + runExecutionData, + connectionType, + additionalData, + node.name, + runIndex, + currentNodeRunIndex, + ).catch((error) => { + Logger.warn( + `There was a problem logging output data of node "${this.getNode().name}": ${ + error.message + }`, + ); + }); + }, helpers: { createDeferredPromise, ...getRequestHelperFunctions(workflow, node, additionalData), diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index cfa9809cda..643fbfeb7e 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -23,6 +23,7 @@ import type { ITaskData, ITaskDataConnections, ITaskDataConnectionsSource, + ITaskMetadata, IWaitingForExecution, IWaitingForExecutionSource, NodeApiError, @@ -65,6 +66,7 @@ export class WorkflowExecute { executionData: { contextData: {}, nodeExecutionStack: [], + metadata: {}, waitingExecution: {}, waitingExecutionSource: {}, }, @@ -133,6 +135,7 @@ export class WorkflowExecute { executionData: { contextData: {}, nodeExecutionStack, + metadata: {}, waitingExecution: {}, waitingExecutionSource: {}, }, @@ -160,7 +163,7 @@ export class WorkflowExecute { workflow: Workflow, runData: IRunData, startNodes: string[], - destinationNode: string, + destinationNode?: string, pinData?: IPinData, ): PCancelable { let incomingNodeConnections: INodeConnections | undefined; @@ -169,6 +172,7 @@ export class WorkflowExecute { this.status = 'running'; const runIndex = 0; + let runNodeFilter: string[] | undefined; // Initialize the nodeExecutionStack and waitingExecution with // the data from runData @@ -182,7 +186,6 @@ export class WorkflowExecute { let incomingSourceData: ITaskDataConnectionsSource | null = null; if (incomingNodeConnections === undefined) { - // If it has no incoming data add the default empty data incomingData.push([ { json: {}, @@ -202,6 +205,9 @@ export class WorkflowExecute { if (node && pinData && pinData[node.name]) { incomingData.push(pinData[node.name]); } else { + if (!runData[connection.node]) { + continue; + } const nodeIncomingData = runData[connection.node][runIndex]?.data?.[connection.type][connection.index]; if (nodeIncomingData) { @@ -226,56 +232,57 @@ export class WorkflowExecute { nodeExecutionStack.push(executeData); - // Check if the destinationNode has to be added as waiting - // because some input data is already fully available - incomingNodeConnections = workflow.connectionsByDestinationNode[destinationNode]; - if (incomingNodeConnections !== undefined) { - for (const connections of incomingNodeConnections.main) { - for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) { - connection = connections[inputIndex]; + if (destinationNode) { + // Check if the destinationNode has to be added as waiting + // because some input data is already fully available + incomingNodeConnections = workflow.connectionsByDestinationNode[destinationNode]; + if (incomingNodeConnections !== undefined) { + for (const connections of incomingNodeConnections.main) { + for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) { + connection = connections[inputIndex]; - if (waitingExecution[destinationNode] === undefined) { - waitingExecution[destinationNode] = {}; - waitingExecutionSource[destinationNode] = {}; - } - if (waitingExecution[destinationNode][runIndex] === undefined) { - waitingExecution[destinationNode][runIndex] = {}; - waitingExecutionSource[destinationNode][runIndex] = {}; - } - if (waitingExecution[destinationNode][runIndex][connection.type] === undefined) { - waitingExecution[destinationNode][runIndex][connection.type] = []; - waitingExecutionSource[destinationNode][runIndex][connection.type] = []; - } + if (waitingExecution[destinationNode] === undefined) { + waitingExecution[destinationNode] = {}; + waitingExecutionSource[destinationNode] = {}; + } + if (waitingExecution[destinationNode][runIndex] === undefined) { + waitingExecution[destinationNode][runIndex] = {}; + waitingExecutionSource[destinationNode][runIndex] = {}; + } + if (waitingExecution[destinationNode][runIndex][connection.type] === undefined) { + waitingExecution[destinationNode][runIndex][connection.type] = []; + waitingExecutionSource[destinationNode][runIndex][connection.type] = []; + } - if (runData[connection.node] !== undefined) { - // Input data exists so add as waiting - // incomingDataDestination.push(runData[connection.node!][runIndex].data![connection.type][connection.index]); - waitingExecution[destinationNode][runIndex][connection.type].push( - runData[connection.node][runIndex].data![connection.type][connection.index], - ); - waitingExecutionSource[destinationNode][runIndex][connection.type].push({ - previousNode: connection.node, - previousNodeOutput: connection.index || undefined, - previousNodeRun: runIndex || undefined, - } as ISourceData); - } else { - waitingExecution[destinationNode][runIndex][connection.type].push(null); - waitingExecutionSource[destinationNode][runIndex][connection.type].push(null); + if (runData[connection.node] !== undefined) { + // Input data exists so add as waiting + // incomingDataDestination.push(runData[connection.node!][runIndex].data![connection.type][connection.index]); + waitingExecution[destinationNode][runIndex][connection.type].push( + runData[connection.node][runIndex].data![connection.type][connection.index], + ); + waitingExecutionSource[destinationNode][runIndex][connection.type].push({ + previousNode: connection.node, + previousNodeOutput: connection.index || undefined, + previousNodeRun: runIndex || undefined, + } as ISourceData); + } else { + waitingExecution[destinationNode][runIndex][connection.type].push(null); + waitingExecutionSource[destinationNode][runIndex][connection.type].push(null); + } } } } + + // Only run the parent nodes and no others + // eslint-disable-next-line prefer-const + runNodeFilter = workflow + .getParentNodes(destinationNode) + .filter((parentNodeName) => !workflow.getNode(parentNodeName)?.disabled); + + runNodeFilter.push(destinationNode); } } - // Only run the parent nodes and no others - let runNodeFilter: string[] | undefined; - // eslint-disable-next-line prefer-const - runNodeFilter = workflow - .getParentNodes(destinationNode) - .filter((parentNodeName) => !workflow.getNode(parentNodeName)?.disabled); - - runNodeFilter.push(destinationNode); - this.runExecutionData = { startData: { destinationNode, @@ -288,6 +295,7 @@ export class WorkflowExecute { executionData: { contextData: {}, nodeExecutionStack, + metadata: {}, waitingExecution, waitingExecutionSource, }, @@ -309,6 +317,22 @@ export class WorkflowExecute { return this.additionalData.hooks.executeHookFunctions(hookName, parameters); } + moveNodeMetadata(): void { + const metadata = get(this.runExecutionData, 'executionData.metadata'); + + if (metadata) { + const runData = get(this.runExecutionData, 'resultData.runData'); + + let index: number; + let metaRunData: ITaskMetadata; + for (const nodeName of Object.keys(metadata)) { + for ([index, metaRunData] of metadata[nodeName].entries()) { + runData[nodeName][index].metadata = metaRunData; + } + } + } + } + /** * Checks the incoming connection does not receive any data */ @@ -1533,6 +1557,9 @@ export class WorkflowExecute { // Static data of workflow changed newStaticData = workflow.staticData; } + + this.moveNodeMetadata(); + await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]).catch( // eslint-disable-next-line @typescript-eslint/no-shadow (error) => { @@ -1601,6 +1628,9 @@ export class WorkflowExecute { // Static data of workflow changed newStaticData = workflow.staticData; } + + this.moveNodeMetadata(); + await this.executeHook('workflowExecuteAfter', [fullRunData, newStaticData]); if (closeFunction) { diff --git a/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue index 87dc9ad46a..cb7e4a0dfe 100644 --- a/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue +++ b/packages/design-system/src/components/N8nNodeCreatorNode/NodeCreatorNode.vue @@ -1,3 +1,26 @@ + + - - diff --git a/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue b/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue index e2989ca7f2..c0db995916 100644 --- a/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue +++ b/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue @@ -95,7 +95,9 @@ export default defineComponent({ { key: 'Mod-Shift-z', run: redo }, ]), indentOnInput(), - theme, + theme({ + isReadOnly: this.isReadOnly, + }), lineNumbers(), highlightActiveLineGutter(), history(), @@ -103,6 +105,7 @@ export default defineComponent({ dropCursor(), indentOnInput(), highlightActiveLine(), + EditorView.editable.of(!this.isReadOnly), EditorState.readOnly.of(this.isReadOnly), EditorView.updateListener.of((viewUpdate: ViewUpdate) => { if (!viewUpdate.docChanged) return; diff --git a/packages/editor-ui/src/components/HtmlEditor/theme.ts b/packages/editor-ui/src/components/HtmlEditor/theme.ts index 5c2a9845c1..1fad0ff3ee 100644 --- a/packages/editor-ui/src/components/HtmlEditor/theme.ts +++ b/packages/editor-ui/src/components/HtmlEditor/theme.ts @@ -2,7 +2,7 @@ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; import { EditorView } from '@codemirror/view'; import { tags } from '@lezer/highlight'; -export const theme = [ +export const theme = ({ isReadOnly }: { isReadOnly: boolean }) => [ EditorView.theme({ '&': { 'font-size': '0.8em', @@ -18,6 +18,9 @@ export const theme = [ '.cm-cursor, .cm-dropCursor': { borderLeftColor: 'var(--color-code-caret)', }, + '&.cm-editor': { + ...(isReadOnly ? { backgroundColor: 'var(--color-code-background-readonly)' } : {}), + }, '&.cm-editor.cm-focused': { outline: '0', }, @@ -31,7 +34,9 @@ export const theme = [ backgroundColor: 'var(--color-code-lineHighlight)', }, '.cm-gutters': { - backgroundColor: 'var(--color-code-gutterBackground)', + backgroundColor: isReadOnly + ? 'var(--color-code-background-readonly)' + : 'var(--color-code-gutterBackground)', color: 'var(--color-code-gutterForeground)', borderTopLeftRadius: 'var(--border-radius-base)', borderBottomLeftRadius: 'var(--border-radius-base)', diff --git a/packages/editor-ui/src/components/InputPanel.vue b/packages/editor-ui/src/components/InputPanel.vue index 18fe2518b2..160229bc3d 100644 --- a/packages/editor-ui/src/components/InputPanel.vue +++ b/packages/editor-ui/src/components/InputPanel.vue @@ -3,14 +3,14 @@ :nodeUi="currentNode" :runIndex="runIndex" :linkedRuns="linkedRuns" - :canLinkRuns="canLinkRuns" + :canLinkRuns="!mappedNode && canLinkRuns" :tooMuchDataTitle="$locale.baseText('ndv.input.tooMuchData.title')" :noDataInBranchMessage="$locale.baseText('ndv.input.noOutputDataInBranch')" :isExecuting="isExecutingPrevious" :executingMessage="$locale.baseText('ndv.input.executingPrevious')" :sessionId="sessionId" :overrideOutputs="connectedCurrentNodeOutputs" - :mappingEnabled="!readOnly" + :mappingEnabled="isMappingEnabled" :distanceFromActive="currentNodeDepth" :isProductionExecutionPreview="isProductionExecutionPreview" paneType="input" @@ -55,11 +55,43 @@ {{ $locale.baseText('ndv.input') }} + + + + -