import { ref, nextTick } from 'vue'; import { useRoute } from 'vue-router'; import { v4 as uuid } from 'uuid'; import type { Connection, ConnectionDetachedParams } from '@jsplumb/core'; import { useHistoryStore } from '@/stores/history.store'; import { CUSTOM_API_CALL_KEY, FORM_TRIGGER_NODE_TYPE, NODE_OUTPUT_DEFAULT_KEY, PLACEHOLDER_FILLED_AT_EXECUTION_TIME, WEBHOOK_NODE_TYPE, } from '@/constants'; import { NodeHelpers, NodeConnectionType, ExpressionEvaluatorProxy } from 'n8n-workflow'; import type { INodeProperties, INodeCredentialDescription, INodeTypeDescription, INodeIssues, ICredentialType, INodeIssueObjectProperty, ConnectionTypes, INodeInputConfiguration, Workflow, INodeExecutionData, ITaskDataConnections, IRunData, IBinaryKeyData, IDataObject, INode, INodePropertyOptions, INodeCredentialsDetails, INodeParameters, ITaskData, IConnections, INodeTypeNameVersion, IConnection, IPinData, } from 'n8n-workflow'; import type { ICredentialsResponse, INodeUi, INodeUpdatePropertiesInformation, NodePanelType, } from '@/Interface'; import { isString } from '@/utils/typeGuards'; import { isObject } from '@/utils/objectUtils'; import { useSettingsStore } from '@/stores/settings.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useCredentialsStore } from '@/stores/credentials.store'; import { get } from 'lodash-es'; import { useI18n } from './useI18n'; import { AddNodeCommand, EnableNodeToggleCommand, RemoveConnectionCommand } from '@/models/history'; import { useTelemetry } from './useTelemetry'; import { hasPermission } from '@/utils/rbac/permissions'; import type { N8nPlusEndpoint } from '@/plugins/jsplumb/N8nPlusEndpointType'; import * as NodeViewUtils from '@/utils/nodeViewUtils'; import { useCanvasStore } from '@/stores/canvas.store'; import { getEndpointScope } from '@/utils/nodeViewUtils'; import { useSourceControlStore } from '@/stores/sourceControl.store'; import { getConnectionInfo } from '@/utils/canvasUtils'; declare namespace HttpRequestNode { namespace V2 { type AuthParams = { authentication: 'none' | 'genericCredentialType' | 'predefinedCredentialType'; genericAuthType: string; nodeCredentialType: string; }; } } export function useNodeHelpers() { const credentialsStore = useCredentialsStore(); const historyStore = useHistoryStore(); const nodeTypesStore = useNodeTypesStore(); const workflowsStore = useWorkflowsStore(); const i18n = useI18n(); const canvasStore = useCanvasStore(); const sourceControlStore = useSourceControlStore(); const route = useRoute(); const isInsertingNodes = ref(false); const credentialsUpdated = ref(false); const isProductionExecutionPreview = ref(false); const pullConnActiveNodeName = ref(null); function hasProxyAuth(node: INodeUi): boolean { return Object.keys(node.parameters).includes('nodeCredentialType'); } function isCustomApiCallSelected(nodeValues: INodeParameters): boolean { const { parameters } = nodeValues; if (!isObject(parameters)) return false; if ('resource' in parameters && 'operation' in parameters) { const { resource, operation } = parameters; if (!isString(resource) || !isString(operation)) return false; return resource.includes(CUSTOM_API_CALL_KEY) || operation.includes(CUSTOM_API_CALL_KEY); } return false; } function getParameterValue(nodeValues: INodeParameters, parameterName: string, path: string) { return get(nodeValues, path ? path + '.' + parameterName : parameterName); } // Returns if the given parameter should be displayed or not function displayParameter( nodeValues: INodeParameters, parameter: INodeProperties | INodeCredentialDescription, path: string, node: INodeUi | null, ) { return NodeHelpers.displayParameterPath(nodeValues, parameter, path, node); } function refreshNodeIssues(): void { const nodes = workflowsStore.allNodes; const workflow = workflowsStore.getCurrentWorkflow(); let nodeType: INodeTypeDescription | null; let foundNodeIssues: INodeIssues | null; nodes.forEach((node) => { if (node.disabled === true) return; nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion); foundNodeIssues = getNodeIssues(nodeType, node, workflow); if (foundNodeIssues !== null) { node.issues = foundNodeIssues; } }); } function getNodeIssues( nodeType: INodeTypeDescription | null, node: INodeUi, workflow: Workflow, ignoreIssues?: string[], ): INodeIssues | null { const pinDataNodeNames = Object.keys(workflowsStore.pinnedWorkflowData ?? {}); let nodeIssues: INodeIssues | null = null; ignoreIssues = ignoreIssues ?? []; if (node.disabled === true || pinDataNodeNames.includes(node.name)) { // Ignore issues on disabled and pindata nodes return null; } if (nodeType === null) { // Node type is not known if (!ignoreIssues.includes('typeUnknown')) { nodeIssues = { typeUnknown: true, }; } } else { // Node type is known // Add potential parameter issues if (!ignoreIssues.includes('parameters')) { nodeIssues = NodeHelpers.getNodeParametersIssues(nodeType.properties, node); } if (!ignoreIssues.includes('credentials')) { // Add potential credential issues const nodeCredentialIssues = getNodeCredentialIssues(node, nodeType); if (nodeIssues === null) { nodeIssues = nodeCredentialIssues; } else { NodeHelpers.mergeIssues(nodeIssues, nodeCredentialIssues); } } const nodeInputIssues = getNodeInputIssues(workflow, node, nodeType); if (nodeIssues === null) { nodeIssues = nodeInputIssues; } else { NodeHelpers.mergeIssues(nodeIssues, nodeInputIssues); } } if (hasNodeExecutionIssues(node) && !ignoreIssues.includes('execution')) { if (nodeIssues === null) { nodeIssues = {}; } nodeIssues.execution = true; } return nodeIssues; } // Set the status on all the nodes which produced an error so that it can be // displayed in the node-view function hasNodeExecutionIssues(node: INodeUi): boolean { const workflowResultData = workflowsStore.getWorkflowRunData; if (workflowResultData === null || !workflowResultData.hasOwnProperty(node.name)) { return false; } for (const taskData of workflowResultData[node.name]) { if (taskData.error !== undefined) { return true; } } return false; } function reportUnsetCredential(credentialType: ICredentialType) { return { credentials: { [credentialType.name]: [ i18n.baseText('nodeHelpers.credentialsUnset', { interpolate: { credentialType: credentialType.displayName, }, }), ], }, }; } function updateNodeInputIssues(node: INodeUi): void { const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion); if (!nodeType) { return; } const workflow = workflowsStore.getCurrentWorkflow(); const nodeInputIssues = getNodeInputIssues(workflow, node, nodeType); workflowsStore.setNodeIssue({ node: node.name, type: 'input', value: nodeInputIssues?.input ? nodeInputIssues.input : null, }); } function updateNodesInputIssues() { const nodes = workflowsStore.allNodes; for (const node of nodes) { updateNodeInputIssues(node); } } function updateNodesExecutionIssues() { const nodes = workflowsStore.allNodes; for (const node of nodes) { workflowsStore.setNodeIssue({ node: node.name, type: 'execution', value: hasNodeExecutionIssues(node) ? true : null, }); } } function updateNodesParameterIssues() { const nodes = workflowsStore.allNodes; for (const node of nodes) { updateNodeParameterIssues(node); } } function updateNodeCredentialIssuesByName(name: string): void { const node = workflowsStore.getNodeByName(name); if (node) { updateNodeCredentialIssues(node); } } function updateNodeCredentialIssues(node: INodeUi): void { const fullNodeIssues: INodeIssues | null = getNodeCredentialIssues(node); let newIssues: INodeIssueObjectProperty | null = null; if (fullNodeIssues !== null) { newIssues = fullNodeIssues.credentials!; } workflowsStore.setNodeIssue({ node: node.name, type: 'credentials', value: newIssues, }); } function updateNodeParameterIssuesByName(name: string): void { const node = workflowsStore.getNodeByName(name); if (node) { updateNodeParameterIssues(node); } } function updateNodeParameterIssues(node: INodeUi, nodeType?: INodeTypeDescription | null): void { const localNodeType = nodeType ?? nodeTypesStore.getNodeType(node.type, node.typeVersion); if (localNodeType === null) { // Could not find localNodeType so can not update issues return; } // All data got updated everywhere so update now the issues const fullNodeIssues: INodeIssues | null = NodeHelpers.getNodeParametersIssues( localNodeType.properties, node, ); let newIssues: INodeIssueObjectProperty | null = null; if (fullNodeIssues !== null) { newIssues = fullNodeIssues.parameters!; } workflowsStore.setNodeIssue({ node: node.name, type: 'parameters', value: newIssues, }); } function getNodeInputIssues( workflow: Workflow, node: INodeUi, nodeType?: INodeTypeDescription, ): INodeIssues | null { const foundIssues: INodeIssueObjectProperty = {}; const workflowNode = workflow.getNode(node.name); let inputs: Array = []; if (nodeType && workflowNode) { inputs = NodeHelpers.getNodeInputs(workflow, workflowNode, nodeType); } inputs.forEach((input) => { if (typeof input === 'string' || input.required !== true) { return; } const parentNodes = workflow.getParentNodes(node.name, input.type, 1); if (parentNodes.length === 0) { foundIssues[input.type] = [ i18n.baseText('nodeIssues.input.missing', { interpolate: { inputName: input.displayName || input.type }, }), ]; } }); if (Object.keys(foundIssues).length) { return { input: foundIssues, }; } return null; } function getNodeCredentialIssues( node: INodeUi, nodeType?: INodeTypeDescription, ): INodeIssues | null { const localNodeType = nodeType ?? nodeTypesStore.getNodeType(node.type, node.typeVersion); if (node.disabled) { // Node is disabled return null; } if (!localNodeType?.credentials) { // Node does not need any credentials or nodeType could not be found return null; } const foundIssues: INodeIssueObjectProperty = {}; let userCredentials: ICredentialsResponse[] | null; let credentialType: ICredentialType | undefined; let credentialDisplayName: string; let selectedCredentials: INodeCredentialsDetails; const { authentication, genericAuthType, nodeCredentialType } = node.parameters as HttpRequestNode.V2.AuthParams; if ( authentication === 'genericCredentialType' && genericAuthType !== '' && selectedCredsAreUnusable(node, genericAuthType) ) { const credential = credentialsStore.getCredentialTypeByName(genericAuthType); return credential ? reportUnsetCredential(credential) : null; } if ( hasProxyAuth(node) && authentication === 'predefinedCredentialType' && nodeCredentialType !== '' && node.credentials !== undefined ) { const stored = credentialsStore.getCredentialsByType(nodeCredentialType); // Prevents HTTP Request node from being unusable if a sharee does not have direct // access to a credential const isCredentialUsedInWorkflow = workflowsStore.usedCredentials?.[node.credentials?.[nodeCredentialType]?.id as string]; if ( selectedCredsDoNotExist(node, nodeCredentialType, stored) && !isCredentialUsedInWorkflow ) { const credential = credentialsStore.getCredentialTypeByName(nodeCredentialType); return credential ? reportUnsetCredential(credential) : null; } } if ( hasProxyAuth(node) && authentication === 'predefinedCredentialType' && nodeCredentialType !== '' && selectedCredsAreUnusable(node, nodeCredentialType) ) { const credential = credentialsStore.getCredentialTypeByName(nodeCredentialType); return credential ? reportUnsetCredential(credential) : null; } for (const credentialTypeDescription of localNodeType.credentials) { // Check if credentials should be displayed else ignore if (!displayParameter(node.parameters, credentialTypeDescription, '', node)) { continue; } // Get the display name of the credential type credentialType = credentialsStore.getCredentialTypeByName(credentialTypeDescription.name); if (!credentialType) { credentialDisplayName = credentialTypeDescription.name; } else { credentialDisplayName = credentialType.displayName; } if (!node.credentials?.[credentialTypeDescription.name]) { // Credentials are not set if (credentialTypeDescription.required) { foundIssues[credentialTypeDescription.name] = [ i18n.baseText('nodeIssues.credentials.notSet', { interpolate: { type: localNodeType.displayName }, }), ]; } } else { // If they are set check if the value is valid selectedCredentials = node.credentials[credentialTypeDescription.name]; if (typeof selectedCredentials === 'string') { selectedCredentials = { id: null, name: selectedCredentials, }; } userCredentials = credentialsStore.getCredentialsByType(credentialTypeDescription.name); if (userCredentials === null) { userCredentials = []; } if (selectedCredentials.id) { const idMatch = userCredentials.find( (credentialData) => credentialData.id === selectedCredentials.id, ); if (idMatch) { continue; } } const nameMatches = userCredentials.filter( (credentialData) => credentialData.name === selectedCredentials.name, ); if (nameMatches.length > 1) { foundIssues[credentialTypeDescription.name] = [ i18n.baseText('nodeIssues.credentials.notIdentified', { interpolate: { name: selectedCredentials.name, type: credentialDisplayName }, }), i18n.baseText('nodeIssues.credentials.notIdentified.hint'), ]; continue; } if (nameMatches.length === 0) { const isCredentialUsedInWorkflow = workflowsStore.usedCredentials?.[selectedCredentials.id as string]; if ( !isCredentialUsedInWorkflow && !hasPermission(['rbac'], { rbac: { scope: 'credential:read' } }) ) { foundIssues[credentialTypeDescription.name] = [ i18n.baseText('nodeIssues.credentials.doNotExist', { interpolate: { name: selectedCredentials.name, type: credentialDisplayName }, }), i18n.baseText('nodeIssues.credentials.doNotExist.hint'), ]; } } } } // TODO: Could later check also if the node has access to the credentials if (Object.keys(foundIssues).length === 0) { return null; } return { credentials: foundIssues, }; } /** * Whether the node has no selected credentials, or none of the node's * selected credentials are of the specified type. */ function selectedCredsAreUnusable(node: INodeUi, credentialType: string) { return !node.credentials || !Object.keys(node.credentials).includes(credentialType); } /** * Whether the node's selected credentials of the specified type * can no longer be found in the database. */ function selectedCredsDoNotExist( node: INodeUi, nodeCredentialType: string, storedCredsByType: ICredentialsResponse[] | null, ) { if (!node.credentials || !storedCredsByType) return false; const selectedCredsByType = node.credentials[nodeCredentialType]; if (!selectedCredsByType) return false; return !storedCredsByType.find((c) => c.id === selectedCredsByType.id); } function updateNodesCredentialsIssues() { const nodes = workflowsStore.allNodes; let issues: INodeIssues | null; for (const node of nodes) { issues = getNodeCredentialIssues(node); workflowsStore.setNodeIssue({ node: node.name, type: 'credentials', value: issues?.credentials ?? null, }); } } function getNodeInputData( node: INodeUi | null, runIndex = 0, outputIndex = 0, paneType: NodePanelType = 'output', connectionType: ConnectionTypes = NodeConnectionType.Main, ): INodeExecutionData[] { if (node === null) { return []; } if (workflowsStore.getWorkflowExecution === null) { return []; } const executionData = workflowsStore.getWorkflowExecution.data; if (!executionData?.resultData) { // unknown status return []; } const runData = executionData.resultData.runData; const taskData = get(runData, [node.name, runIndex]); if (!taskData) { return []; } let data: ITaskDataConnections | undefined = taskData.data; if (paneType === 'input' && taskData.inputOverride) { data = taskData.inputOverride; } if (!data) { return []; } return getInputData(data, outputIndex, connectionType); } function getInputData( connectionsData: ITaskDataConnections, outputIndex: number, connectionType: ConnectionTypes = NodeConnectionType.Main, ): INodeExecutionData[] { return connectionsData?.[connectionType]?.[outputIndex] ?? []; } function getBinaryData( workflowRunData: IRunData | null, node: string | null, runIndex: number, outputIndex: number, connectionType: ConnectionTypes = NodeConnectionType.Main, ): IBinaryKeyData[] { if (node === null) { return []; } const runData: IRunData | null = workflowRunData; const runDataOfNode = runData?.[node]?.[runIndex]?.data; if (!runDataOfNode) { return []; } const inputData = getInputData(runDataOfNode, outputIndex, connectionType); const returnData: IBinaryKeyData[] = []; for (let i = 0; i < inputData.length; i++) { const binaryDataInIdx = inputData[i]?.binary; if (binaryDataInIdx !== undefined) { returnData.push(binaryDataInIdx); } } return returnData; } function disableNodes(nodes: INodeUi[], trackHistory = false) { const telemetry = useTelemetry(); if (trackHistory) { historyStore.startRecordingUndo(); } const newDisabledState = nodes.some((node) => !node.disabled); for (const node of nodes) { if (newDisabledState === node.disabled) { continue; } // Toggle disabled flag const updateInformation = { name: node.name, properties: { disabled: newDisabledState, } as IDataObject, } as INodeUpdatePropertiesInformation; telemetry.track('User set node enabled status', { node_type: node.type, is_enabled: node.disabled, workflow_id: workflowsStore.workflowId, }); workflowsStore.updateNodeProperties(updateInformation); workflowsStore.clearNodeExecutionData(node.name); updateNodeParameterIssues(node); updateNodeCredentialIssues(node); updateNodesInputIssues(); if (trackHistory) { historyStore.pushCommandToUndo( new EnableNodeToggleCommand(node.name, node.disabled === true, newDisabledState), ); } } if (trackHistory) { historyStore.stopRecordingUndo(); } } function getNodeSubtitle( data: INode, nodeType: INodeTypeDescription, workflow: Workflow, ): string | undefined { if (!data) { return undefined; } if (data.notesInFlow) { return data.notes; } if (nodeType?.subtitle !== undefined) { try { ExpressionEvaluatorProxy.setEvaluator( useSettingsStore().settings.expressions?.evaluator ?? 'tmpl', ); return workflow.expression.getSimpleParameterValue( data, nodeType.subtitle, 'internal', {}, undefined, PLACEHOLDER_FILLED_AT_EXECUTION_TIME, ) as string | undefined; } catch (e) { return undefined; } } if (data.parameters.operation !== undefined) { const operation = data.parameters.operation as string; if (nodeType === null) { return operation; } const operationData = nodeType.properties.find((property: INodeProperties) => { return property.name === 'operation'; }); if (operationData === undefined) { return operation; } if (operationData.options === undefined) { return operation; } const optionData = operationData.options.find((option) => { return (option as INodePropertyOptions).value === data.parameters.operation; }); if (optionData === undefined) { return operation; } return optionData.name; } return undefined; } function setSuccessOutput(data: ITaskData[], sourceNode: INodeUi | null) { if (!sourceNode) { throw new Error('Source node is null or not defined'); } const allNodeConnections = workflowsStore.outgoingConnectionsByNodeName(sourceNode.name); const connectionType = Object.keys(allNodeConnections)[0] as NodeConnectionType; const nodeConnections = allNodeConnections[connectionType]; const outputMap = NodeViewUtils.getOutputSummary( data, nodeConnections || [], connectionType ?? NodeConnectionType.Main, ); const sourceNodeType = nodeTypesStore.getNodeType(sourceNode.type, sourceNode.typeVersion); Object.keys(outputMap).forEach((sourceOutputIndex: string) => { Object.keys(outputMap[sourceOutputIndex]).forEach((targetNodeName: string) => { Object.keys(outputMap[sourceOutputIndex][targetNodeName]).forEach( (targetInputIndex: string) => { if (targetNodeName) { const targetNode = workflowsStore.getNodeByName(targetNodeName); const connection = NodeViewUtils.getJSPlumbConnection( sourceNode, parseInt(sourceOutputIndex, 10), targetNode, parseInt(targetInputIndex, 10), connectionType, sourceNodeType, canvasStore.jsPlumbInstance, ); if (connection) { const output = outputMap[sourceOutputIndex][targetNodeName][targetInputIndex]; if (output.isArtificialRecoveredEventItem) { NodeViewUtils.recoveredConnection(connection); } else if (!output?.total && !output.isArtificialRecoveredEventItem) { NodeViewUtils.resetConnection(connection); } else { NodeViewUtils.addConnectionOutputSuccess(connection, output); } } } const endpoint = NodeViewUtils.getPlusEndpoint( sourceNode, parseInt(sourceOutputIndex, 10), canvasStore.jsPlumbInstance, ); if (endpoint?.endpoint) { const output = outputMap[sourceOutputIndex][NODE_OUTPUT_DEFAULT_KEY][0]; if (output && output.total > 0) { (endpoint.endpoint as N8nPlusEndpoint).setSuccessOutput( NodeViewUtils.getRunItemsLabel(output), ); } else { (endpoint.endpoint as N8nPlusEndpoint).clearSuccessOutput(); } } }, ); }); }); } function matchCredentials(node: INodeUi) { if (!node.credentials) { return; } Object.entries(node.credentials).forEach( ([nodeCredentialType, nodeCredentials]: [string, INodeCredentialsDetails]) => { const credentialOptions = credentialsStore.getCredentialsByType(nodeCredentialType); // Check if workflows applies old credentials style if (typeof nodeCredentials === 'string') { nodeCredentials = { id: null, name: nodeCredentials, }; credentialsUpdated.value = true; } if (nodeCredentials.id) { // Check whether the id is matching with a credential const credentialsId = nodeCredentials.id.toString(); // due to a fixed bug in the migration UpdateWorkflowCredentials (just sqlite) we have to cast to string and check later if it has been a number const credentialsForId = credentialOptions.find( (optionData: ICredentialsResponse) => optionData.id === credentialsId, ); if (credentialsForId) { if ( credentialsForId.name !== nodeCredentials.name || typeof nodeCredentials.id === 'number' ) { node.credentials![nodeCredentialType] = { id: credentialsForId.id, name: credentialsForId.name, }; credentialsUpdated.value = true; } return; } } // No match for id found or old credentials type used node.credentials![nodeCredentialType] = nodeCredentials; // check if only one option with the name would exist const credentialsForName = credentialOptions.filter( (optionData: ICredentialsResponse) => optionData.name === nodeCredentials.name, ); // only one option exists for the name, take it if (credentialsForName.length === 1) { node.credentials![nodeCredentialType].id = credentialsForName[0].id; credentialsUpdated.value = true; } }, ); } function deleteJSPlumbConnection(connection: Connection, trackHistory = false) { // Make sure to remove the overlay else after the second move // it visibly stays behind free floating without a connection. connection.removeOverlays(); pullConnActiveNodeName.value = null; // prevent new connections when connectionDetached is triggered canvasStore.jsPlumbInstance?.deleteConnection(connection); // on delete, triggers connectionDetached event which applies mutation to store if (trackHistory && connection.__meta) { const connectionData: [IConnection, IConnection] = [ { index: connection.__meta?.sourceOutputIndex, node: connection.__meta.sourceNodeName, type: NodeConnectionType.Main, }, { index: connection.__meta?.targetOutputIndex, node: connection.__meta.targetNodeName, type: NodeConnectionType.Main, }, ]; const removeCommand = new RemoveConnectionCommand(connectionData); historyStore.pushCommandToUndo(removeCommand); } } async function loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise { const allNodes: INodeTypeDescription[] = nodeTypesStore.allNodeTypes; const nodesToBeFetched: INodeTypeNameVersion[] = []; allNodes.forEach((node) => { const nodeVersions = Array.isArray(node.version) ? node.version : [node.version]; if ( !!nodeInfos.find((n) => n.name === node.name && nodeVersions.includes(n.version)) && !node.hasOwnProperty('properties') ) { nodesToBeFetched.push({ name: node.name, version: Array.isArray(node.version) ? node.version.slice(-1)[0] : node.version, }); } }); if (nodesToBeFetched.length > 0) { // Only call API if node information is actually missing canvasStore.startLoading(); await nodeTypesStore.getNodesInformation(nodesToBeFetched); canvasStore.stopLoading(); } } function addConnectionsTestData() { canvasStore.jsPlumbInstance?.connections.forEach((connection) => { NodeViewUtils.addConnectionTestData( connection.source, connection.target, connection?.connector?.hasOwnProperty('canvas') ? connection?.connector.canvas : undefined, ); }); } async function processConnectionBatch(batchedConnectionData: Array<[IConnection, IConnection]>) { const batchSize = 100; for (let i = 0; i < batchedConnectionData.length; i += batchSize) { const batch = batchedConnectionData.slice(i, i + batchSize); batch.forEach((connectionData) => { addConnection(connectionData); }); } } function addPinDataConnections(pinData?: IPinData) { if (!pinData) { return; } Object.keys(pinData).forEach((nodeName) => { const node = workflowsStore.getNodeByName(nodeName); if (!node) { return; } const nodeElement = document.getElementById(node.id); if (!nodeElement) { return; } const hasRun = workflowsStore.getWorkflowResultDataByNodeName(nodeName) !== null; // In case we are showing a production execution preview we want // to show pinned data connections as they wouldn't have been pinned const classNames = isProductionExecutionPreview.value ? [] : ['pinned']; if (hasRun) { classNames.push('has-run'); } const connections = canvasStore.jsPlumbInstance?.getConnections({ source: nodeElement, }); const connectionsArray = Array.isArray(connections) ? connections : Object.values(connections); connectionsArray.forEach((connection) => { NodeViewUtils.addConnectionOutputSuccess(connection, { total: pinData[nodeName].length, iterations: 0, classNames, }); }); }); } function removePinDataConnections(pinData: IPinData) { Object.keys(pinData).forEach((nodeName) => { const node = workflowsStore.getNodeByName(nodeName); if (!node) { return; } const nodeElement = document.getElementById(node.id); if (!nodeElement) { return; } const connections = canvasStore.jsPlumbInstance?.getConnections({ source: nodeElement, }); const connectionsArray = Array.isArray(connections) ? connections : Object.values(connections); canvasStore.jsPlumbInstance.setSuspendDrawing(true); connectionsArray.forEach(NodeViewUtils.resetConnection); canvasStore.jsPlumbInstance.setSuspendDrawing(false, true); }); } function getOutputEndpointUUID( nodeName: string, connectionType: NodeConnectionType, index: number, ): string | null { const node = workflowsStore.getNodeByName(nodeName); if (!node) { return null; } return NodeViewUtils.getOutputEndpointUUID(node.id, connectionType, index); } function getInputEndpointUUID( nodeName: string, connectionType: NodeConnectionType, index: number, ) { const node = workflowsStore.getNodeByName(nodeName); if (!node) { return null; } return NodeViewUtils.getInputEndpointUUID(node.id, connectionType, index); } function addConnection(connection: [IConnection, IConnection]) { const outputUuid = getOutputEndpointUUID( connection[0].node, connection[0].type, connection[0].index, ); const inputUuid = getInputEndpointUUID( connection[1].node, connection[1].type, connection[1].index, ); if (!outputUuid || !inputUuid) { return; } const uuids: [string, string] = [outputUuid, inputUuid]; // Create connections in DOM canvasStore.jsPlumbInstance?.connect({ uuids, detachable: !route?.meta?.readOnlyCanvas && !sourceControlStore.preferences.branchReadOnly, }); setTimeout(() => { addPinDataConnections(workflowsStore.pinnedWorkflowData); }); } function removeConnection( connection: [IConnection, IConnection], removeVisualConnection = false, ) { if (removeVisualConnection) { const sourceNode = workflowsStore.getNodeByName(connection[0].node); const targetNode = workflowsStore.getNodeByName(connection[1].node); if (!sourceNode || !targetNode) { return; } const sourceElement = document.getElementById(sourceNode.id); const targetElement = document.getElementById(targetNode.id); if (sourceElement && targetElement) { const connections = canvasStore.jsPlumbInstance?.getConnections({ source: sourceElement, target: targetElement, }); if (Array.isArray(connections)) { connections.forEach((connectionInstance: Connection) => { if (connectionInstance.__meta) { // Only delete connections from specific indexes (if it can be determined by meta) if ( connectionInstance.__meta.sourceOutputIndex === connection[0].index && connectionInstance.__meta.targetOutputIndex === connection[1].index ) { deleteJSPlumbConnection(connectionInstance); } } else { deleteJSPlumbConnection(connectionInstance); } }); } } } workflowsStore.removeConnection({ connection }); } function removeConnectionByConnectionInfo( info: ConnectionDetachedParams, removeVisualConnection = false, trackHistory = false, ) { const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info); if (connectionInfo) { if (removeVisualConnection) { deleteJSPlumbConnection(info.connection, trackHistory); } else if (trackHistory) { historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo)); } workflowsStore.removeConnection({ connection: connectionInfo }); } } async function addConnections(connections: IConnections) { const batchedConnectionData: Array<[IConnection, IConnection]> = []; for (const sourceNode in connections) { for (const type in connections[sourceNode]) { connections[sourceNode][type].forEach((outwardConnections, sourceIndex) => { if (outwardConnections) { outwardConnections.forEach((targetData) => { batchedConnectionData.push([ { node: sourceNode, type: getEndpointScope(type) ?? NodeConnectionType.Main, index: sourceIndex, }, { node: targetData.node, type: targetData.type, index: targetData.index }, ]); }); } }); } } // Process the connections in batches await processConnectionBatch(batchedConnectionData); setTimeout(addConnectionsTestData, 0); } async function addNodes(nodes: INodeUi[], connections?: IConnections, trackHistory = false) { if (!nodes?.length) { return; } isInsertingNodes.value = true; // Before proceeding we must check if all nodes contain the `properties` attribute. // Nodes are loaded without this information so we must make sure that all nodes // being added have this information. await loadNodesProperties( nodes.map((node) => ({ name: node.type, version: node.typeVersion })), ); // Add the node to the node-list let nodeType: INodeTypeDescription | null; nodes.forEach((node) => { const newNode: INodeUi = { ...node, }; if (!newNode.id) { newNode.id = uuid(); } nodeType = nodeTypesStore.getNodeType(newNode.type, newNode.typeVersion); // Make sure that some properties always exist if (!newNode.hasOwnProperty('disabled')) { newNode.disabled = false; } if (!newNode.hasOwnProperty('parameters')) { newNode.parameters = {}; } // Load the default parameter values because only values which differ // from the defaults get saved if (nodeType !== null) { let nodeParameters = null; try { nodeParameters = NodeHelpers.getNodeParameters( nodeType.properties, newNode.parameters, true, false, node, ); } catch (e) { console.error( i18n.baseText('nodeView.thereWasAProblemLoadingTheNodeParametersOfNode') + `: "${newNode.name}"`, ); console.error(e); } newNode.parameters = nodeParameters ?? {}; // if it's a webhook and the path is empty set the UUID as the default path if ( [WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE].includes(newNode.type) && newNode.parameters.path === '' ) { newNode.parameters.path = newNode.webhookId as string; } } // check and match credentials, apply new format if old is used matchCredentials(newNode); workflowsStore.addNode(newNode); if (trackHistory) { historyStore.pushCommandToUndo(new AddNodeCommand(newNode)); } }); // Wait for the nodes to be rendered await nextTick(); canvasStore.jsPlumbInstance?.setSuspendDrawing(true); if (connections) { await addConnections(connections); } // Add the node issues at the end as the node-connections are required refreshNodeIssues(); updateNodesInputIssues(); /////////////////////////////this.resetEndpointsErrors(); isInsertingNodes.value = false; // Now it can draw again canvasStore.jsPlumbInstance?.setSuspendDrawing(false, true); } return { hasProxyAuth, isCustomApiCallSelected, getParameterValue, displayParameter, getNodeIssues, updateNodesInputIssues, updateNodesExecutionIssues, updateNodesParameterIssues, updateNodeInputIssues, updateNodeCredentialIssuesByName, updateNodeCredentialIssues, updateNodeParameterIssuesByName, updateNodeParameterIssues, getBinaryData, disableNodes, getNodeSubtitle, updateNodesCredentialsIssues, getNodeInputData, setSuccessOutput, isInsertingNodes, credentialsUpdated, isProductionExecutionPreview, pullConnActiveNodeName, deleteJSPlumbConnection, loadNodesProperties, addNodes, addConnection, removeConnection, removeConnectionByConnectionInfo, addPinDataConnections, removePinDataConnections, }; }