2024-06-24 06:08:24 -07:00
import { ref , nextTick } from 'vue' ;
import { useRoute } from 'vue-router' ;
import { v4 as uuid } from 'uuid' ;
import type { Connection , ConnectionDetachedParams } from '@jsplumb/core' ;
2023-12-08 07:59:03 -08:00
import { useHistoryStore } from '@/stores/history.store' ;
2024-01-19 05:44:54 -08:00
import {
CUSTOM_API_CALL_KEY ,
2024-06-24 06:08:24 -07:00
FORM_TRIGGER_NODE_TYPE ,
2024-01-19 05:44:54 -08:00
NODE_OUTPUT_DEFAULT_KEY ,
PLACEHOLDER_FILLED_AT_EXECUTION_TIME ,
2024-07-31 06:11:42 -07:00
SPLIT_IN_BATCHES_NODE_TYPE ,
2024-06-24 06:08:24 -07:00
WEBHOOK_NODE_TYPE ,
2024-01-19 05:44:54 -08:00
} from '@/constants' ;
2023-12-08 07:59:03 -08:00
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 ,
2024-01-19 05:44:54 -08:00
ITaskData ,
2024-06-24 06:08:24 -07:00
IConnections ,
INodeTypeNameVersion ,
IConnection ,
IPinData ,
2023-12-08 07:59:03 -08:00
} 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' ;
2024-06-24 06:08:24 -07:00
import { AddNodeCommand , EnableNodeToggleCommand , RemoveConnectionCommand } from '@/models/history' ;
2023-12-08 07:59:03 -08:00
import { useTelemetry } from './useTelemetry' ;
2024-06-06 06:30:17 -07:00
import { hasPermission } from '@/utils/rbac/permissions' ;
2024-01-19 05:44:54 -08:00
import type { N8nPlusEndpoint } from '@/plugins/jsplumb/N8nPlusEndpointType' ;
import * as NodeViewUtils from '@/utils/nodeViewUtils' ;
import { useCanvasStore } from '@/stores/canvas.store' ;
2024-06-24 06:08:24 -07:00
import { getEndpointScope } from '@/utils/nodeViewUtils' ;
import { useSourceControlStore } from '@/stores/sourceControl.store' ;
import { getConnectionInfo } from '@/utils/canvasUtils' ;
2024-08-21 23:31:48 -07:00
import type { UnpinNodeDataEvent } from '@/event-bus/data-pinning' ;
2023-12-08 07:59:03 -08:00
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 ( ) ;
2024-01-19 05:44:54 -08:00
const canvasStore = useCanvasStore ( ) ;
2024-06-24 06:08:24 -07:00
const sourceControlStore = useSourceControlStore ( ) ;
const route = useRoute ( ) ;
const isInsertingNodes = ref ( false ) ;
const credentialsUpdated = ref ( false ) ;
const isProductionExecutionPreview = ref ( false ) ;
const pullConnActiveNodeName = ref < string | null > ( null ) ;
2023-12-08 07:59:03 -08:00
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 ;
2024-07-30 08:41:26 -07:00
if ( 'resource' in parameters || 'operation' in parameters ) {
2024-06-03 02:34:51 -07:00
const { resource , operation } = parameters ;
2023-12-08 07:59:03 -08:00
2024-07-30 08:41:26 -07:00
return (
( isString ( resource ) && resource . includes ( CUSTOM_API_CALL_KEY ) ) ||
( isString ( operation ) && operation . includes ( CUSTOM_API_CALL_KEY ) )
) ;
2024-06-03 02:34:51 -07:00
}
return false ;
2023-12-08 07:59:03 -08:00
}
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 {
2024-01-04 01:22:56 -08:00
const pinDataNodeNames = Object . keys ( workflowsStore . pinnedWorkflowData ? ? { } ) ;
2023-12-08 07:59:03 -08:00
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 ,
} ,
} ) ,
] ,
} ,
} ;
}
2024-07-08 03:25:18 -07:00
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 ,
} ) ;
}
2023-12-08 07:59:03 -08:00
function updateNodesInputIssues() {
const nodes = workflowsStore . allNodes ;
for ( const node of nodes ) {
2024-07-08 03:25:18 -07:00
updateNodeInputIssues ( node ) ;
2023-12-08 07:59:03 -08:00
}
}
function updateNodesExecutionIssues() {
const nodes = workflowsStore . allNodes ;
for ( const node of nodes ) {
workflowsStore . setNodeIssue ( {
node : node.name ,
type : 'execution' ,
value : hasNodeExecutionIssues ( node ) ? true : null ,
} ) ;
}
}
2024-07-08 03:25:18 -07:00
function updateNodesParameterIssues() {
const nodes = workflowsStore . allNodes ;
for ( const node of nodes ) {
updateNodeParameterIssues ( node ) ;
}
}
2023-12-08 07:59:03 -08:00
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 ) ;
}
}
2024-06-10 06:23:06 -07:00
function updateNodeParameterIssues ( node : INodeUi , nodeType? : INodeTypeDescription | null ) : void {
2023-12-08 07:59:03 -08:00
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 < ConnectionTypes | INodeInputConfiguration > = [ ] ;
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 ) ;
2024-03-11 02:43:30 -07:00
// 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
) {
2023-12-08 07:59:03 -08:00
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 ,
} ;
}
2024-05-17 01:53:15 -07:00
userCredentials = credentialsStore . getCredentialsByType ( credentialTypeDescription . name ) ;
2023-12-08 07:59:03 -08:00
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' ,
2024-06-03 02:34:51 -07:00
value : issues?.credentials ? ? null ,
2023-12-08 07:59:03 -08:00
} ) ;
}
}
function getNodeInputData (
node : INodeUi | null ,
runIndex = 0 ,
outputIndex = 0 ,
paneType : NodePanelType = 'output' ,
connectionType : ConnectionTypes = NodeConnectionType . Main ,
) : INodeExecutionData [ ] {
2024-07-31 06:11:42 -07:00
//TODO: check if this needs to be fixed in different place
if (
node ? . type === SPLIT_IN_BATCHES_NODE_TYPE &&
paneType === 'input' &&
runIndex !== 0 &&
outputIndex !== 0
) {
runIndex = runIndex - 1 ;
}
2023-12-08 07:59:03 -08:00
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 ;
2023-12-13 06:57:01 -08:00
const taskData = get ( runData , [ node . name , runIndex ] ) ;
2023-12-08 07:59:03 -08:00
if ( ! taskData ) {
return [ ] ;
}
2023-12-28 00:49:58 -08:00
let data : ITaskDataConnections | undefined = taskData . data ;
2023-12-08 07:59:03 -08:00
if ( paneType === 'input' && taskData . inputOverride ) {
2024-04-04 01:13:37 -07:00
data = taskData . inputOverride ;
2023-12-08 07:59:03 -08:00
}
if ( ! data ) {
return [ ] ;
}
return getInputData ( data , outputIndex , connectionType ) ;
}
function getInputData (
connectionsData : ITaskDataConnections ,
outputIndex : number ,
connectionType : ConnectionTypes = NodeConnectionType . Main ,
) : INodeExecutionData [ ] {
2024-04-04 01:13:37 -07:00
return connectionsData ? . [ connectionType ] ? . [ outputIndex ] ? ? [ ] ;
2023-12-08 07:59:03 -08:00
}
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 ;
2024-04-04 01:13:37 -07:00
const runDataOfNode = runData ? . [ node ] ? . [ runIndex ] ? . data ;
if ( ! runDataOfNode ) {
2023-12-08 07:59:03 -08:00
return [ ] ;
}
2024-04-04 01:13:37 -07:00
const inputData = getInputData ( runDataOfNode , outputIndex , connectionType ) ;
2023-12-08 07:59:03 -08:00
const returnData : IBinaryKeyData [ ] = [ ] ;
for ( let i = 0 ; i < inputData . length ; i ++ ) {
2024-04-04 01:13:37 -07:00
const binaryDataInIdx = inputData [ i ] ? . binary ;
if ( binaryDataInIdx !== undefined ) {
returnData . push ( binaryDataInIdx ) ;
2023-12-08 07:59:03 -08:00
}
}
return returnData ;
}
function disableNodes ( nodes : INodeUi [ ] , trackHistory = false ) {
const telemetry = useTelemetry ( ) ;
if ( trackHistory ) {
historyStore . startRecordingUndo ( ) ;
}
2024-02-02 07:02:41 -08:00
const newDisabledState = nodes . some ( ( node ) = > ! node . disabled ) ;
2023-12-08 07:59:03 -08:00
for ( const node of nodes ) {
2024-02-02 07:02:41 -08:00
if ( newDisabledState === node . disabled ) {
continue ;
}
2023-12-08 07:59:03 -08:00
// Toggle disabled flag
const updateInformation = {
name : node.name ,
properties : {
2024-02-02 07:02:41 -08:00
disabled : newDisabledState ,
2023-12-08 07:59:03 -08:00
} 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 (
2024-02-02 07:02:41 -08:00
new EnableNodeToggleCommand ( node . name , node . disabled === true , newDisabledState ) ,
2023-12-08 07:59:03 -08:00
) ;
}
}
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 ;
}
2024-01-19 05:44:54 -08:00
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 ) ;
2024-06-03 02:34:51 -07:00
const connectionType = Object . keys ( allNodeConnections ) [ 0 ] as NodeConnectionType ;
2024-01-19 05:44:54 -08:00
const nodeConnections = allNodeConnections [ connectionType ] ;
const outputMap = NodeViewUtils . getOutputSummary (
data ,
nodeConnections || [ ] ,
2024-06-03 02:34:51 -07:00
connectionType ? ? NodeConnectionType . Main ,
2024-01-19 05:44:54 -08:00
) ;
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 ) ,
2024-06-03 02:34:51 -07:00
connectionType ,
2024-01-19 05:44:54 -08:00
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 ( ) ;
}
}
} ,
) ;
} ) ;
} ) ;
}
2024-06-24 06:08:24 -07:00
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 < void > {
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 ,
} ) ;
} ) ;
} ) ;
}
2024-08-21 23:31:48 -07:00
function removePinDataConnections ( event : UnpinNodeDataEvent ) {
for ( const nodeName of event . nodeNames ) {
2024-06-24 06:08:24 -07:00
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 ) ;
2024-08-21 23:31:48 -07:00
}
2024-06-24 06:08:24 -07:00
}
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 ) ;
}
2023-12-08 07:59:03 -08:00
return {
hasProxyAuth ,
isCustomApiCallSelected ,
getParameterValue ,
displayParameter ,
getNodeIssues ,
updateNodesInputIssues ,
updateNodesExecutionIssues ,
2024-07-08 03:25:18 -07:00
updateNodesParameterIssues ,
updateNodeInputIssues ,
2023-12-08 07:59:03 -08:00
updateNodeCredentialIssuesByName ,
updateNodeCredentialIssues ,
updateNodeParameterIssuesByName ,
updateNodeParameterIssues ,
getBinaryData ,
disableNodes ,
getNodeSubtitle ,
updateNodesCredentialsIssues ,
getNodeInputData ,
2024-01-19 05:44:54 -08:00
setSuccessOutput ,
2024-07-10 01:53:27 -07:00
matchCredentials ,
2024-06-24 06:08:24 -07:00
isInsertingNodes ,
credentialsUpdated ,
isProductionExecutionPreview ,
pullConnActiveNodeName ,
deleteJSPlumbConnection ,
loadNodesProperties ,
addNodes ,
2024-07-18 01:59:11 -07:00
addConnections ,
2024-06-24 06:08:24 -07:00
addConnection ,
removeConnection ,
removeConnectionByConnectionInfo ,
addPinDataConnections ,
removePinDataConnections ,
2023-12-08 07:59:03 -08:00
} ;
}