2019-06-23 03:35:23 -07:00
< template >
< div class = "node-view-root" >
< div
class = "node-view-wrapper"
: class = "workflowClasses"
2020-10-23 04:44:34 -07:00
@ touchstart = "mouseDown"
@ touchend = "mouseUp"
@ touchmove = "mouseMoveNodeWorkflow"
2019-06-23 03:35:23 -07:00
@ mousedown = "mouseDown"
2020-10-23 09:15:52 -07:00
v - touch : tap = "touchTap"
2019-06-23 03:35:23 -07:00
@ mouseup = "mouseUp"
2019-07-25 22:41:09 -07:00
@ wheel = "wheelScroll"
2019-06-23 03:35:23 -07:00
>
2020-10-23 04:44:34 -07:00
< div id = "node-view-background" class = "node-view-background" :style ="backgroundStyle" > < / div >
2019-06-23 03:35:23 -07:00
< div id = "node-view" class = "node-view" :style ="workflowStyle" >
< node
v - for = "nodeData in nodes"
@ duplicateNode = "duplicateNode"
@ deselectAllNodes = "deselectAllNodes"
2019-07-17 10:05:03 -07:00
@ deselectNode = "nodeDeselectedByName"
2019-06-23 03:35:23 -07:00
@ nodeSelected = "nodeSelectedByName"
@ removeNode = "removeNode"
@ runWorkflow = "runWorkflow"
: id = "'node-' + getNodeIndex(nodeData.name)"
: key = "getNodeIndex(nodeData.name)"
: name = "nodeData.name"
2020-05-24 05:06:22 -07:00
: isReadOnly = "isReadOnly"
2019-06-23 03:35:23 -07:00
: instance = "instance"
2021-08-07 00:35:59 -07:00
: isActive = "!!activeNode && activeNode.name === nodeData.name"
2019-06-23 03:35:23 -07:00
> < / node >
< / div >
< / div >
< DataDisplay @valueChanged ="valueChanged" / >
< div v-if ="!createNodeActive && !isReadOnly" class="node-creator-button" title="Add Node" @click="openNodeCreator" >
2021-08-29 04:36:17 -07:00
< n8n -icon -button size = "xlarge" icon = "plus" / >
2019-06-23 03:35:23 -07:00
< / div >
< node -creator
: active = "createNodeActive"
@ nodeTypeSelected = "nodeTypeSelected"
@ closeNodeCreator = "closeNodeCreator"
> < / n o d e - c r e a t o r >
2021-05-29 11:31:21 -07:00
< div : class = "{ 'zoom-menu': true, expanded: !sidebarMenuCollapsed }" >
2021-06-22 10:33:07 -07:00
< button @click ="zoomToFit" class = "button-white" title = "Zoom to Fit" >
< font -awesome -icon icon = "expand" / >
< / button >
2021-06-23 03:49:34 -07:00
< button @click ="zoomIn()" class = "button-white" title = "Zoom In" >
2019-06-23 03:35:23 -07:00
< font -awesome -icon icon = "search-plus" / >
< / button >
2021-06-23 03:49:34 -07:00
< button @click ="zoomOut()" class = "button-white" title = "Zoom Out" >
2019-06-23 03:35:23 -07:00
< font -awesome -icon icon = "search-minus" / >
< / button >
< button
v - if = "nodeViewScale !== 1"
2021-06-23 03:49:34 -07:00
@ click = "resetZoom()"
2019-06-23 03:35:23 -07:00
class = "button-white"
title = "Reset Zoom"
>
< font -awesome -icon icon = "undo" title = "Reset Zoom" / >
< / button >
< / div >
< div class = "workflow-execute-wrapper" v-if ="!isReadOnly" >
2021-08-29 04:36:17 -07:00
< n8n -button
2019-06-23 03:35:23 -07:00
@ click . stop = "runWorkflow()"
2021-08-29 04:36:17 -07:00
: loading = "workflowRunning"
: label = "runButtonText"
size = "large"
icon = "play-circle"
2019-06-23 03:35:23 -07:00
title = "Executes the Workflow from the Start or Webhook Node."
2021-08-29 04:36:17 -07:00
: type = "workflowRunning ? 'light' : 'primary'"
/ >
2019-06-23 03:35:23 -07:00
2021-08-29 04:36:17 -07:00
< n8n -icon -button
2019-06-23 03:35:23 -07:00
v - if = "workflowRunning === true && !executionWaitingForWebhook"
2021-08-29 04:36:17 -07:00
icon = "stop"
size = "large"
2019-06-23 03:35:23 -07:00
class = "stop-execution"
2021-08-29 04:36:17 -07:00
type = "light"
2019-06-23 03:35:23 -07:00
: title = "stopExecutionInProgress ? 'Stopping current execution':'Stop current execution'"
2021-08-29 04:36:17 -07:00
: loading = "stopExecutionInProgress"
@ click . stop = "stopExecution()"
/ >
< n8n -icon -button
2019-06-23 03:35:23 -07:00
v - if = "workflowRunning === true && executionWaitingForWebhook === true"
class = "stop-execution"
2021-08-29 04:36:17 -07:00
icon = "stop"
size = "large"
2019-06-23 03:35:23 -07:00
title = "Stop waiting for Webhook call"
2021-08-29 04:36:17 -07:00
type = "light"
@ click . stop = "stopWaitingForWebhook()"
/ >
< n8n -icon -button
2019-06-24 04:52:03 -07:00
v - if = "!isReadOnly && workflowExecution && !workflowRunning"
title = "Deletes the current Execution Data."
2021-08-29 04:36:17 -07:00
icon = "trash"
size = "large"
@ click . stop = "clearExecutionData()"
/ >
2019-06-23 03:35:23 -07:00
< / div >
2021-05-29 11:31:21 -07:00
< Modals / >
2019-06-23 03:35:23 -07:00
< / div >
< / template >
< script lang = "ts" >
import Vue from 'vue' ;
2020-05-24 05:06:22 -07:00
import {
OverlaySpec ,
} from 'jsplumb' ;
2019-12-29 13:02:21 -08:00
import { MessageBoxInputData } from 'element-ui/types/message-box' ;
2019-08-02 08:06:06 -07:00
import { jsPlumb , Endpoint , OnConnectionBindInfo } from 'jsplumb' ;
2021-10-18 20:57:49 -07:00
import { NODE _NAME _PREFIX , PLACEHOLDER _EMPTY _WORKFLOW _ID , START _NODE _TYPE , WEBHOOK _NODE _TYPE , WORKFLOW _OPEN _MODAL _KEY } from '@/constants' ;
2019-06-23 03:35:23 -07:00
import { copyPaste } from '@/components/mixins/copyPaste' ;
2021-01-19 14:48:30 -08:00
import { externalHooks } from '@/components/mixins/externalHooks' ;
2019-06-23 03:35:23 -07:00
import { genericHelpers } from '@/components/mixins/genericHelpers' ;
import { mouseSelect } from '@/components/mixins/mouseSelect' ;
import { moveNodeWorkflow } from '@/components/mixins/moveNodeWorkflow' ;
import { restApi } from '@/components/mixins/restApi' ;
import { showMessage } from '@/components/mixins/showMessage' ;
2020-08-25 11:38:09 -07:00
import { titleChange } from '@/components/mixins/titleChange' ;
2021-07-22 01:22:17 -07:00
import { newVersions } from '@/components/mixins/newVersions' ;
2020-08-25 11:38:09 -07:00
2019-06-23 03:35:23 -07:00
import { workflowHelpers } from '@/components/mixins/workflowHelpers' ;
import { workflowRun } from '@/components/mixins/workflowRun' ;
import DataDisplay from '@/components/DataDisplay.vue' ;
2021-05-29 11:31:21 -07:00
import Modals from '@/components/Modals.vue' ;
2019-06-23 03:35:23 -07:00
import Node from '@/components/Node.vue' ;
2021-06-17 22:58:26 -07:00
import NodeCreator from '@/components/NodeCreator/NodeCreator.vue' ;
2019-06-23 03:35:23 -07:00
import NodeSettings from '@/components/NodeSettings.vue' ;
import RunData from '@/components/RunData.vue' ;
2021-06-23 03:49:34 -07:00
import { getLeftmostTopNode , getWorkflowCorners , scaleSmaller , scaleBigger , scaleReset } from './helpers' ;
2021-06-22 10:33:07 -07:00
2019-06-23 03:35:23 -07:00
import mixins from 'vue-typed-mixins' ;
2020-09-02 00:16:16 -07:00
import { v4 as uuidv4 } from 'uuid' ;
2019-06-23 03:35:23 -07:00
import {
IConnection ,
IConnections ,
IDataObject ,
INode ,
INodeConnections ,
INodeIssues ,
INodeTypeDescription ,
2021-09-21 10:38:24 -07:00
INodeTypeNameVersion ,
NodeInputConnections ,
2019-06-23 03:35:23 -07:00
NodeHelpers ,
2019-08-09 09:47:33 -07:00
Workflow ,
2021-02-09 14:32:40 -08:00
IRun ,
2021-10-13 15:21:00 -07:00
INodeCredentialsDetails ,
2019-06-23 03:35:23 -07:00
} from 'n8n-workflow' ;
import {
IConnectionsUi ,
2021-10-13 15:21:00 -07:00
ICredentialsResponse ,
2019-06-23 03:35:23 -07:00
IExecutionResponse ,
IN8nUISettings ,
IWorkflowDb ,
IWorkflowData ,
INodeUi ,
IUpdateInformation ,
IWorkflowDataUpdate ,
XYPositon ,
2021-02-09 14:32:40 -08:00
IPushDataExecutionFinished ,
2021-05-29 11:31:21 -07:00
ITag ,
2021-06-22 10:33:07 -07:00
IWorkflowTemplate ,
2021-09-22 00:23:37 -07:00
IExecutionsSummary ,
2019-06-23 03:35:23 -07:00
} from '../Interface' ;
2021-05-29 11:31:21 -07:00
import { mapGetters } from 'vuex' ;
2021-11-09 00:59:48 -08:00
import { loadLanguage } from '@/i18n' ;
2019-06-23 03:35:23 -07:00
2021-06-22 10:33:07 -07:00
const NODE _SIZE = 100 ;
const DEFAULT _START _POSITION _X = 250 ;
const DEFAULT _START _POSITION _Y = 300 ;
const HEADER _HEIGHT = 65 ;
const SIDEBAR _WIDTH = 65 ;
const DEFAULT _START _NODE = {
name : 'Start' ,
2021-10-18 20:57:49 -07:00
type : START _NODE _TYPE ,
2021-06-22 10:33:07 -07:00
typeVersion : 1 ,
position : [
DEFAULT _START _POSITION _X ,
DEFAULT _START _POSITION _Y ,
] as XYPositon ,
parameters : { } ,
} ;
2019-06-23 03:35:23 -07:00
export default mixins (
copyPaste ,
2021-01-19 14:48:30 -08:00
externalHooks ,
2019-06-23 03:35:23 -07:00
genericHelpers ,
mouseSelect ,
moveNodeWorkflow ,
restApi ,
showMessage ,
2020-08-25 11:38:09 -07:00
titleChange ,
2019-06-23 03:35:23 -07:00
workflowHelpers ,
workflowRun ,
2021-07-22 01:22:17 -07:00
newVersions ,
2019-06-23 03:35:23 -07:00
)
. extend ( {
name : 'NodeView' ,
components : {
DataDisplay ,
2021-05-29 11:31:21 -07:00
Modals ,
2019-06-23 03:35:23 -07:00
Node ,
NodeCreator ,
NodeSettings ,
RunData ,
} ,
errorCaptured : ( err , vm , info ) => {
2019-09-20 05:07:02 -07:00
console . error ( 'errorCaptured' ) ; // eslint-disable-line no-console
console . error ( err ) ; // eslint-disable-line no-console
2019-06-23 03:35:23 -07:00
} ,
watch : {
// Listen to route changes and load the workflow accordingly
'$route' : 'initView' ,
activeNode ( ) {
// When a node gets set as active deactivate the create-menu
this . createNodeActive = false ;
} ,
2020-07-20 07:57:58 -07:00
nodes : {
2021-05-05 17:46:33 -07:00
async handler ( value , oldValue ) {
2020-07-20 07:57:58 -07:00
// Load a workflow
let workflowId = null as string | null ;
if ( this . $route && this . $route . params . name ) {
workflowId = this . $route . params . name ;
}
} ,
2020-09-01 07:06:08 -07:00
deep : true ,
2020-07-20 07:57:58 -07:00
} ,
connections : {
2021-05-05 17:46:33 -07:00
async handler ( value , oldValue ) {
2020-07-20 07:57:58 -07:00
// Load a workflow
let workflowId = null as string | null ;
if ( this . $route && this . $route . params . name ) {
workflowId = this . $route . params . name ;
}
} ,
2020-09-01 07:06:08 -07:00
deep : true ,
2020-07-20 07:57:58 -07:00
} ,
2021-11-09 00:59:48 -08:00
defaultLocale ( newLocale , oldLocale ) {
console . log ( ` Switching locale from ${ oldLocale } to ${ newLocale } ` ) ;
loadLanguage ( newLocale ) ;
} ,
2019-06-23 03:35:23 -07:00
} ,
2020-10-25 04:47:49 -07:00
async beforeRouteLeave ( to , from , next ) {
const result = this . $store . getters . getStateIsDirty ;
if ( result ) {
const importConfirm = await this . confirmMessage ( ` When you switch workflows your current workflow changes will be lost. ` , 'Save your Changes?' , 'warning' , 'Yes, switch workflows and forget changes' ) ;
if ( importConfirm === false ) {
next ( false ) ;
} else {
// Prevent other popups from displaying
this . $store . commit ( 'setStateDirty' , false ) ;
next ( ) ;
}
} else {
next ( ) ;
}
} ,
2019-06-23 03:35:23 -07:00
computed : {
2021-05-29 11:31:21 -07:00
... mapGetters ( 'ui' , [
'sidebarMenuCollapsed' ,
] ) ,
2021-11-09 00:59:48 -08:00
defaultLocale ( ) : string {
return this . $store . getters . defaultLocale ;
} ,
2019-06-23 03:35:23 -07:00
activeNode ( ) : INodeUi | null {
return this . $store . getters . activeNode ;
} ,
executionWaitingForWebhook ( ) : boolean {
return this . $store . getters . executionWaitingForWebhook ;
} ,
lastSelectedNode ( ) : INodeUi {
return this . $store . getters . lastSelectedNode ;
} ,
connections ( ) : IConnectionsUi {
return this . $store . getters . allConnections ;
} ,
nodes ( ) : INodeUi [ ] {
return this . $store . getters . allNodes ;
} ,
runButtonText ( ) : string {
if ( this . workflowRunning === false ) {
return 'Execute Workflow' ;
}
if ( this . executionWaitingForWebhook === true ) {
return 'Waiting for Webhook-Call' ;
}
return 'Executing Workflow' ;
} ,
workflowStyle ( ) : object {
const offsetPosition = this . $store . getters . getNodeViewOffsetPosition ;
return {
left : offsetPosition [ 0 ] + 'px' ,
top : offsetPosition [ 1 ] + 'px' ,
} ;
} ,
backgroundStyle ( ) : object {
const offsetPosition = this . $store . getters . getNodeViewOffsetPosition ;
return {
'transform' : ` scale( ${ this . nodeViewScale } ) ` ,
'background-position' : ` right ${ - offsetPosition [ 0 ] } px bottom ${ - offsetPosition [ 1 ] } px ` ,
} ;
} ,
workflowClasses ( ) {
const returnClasses = [ ] ;
if ( this . ctrlKeyPressed === true ) {
if ( this . $store . getters . isNodeViewMoveInProgress === true ) {
returnClasses . push ( 'move-in-process' ) ;
} else {
returnClasses . push ( 'move-active' ) ;
}
}
if ( this . selectActive || this . ctrlKeyPressed === true ) {
// Makes sure that nothing gets selected while select or move is active
returnClasses . push ( 'do-not-select' ) ;
}
return returnClasses ;
} ,
2019-06-24 04:52:03 -07:00
workflowExecution ( ) : IExecutionResponse | null {
return this . $store . getters . getWorkflowExecution ;
} ,
2019-06-23 03:35:23 -07:00
workflowRunning ( ) : boolean {
return this . $store . getters . isActionActive ( 'workflowRunning' ) ;
} ,
} ,
data ( ) {
return {
createNodeActive : false ,
instance : jsPlumb . getInstance ( ) ,
2019-07-17 03:47:20 -07:00
lastClickPosition : [ 450 , 450 ] as XYPositon ,
2019-06-23 03:35:23 -07:00
nodeViewScale : 1 ,
ctrlKeyPressed : false ,
stopExecutionInProgress : false ,
2021-06-22 10:33:07 -07:00
blankRedirect : false ,
2021-10-13 15:21:00 -07:00
credentialsUpdated : false ,
2019-06-23 03:35:23 -07:00
} ;
} ,
beforeDestroy ( ) {
// Make sure the event listeners get removed again else we
// could add up with them registred multiple times
document . removeEventListener ( 'keydown' , this . keyDown ) ;
document . removeEventListener ( 'keyup' , this . keyUp ) ;
} ,
methods : {
2019-06-24 04:52:03 -07:00
clearExecutionData ( ) {
this . $store . commit ( 'setWorkflowExecutionData' , null ) ;
this . updateNodesExecutionIssues ( ) ;
} ,
2019-06-23 03:35:23 -07:00
openNodeCreator ( ) {
this . createNodeActive = true ;
2021-10-18 20:57:49 -07:00
this . $externalHooks ( ) . run ( 'nodeView.createNodeActiveChanged' , { source : 'add_node_button' , createNodeActive : this . createNodeActive } ) ;
this . $telemetry . trackNodesPanel ( 'nodeView.createNodeActiveChanged' , { source : 'add_node_button' , workflow _id : this . $store . getters . workflowId , createNodeActive : this . createNodeActive } ) ;
2019-06-23 03:35:23 -07:00
} ,
async openExecution ( executionId : string ) {
2021-06-26 23:37:53 -07:00
this . resetWorkspace ( ) ;
2019-06-23 03:35:23 -07:00
let data : IExecutionResponse | undefined ;
try {
data = await this . restApi ( ) . getExecution ( executionId ) ;
} catch ( error ) {
this . $showError ( error , 'Problem loading execution' , 'There was a problem opening the execution:' ) ;
return ;
}
if ( data === undefined ) {
throw new Error ( ` Execution with id " ${ executionId } " could not be found! ` ) ;
}
2020-09-09 05:28:13 -07:00
this . $store . commit ( 'setWorkflowName' , { newName : data . workflowData . name , setStateDirty : false } ) ;
2019-06-23 03:35:23 -07:00
this . $store . commit ( 'setWorkflowId' , PLACEHOLDER _EMPTY _WORKFLOW _ID ) ;
this . $store . commit ( 'setWorkflowExecutionData' , data ) ;
await this . addNodes ( JSON . parse ( JSON . stringify ( data . workflowData . nodes ) ) , JSON . parse ( JSON . stringify ( data . workflowData . connections ) ) ) ;
2021-06-26 23:37:53 -07:00
this . $nextTick ( ( ) => {
this . zoomToFit ( ) ;
this . $store . commit ( 'setStateDirty' , false ) ;
} ) ;
2021-05-05 17:46:33 -07:00
this . $externalHooks ( ) . run ( 'execution.open' , { workflowId : data . workflowData . id , workflowName : data . workflowData . name , executionId } ) ;
2021-10-18 20:57:49 -07:00
this . $telemetry . track ( 'User opened read-only execution' , { workflow _id : data . workflowData . id , execution _mode : data . mode , execution _finished : data . finished } ) ;
2021-07-10 02:34:41 -07:00
if ( data . finished !== true && data . data . resultData . error ) {
// Check if any node contains an error
let nodeErrorFound = false ;
if ( data . data . resultData . runData ) {
const runData = data . data . resultData . runData ;
errorCheck :
for ( const nodeName of Object . keys ( runData ) ) {
for ( const taskData of runData [ nodeName ] ) {
if ( taskData . error ) {
nodeErrorFound = true ;
break errorCheck ;
}
}
}
}
if ( nodeErrorFound === false ) {
2021-10-18 20:57:49 -07:00
const resultError = data . data . resultData . error ;
const errorMessage = this . $getExecutionError ( resultError ) ;
const shouldTrack = resultError && resultError . node && resultError . node . type . startsWith ( 'n8n-nodes-base' ) ;
2021-07-10 02:34:41 -07:00
this . $showMessage ( {
title : 'Failed execution' ,
message : errorMessage ,
type : 'error' ,
2021-10-18 20:57:49 -07:00
} , shouldTrack ) ;
2021-07-10 02:34:41 -07:00
if ( data . data . resultData . error . stack ) {
// Display some more information for now in console to make debugging easier
// TODO: Improve this in the future by displaying in UI
2021-07-11 09:18:01 -07:00
console . error ( ` Execution ${ executionId } error: ` ) ; // eslint-disable-line no-console
console . error ( data . data . resultData . error . stack ) ; // eslint-disable-line no-console
2021-07-10 02:34:41 -07:00
}
}
}
2021-09-22 00:23:37 -07:00
if ( ( data as IExecutionsSummary ) . waitTill ) {
this . $showMessage ( {
title : ` This execution hasn't finished yet ` ,
message : ` <a onclick="window.location.reload(false);">Refresh</a> to see the latest status.<br/> <a href="https://docs.n8n.io/nodes/n8n-nodes-base.wait/" target="_blank">More info</a> ` ,
type : 'warning' ,
duration : 0 ,
} ) ;
}
2019-06-23 03:35:23 -07:00
} ,
2021-06-22 10:33:07 -07:00
async openWorkflowTemplate ( templateId : string ) {
this . setLoadingText ( 'Loading template' ) ;
this . resetWorkspace ( ) ;
let data : IWorkflowTemplate | undefined ;
try {
this . $externalHooks ( ) . run ( 'template.requested' , { templateId } ) ;
data = await this . $store . dispatch ( 'workflows/getWorkflowTemplate' , templateId ) ;
if ( ! data ) {
throw new Error ( ` Workflow template with id " ${ templateId } " could not be found! ` ) ;
}
data . workflow . nodes . forEach ( ( node ) => {
if ( ! this . $store . getters . nodeType ( node . type ) ) {
const name = node . type . replace ( 'n8n-nodes-base.' , '' ) ;
throw new Error ( ` The ${ name } node is not supported ` ) ;
}
} ) ;
} catch ( error ) {
this . $showError ( error , ` Couldn't import workflow ` ) ;
this . $router . push ( { name : 'NodeViewNew' } ) ;
return ;
}
const nodes = data . workflow . nodes ;
const hasStartNode = ! ! nodes . find ( node => node . type === START _NODE _TYPE ) ;
const leftmostTop = getLeftmostTopNode ( nodes ) ;
const diffX = DEFAULT _START _POSITION _X - leftmostTop . position [ 0 ] ;
const diffY = DEFAULT _START _POSITION _Y - leftmostTop . position [ 1 ] ;
data . workflow . nodes . map ( ( node ) => {
node . position [ 0 ] += diffX + ( hasStartNode ? 0 : NODE _SIZE * 2 ) ;
node . position [ 1 ] += diffY ;
} ) ;
if ( ! hasStartNode ) {
data . workflow . nodes . push ( DEFAULT _START _NODE ) ;
}
this . blankRedirect = true ;
2021-10-07 12:54:22 -07:00
this . $router . push ( { name : 'NodeViewNew' , query : { templateId } } ) ;
2021-06-22 10:33:07 -07:00
await this . addNodes ( data . workflow . nodes , data . workflow . connections ) ;
await this . $store . dispatch ( 'workflows/setNewWorkflowName' , data . name ) ;
this . $nextTick ( ( ) => {
this . zoomToFit ( ) ;
this . $store . commit ( 'setStateDirty' , true ) ;
} ) ;
this . $externalHooks ( ) . run ( 'template.open' , { templateId , templateName : data . name , workflow : data . workflow } ) ;
} ,
2019-06-23 03:35:23 -07:00
async openWorkflow ( workflowId : string ) {
this . resetWorkspace ( ) ;
let data : IWorkflowDb | undefined ;
try {
data = await this . restApi ( ) . getWorkflow ( workflowId ) ;
} catch ( error ) {
this . $showError ( error , 'Problem opening workflow' , 'There was a problem opening the workflow:' ) ;
return ;
}
if ( data === undefined ) {
throw new Error ( ` Workflow with id " ${ workflowId } " could not be found! ` ) ;
}
this . $store . commit ( 'setActive' , data . active || false ) ;
this . $store . commit ( 'setWorkflowId' , workflowId ) ;
2020-09-09 05:28:13 -07:00
this . $store . commit ( 'setWorkflowName' , { newName : data . name , setStateDirty : false } ) ;
2019-06-23 03:35:23 -07:00
this . $store . commit ( 'setWorkflowSettings' , data . settings || { } ) ;
2021-05-29 11:31:21 -07:00
const tags = ( data . tags || [ ] ) as ITag [ ] ;
this . $store . commit ( 'tags/upsertTags' , tags ) ;
const tagIds = tags . map ( ( tag ) => tag . id ) ;
this . $store . commit ( 'setWorkflowTagIds' , tagIds || [ ] ) ;
2019-06-23 03:35:23 -07:00
await this . addNodes ( data . nodes , data . connections ) ;
2021-10-13 15:21:00 -07:00
if ( ! this . credentialsUpdated ) {
this . $store . commit ( 'setStateDirty' , false ) ;
}
2020-07-09 13:54:50 -07:00
2021-06-22 10:33:07 -07:00
this . zoomToFit ( ) ;
2020-10-25 04:47:49 -07:00
2021-01-19 14:48:30 -08:00
this . $externalHooks ( ) . run ( 'workflow.open' , { workflowId , workflowName : data . name } ) ;
2020-07-09 13:54:50 -07:00
return data ;
2019-06-23 03:35:23 -07:00
} ,
2020-10-23 09:15:52 -07:00
touchTap ( e : MouseEvent | TouchEvent ) {
if ( this . isTouchDevice ) {
this . mouseDown ( e ) ;
}
} ,
2020-10-23 04:44:34 -07:00
mouseDown ( e : MouseEvent | TouchEvent ) {
2019-06-23 03:35:23 -07:00
// Save the location of the mouse click
2020-10-23 04:44:34 -07:00
const position = this . getMousePosition ( e ) ;
2019-07-17 03:44:00 -07:00
const offsetPosition = this . $store . getters . getNodeViewOffsetPosition ;
2020-10-23 04:44:34 -07:00
this . lastClickPosition [ 0 ] = position . x - offsetPosition [ 0 ] ;
this . lastClickPosition [ 1 ] = position . y - offsetPosition [ 1 ] ;
2019-06-23 03:35:23 -07:00
2020-10-23 04:44:34 -07:00
this . mouseDownMouseSelect ( e as MouseEvent ) ;
this . mouseDownMoveWorkflow ( e as MouseEvent ) ;
2019-06-23 03:35:23 -07:00
// Hide the node-creator
this . createNodeActive = false ;
} ,
mouseUp ( e : MouseEvent ) {
this . mouseUpMouseSelect ( e ) ;
this . mouseUpMoveWorkflow ( e ) ;
} ,
2019-07-25 22:41:09 -07:00
wheelScroll ( e : WheelEvent ) {
2021-02-07 22:51:23 -08:00
//* Control + scroll zoom
2021-02-07 22:52:09 -08:00
if ( e . ctrlKey ) {
if ( e . deltaY > 0 ) {
2021-06-23 03:49:34 -07:00
this . zoomOut ( ) ;
2021-02-07 22:52:09 -08:00
} else {
2021-06-23 03:49:34 -07:00
this . zoomIn ( ) ;
2021-02-07 22:52:09 -08:00
}
2021-02-07 22:51:23 -08:00
e . preventDefault ( ) ;
return ;
}
2019-07-25 22:41:09 -07:00
this . wheelMoveWorkflow ( e ) ;
} ,
2019-06-23 03:35:23 -07:00
keyUp ( e : KeyboardEvent ) {
2019-07-16 22:26:44 -07:00
if ( e . key === this . controlKeyCode ) {
2019-06-23 03:35:23 -07:00
this . ctrlKeyPressed = false ;
}
} ,
async keyDown ( e : KeyboardEvent ) {
// @ts-ignore
const path = e . path || ( e . composedPath && e . composedPath ( ) ) ;
// Check if the keys got emitted from a message box or from something
// else which should ignore the default keybindings
for ( let index = 0 ; index < path . length ; index ++ ) {
if ( path [ index ] . className && typeof path [ index ] . className === 'string' && (
2021-06-23 03:49:34 -07:00
path [ index ] . className . includes ( 'ignore-key-press' )
2019-06-23 03:35:23 -07:00
) ) {
return ;
}
}
2021-06-29 01:47:28 -07:00
// el-dialog or el-message-box element is open
if ( window . document . body . classList . contains ( 'el-popup-parent--hidden' ) ) {
return ;
}
if ( e . key === 'Escape' ) {
this . createNodeActive = false ;
if ( this . activeNode ) {
this . $externalHooks ( ) . run ( 'dataDisplay.nodeEditingFinished' ) ;
this . $store . commit ( 'setActiveNode' , null ) ;
}
return ;
}
// node modal is open
if ( this . activeNode ) {
2021-05-29 11:31:21 -07:00
return ;
}
2019-06-23 03:35:23 -07:00
if ( e . key === 'd' ) {
this . callDebounced ( 'deactivateSelectedNode' , 350 ) ;
} else if ( e . key === 'Delete' ) {
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
this . callDebounced ( 'deleteSelectedNodes' , 500 ) ;
2021-06-29 01:47:28 -07:00
2019-06-23 03:35:23 -07:00
} else if ( e . key === 'Tab' ) {
2020-01-20 08:47:52 -08:00
this . createNodeActive = ! this . createNodeActive && ! this . isReadOnly ;
2021-10-18 20:57:49 -07:00
this . $externalHooks ( ) . run ( 'nodeView.createNodeActiveChanged' , { source : 'tab' , createNodeActive : this . createNodeActive } ) ;
this . $telemetry . trackNodesPanel ( 'nodeView.createNodeActiveChanged' , { source : 'tab' , workflow _id : this . $store . getters . workflowId , createNodeActive : this . createNodeActive } ) ;
2019-07-16 22:26:44 -07:00
} else if ( e . key === this . controlKeyCode ) {
2019-06-23 03:35:23 -07:00
this . ctrlKeyPressed = true ;
2021-06-29 01:47:28 -07:00
} else if ( e . key === 'F2' && ! this . isReadOnly ) {
2019-06-23 03:35:23 -07:00
const lastSelectedNode = this . lastSelectedNode ;
if ( lastSelectedNode !== null ) {
this . callDebounced ( 'renameNodePrompt' , 1500 , lastSelectedNode . name ) ;
}
2021-06-23 03:49:34 -07:00
} else if ( ( e . key === '=' || e . key === '+' ) && ! this . isCtrlKeyPressed ( e ) ) {
this . zoomIn ( ) ;
} else if ( ( e . key === '_' || e . key === '-' ) && ! this . isCtrlKeyPressed ( e ) ) {
this . zoomOut ( ) ;
} else if ( ( e . key === '0' ) && ! this . isCtrlKeyPressed ( e ) ) {
this . resetZoom ( ) ;
} else if ( ( e . key === '1' ) && ! this . isCtrlKeyPressed ( e ) ) {
this . zoomToFit ( ) ;
2019-07-16 22:26:44 -07:00
} else if ( ( e . key === 'a' ) && ( this . isCtrlKeyPressed ( e ) === true ) ) {
2019-06-23 03:35:23 -07:00
// Select all nodes
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
this . callDebounced ( 'selectAllNodes' , 1000 ) ;
2019-07-16 22:26:44 -07:00
} else if ( ( e . key === 'c' ) && ( this . isCtrlKeyPressed ( e ) === true ) ) {
2019-06-23 03:35:23 -07:00
this . callDebounced ( 'copySelectedNodes' , 1000 ) ;
2019-07-16 22:26:44 -07:00
} else if ( ( e . key === 'x' ) && ( this . isCtrlKeyPressed ( e ) === true ) ) {
2019-06-23 03:35:23 -07:00
// Cut nodes
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
this . callDebounced ( 'cutSelectedNodes' , 1000 ) ;
2019-07-16 22:26:44 -07:00
} else if ( e . key === 'o' && this . isCtrlKeyPressed ( e ) === true ) {
2019-06-23 03:35:23 -07:00
// Open workflow dialog
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
2021-10-18 20:57:49 -07:00
this . $store . dispatch ( 'ui/openModal' , WORKFLOW _OPEN _MODAL _KEY ) ;
2019-07-16 22:26:44 -07:00
} else if ( e . key === 'n' && this . isCtrlKeyPressed ( e ) === true && e . altKey === true ) {
2019-06-23 03:35:23 -07:00
// Create a new workflow
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
2021-06-04 14:13:42 -07:00
if ( this . $router . currentRoute . name === 'NodeViewNew' ) {
this . $root . $emit ( 'newWorkflow' ) ;
} else {
this . $router . push ( { name : 'NodeViewNew' } ) ;
}
2019-06-23 03:35:23 -07:00
this . $showMessage ( {
2021-06-04 14:13:42 -07:00
title : 'Workflow created' ,
2021-08-27 08:25:54 -07:00
message : 'A new workflow was successfully created!' ,
2019-06-23 03:35:23 -07:00
type : 'success' ,
} ) ;
2019-07-16 22:26:44 -07:00
} else if ( ( e . key === 's' ) && ( this . isCtrlKeyPressed ( e ) === true ) ) {
2019-06-23 03:35:23 -07:00
// Save workflow
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
2021-05-29 11:31:21 -07:00
if ( this . isReadOnly ) {
return ;
}
2020-07-20 07:57:58 -07:00
2021-10-18 20:57:49 -07:00
this . callDebounced ( 'saveCurrentWorkflow' , 1000 , undefined , true ) ;
2019-06-23 03:35:23 -07:00
} else if ( e . key === 'Enter' ) {
// Activate the last selected node
const lastSelectedNode = this . $store . getters . lastSelectedNode ;
if ( lastSelectedNode !== null ) {
this . $store . commit ( 'setActiveNode' , lastSelectedNode . name ) ;
}
2019-07-25 09:50:45 -07:00
} else if ( e . key === 'ArrowRight' && e . shiftKey === true ) {
2019-07-17 09:44:05 -07:00
// Select all downstream nodes
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
this . callDebounced ( 'selectDownstreamNodes' , 1000 ) ;
2019-07-25 09:50:45 -07:00
} else if ( e . key === 'ArrowRight' ) {
2019-06-23 03:35:23 -07:00
// Set child node active
const lastSelectedNode = this . $store . getters . lastSelectedNode ;
if ( lastSelectedNode === null ) {
return ;
}
const connections = this . $store . getters . connectionsByNodeName ( lastSelectedNode . name ) ;
if ( connections . main === undefined || connections . main . length === 0 ) {
return ;
}
this . callDebounced ( 'nodeSelectedByName' , 100 , connections . main [ 0 ] [ 0 ] . node , false , true ) ;
2019-07-25 09:50:45 -07:00
} else if ( e . key === 'ArrowLeft' && e . shiftKey === true ) {
2019-07-17 09:44:05 -07:00
// Select all downstream nodes
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
this . callDebounced ( 'selectUpstreamNodes' , 1000 ) ;
2019-07-25 09:50:45 -07:00
} else if ( e . key === 'ArrowLeft' ) {
2019-06-23 03:35:23 -07:00
// Set parent node active
const lastSelectedNode = this . $store . getters . lastSelectedNode ;
if ( lastSelectedNode === null ) {
return ;
}
const workflow = this . getWorkflow ( ) ;
if ( ! workflow . connectionsByDestinationNode . hasOwnProperty ( lastSelectedNode . name ) ) {
return ;
}
const connections = workflow . connectionsByDestinationNode [ lastSelectedNode . name ] ;
if ( connections . main === undefined || connections . main . length === 0 ) {
return ;
}
this . callDebounced ( 'nodeSelectedByName' , 100 , connections . main [ 0 ] [ 0 ] . node , false , true ) ;
2019-07-25 09:50:45 -07:00
} else if ( [ 'ArrowUp' , 'ArrowDown' ] . includes ( e . key ) ) {
2019-06-23 03:35:23 -07:00
// Set sibling node as active
// Check first if it has a parent node
const lastSelectedNode = this . $store . getters . lastSelectedNode ;
if ( lastSelectedNode === null ) {
return ;
}
const workflow = this . getWorkflow ( ) ;
if ( ! workflow . connectionsByDestinationNode . hasOwnProperty ( lastSelectedNode . name ) ) {
return ;
}
const connections = workflow . connectionsByDestinationNode [ lastSelectedNode . name ] ;
if ( connections . main === undefined || connections . main . length === 0 ) {
return ;
}
const parentNode = connections . main [ 0 ] [ 0 ] . node ;
const connectionsParent = this . $store . getters . connectionsByNodeName ( parentNode ) ;
if ( connectionsParent . main === undefined || connectionsParent . main . length === 0 ) {
return ;
}
// Get all the sibling nodes and their x positions to know which one to set active
let siblingNode : INodeUi ;
2019-07-25 09:50:45 -07:00
let lastCheckedNodePosition = e . key === 'ArrowUp' ? - 99999999 : 99999999 ;
2019-06-23 03:35:23 -07:00
let nextSelectNode : string | null = null ;
for ( const ouputConnections of connectionsParent . main ) {
for ( const ouputConnection of ouputConnections ) {
if ( ouputConnection . node === lastSelectedNode . name ) {
// Ignore current node
continue ;
}
siblingNode = this . $store . getters . nodeByName ( ouputConnection . node ) ;
2019-07-25 09:50:45 -07:00
if ( e . key === 'ArrowUp' ) {
2019-06-23 03:35:23 -07:00
// Get the next node on the left
2019-07-25 09:50:45 -07:00
if ( siblingNode . position [ 1 ] <= lastSelectedNode . position [ 1 ] && siblingNode . position [ 1 ] > lastCheckedNodePosition ) {
2019-06-23 03:35:23 -07:00
nextSelectNode = siblingNode . name ;
2019-07-25 09:50:45 -07:00
lastCheckedNodePosition = siblingNode . position [ 1 ] ;
2019-06-23 03:35:23 -07:00
}
} else {
// Get the next node on the right
2019-07-25 09:50:45 -07:00
if ( siblingNode . position [ 1 ] >= lastSelectedNode . position [ 1 ] && siblingNode . position [ 1 ] < lastCheckedNodePosition ) {
2019-06-23 03:35:23 -07:00
nextSelectNode = siblingNode . name ;
2019-07-25 09:50:45 -07:00
lastCheckedNodePosition = siblingNode . position [ 1 ] ;
2019-06-23 03:35:23 -07:00
}
}
}
}
if ( nextSelectNode !== null ) {
this . callDebounced ( 'nodeSelectedByName' , 100 , nextSelectNode , false , true ) ;
}
}
} ,
deactivateSelectedNode ( ) {
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
2020-02-09 23:18:44 -08:00
this . disableNodes ( this . $store . getters . getSelectedNodes ) ;
2019-06-23 03:35:23 -07:00
} ,
deleteSelectedNodes ( ) {
// Copy "selectedNodes" as the nodes get deleted out of selection
// when they get deleted and if we would use original it would mess
// with the index and would so not delete all nodes
const nodesToDelete : string [ ] = this . $store . getters . getSelectedNodes . map ( ( node : INodeUi ) => {
return node . name ;
} ) ;
nodesToDelete . forEach ( ( nodeName : string ) => {
this . removeNode ( nodeName ) ;
} ) ;
} ,
selectAllNodes ( ) {
this . nodes . forEach ( ( node ) => {
this . nodeSelectedByName ( node . name ) ;
} ) ;
} ,
2019-07-17 09:44:05 -07:00
selectUpstreamNodes ( ) {
const lastSelectedNode = this . $store . getters . lastSelectedNode as INodeUi | null ;
if ( lastSelectedNode === null ) {
return ;
}
this . deselectAllNodes ( ) ;
// Get all upstream nodes and select them
const workflow = this . getWorkflow ( ) ;
for ( const nodeName of workflow . getParentNodes ( lastSelectedNode . name ) ) {
this . nodeSelectedByName ( nodeName ) ;
}
// At the end select the previously selected node again
this . nodeSelectedByName ( lastSelectedNode . name ) ;
} ,
selectDownstreamNodes ( ) {
const lastSelectedNode = this . $store . getters . lastSelectedNode as INodeUi | null ;
if ( lastSelectedNode === null ) {
return ;
}
this . deselectAllNodes ( ) ;
// Get all downstream nodes and select them
const workflow = this . getWorkflow ( ) ;
for ( const nodeName of workflow . getChildNodes ( lastSelectedNode . name ) ) {
this . nodeSelectedByName ( nodeName ) ;
}
// At the end select the previously selected node again
this . nodeSelectedByName ( lastSelectedNode . name ) ;
} ,
2019-06-23 03:35:23 -07:00
cutSelectedNodes ( ) {
this . copySelectedNodes ( ) ;
this . deleteSelectedNodes ( ) ;
} ,
copySelectedNodes ( ) {
this . getSelectedNodesToSave ( ) . then ( ( data ) => {
const nodeData = JSON . stringify ( data , null , 2 ) ;
this . copyToClipboard ( nodeData ) ;
2021-10-18 20:57:49 -07:00
if ( data . nodes . length > 0 ) {
this . $telemetry . track ( 'User copied nodes' , {
node _types : data . nodes . map ( ( node ) => node . type ) ,
workflow _id : this . $store . getters . workflowId ,
} ) ;
}
2019-06-23 03:35:23 -07:00
} ) ;
} ,
2021-06-23 03:49:34 -07:00
resetZoom ( ) {
const { scale , offset } = scaleReset ( { scale : this . nodeViewScale , offset : this . $store . getters . getNodeViewOffsetPosition } ) ;
this . setZoomLevel ( scale ) ;
this . $store . commit ( 'setNodeViewOffsetPosition' , { newOffset : offset } ) ;
} ,
zoomIn ( ) {
const { scale , offset : [ xOffset , yOffset ] } = scaleBigger ( { scale : this . nodeViewScale , offset : this . $store . getters . getNodeViewOffsetPosition } ) ;
this . setZoomLevel ( scale ) ;
this . $store . commit ( 'setNodeViewOffsetPosition' , { newOffset : [ xOffset , yOffset ] } ) ;
} ,
zoomOut ( ) {
const { scale , offset : [ xOffset , yOffset ] } = scaleSmaller ( { scale : this . nodeViewScale , offset : this . $store . getters . getNodeViewOffsetPosition } ) ;
2021-06-22 10:33:07 -07:00
this . setZoomLevel ( scale ) ;
2021-06-23 03:49:34 -07:00
this . $store . commit ( 'setNodeViewOffsetPosition' , { newOffset : [ xOffset , yOffset ] } ) ;
2021-06-22 10:33:07 -07:00
} ,
2019-06-23 03:35:23 -07:00
2021-06-22 10:33:07 -07:00
setZoomLevel ( zoomLevel : number ) {
this . nodeViewScale = zoomLevel ; // important for background
2019-06-23 03:35:23 -07:00
const element = this . instance . getContainer ( ) as HTMLElement ;
2021-06-22 10:33:07 -07:00
// https://docs.jsplumbtoolkit.com/community/current/articles/zooming.html
2019-06-23 03:35:23 -07:00
const prependProperties = [ 'webkit' , 'moz' , 'ms' , 'o' ] ;
const scaleString = 'scale(' + zoomLevel + ')' ;
for ( let i = 0 ; i < prependProperties . length ; i ++ ) {
// @ts-ignore
element . style [ prependProperties [ i ] + 'Transform' ] = scaleString ;
}
element . style [ 'transform' ] = scaleString ;
// @ts-ignore
this . instance . setZoom ( zoomLevel ) ;
} ,
2021-06-22 10:33:07 -07:00
zoomToFit ( ) {
const nodes = this . $store . getters . allNodes as INodeUi [ ] ;
2021-06-26 23:37:53 -07:00
if ( nodes . length === 0 ) { // some unknown workflow executions
return ;
}
2021-06-22 10:33:07 -07:00
const { minX , minY , maxX , maxY } = getWorkflowCorners ( nodes ) ;
const PADDING = NODE _SIZE * 4 ;
const editorWidth = window . innerWidth ;
const diffX = maxX - minX + SIDEBAR _WIDTH + PADDING ;
const scaleX = editorWidth / diffX ;
const editorHeight = window . innerHeight ;
const diffY = maxY - minY + HEADER _HEIGHT + PADDING ;
const scaleY = editorHeight / diffY ;
const zoomLevel = Math . min ( scaleX , scaleY , 1 ) ;
let xOffset = ( minX * - 1 ) * zoomLevel + SIDEBAR _WIDTH ; // find top right corner
xOffset += ( editorWidth - SIDEBAR _WIDTH - ( maxX - minX + NODE _SIZE ) * zoomLevel ) / 2 ; // add padding to center workflow
let yOffset = ( minY * - 1 ) * zoomLevel + HEADER _HEIGHT ; // find top right corner
yOffset += ( editorHeight - HEADER _HEIGHT - ( maxY - minY + NODE _SIZE * 2 ) * zoomLevel ) / 2 ; // add padding to center workflow
this . setZoomLevel ( zoomLevel ) ;
this . $store . commit ( 'setNodeViewOffsetPosition' , { newOffset : [ xOffset , yOffset ] } ) ;
} ,
2019-06-23 03:35:23 -07:00
async stopExecution ( ) {
const executionId = this . $store . getters . activeExecutionId ;
if ( executionId === null ) {
return ;
}
try {
this . stopExecutionInProgress = true ;
2021-07-23 08:50:47 -07:00
await this . restApi ( ) . stopCurrentExecution ( executionId ) ;
2019-06-23 03:35:23 -07:00
this . $showMessage ( {
title : 'Execution stopped' ,
2021-08-27 08:25:54 -07:00
message : ` The execution with the id " ${ executionId } " was stopped! ` ,
2019-06-23 03:35:23 -07:00
type : 'success' ,
} ) ;
} catch ( error ) {
2021-02-09 14:32:40 -08:00
// Execution stop might fail when the execution has already finished. Let's treat this here.
2021-09-22 00:23:37 -07:00
const execution = await this . restApi ( ) . getExecution ( executionId ) ;
2021-02-09 14:32:40 -08:00
if ( execution . finished ) {
const executedData = {
data : execution . data ,
finished : execution . finished ,
mode : execution . mode ,
startedAt : execution . startedAt ,
stoppedAt : execution . stoppedAt ,
} as IRun ;
const pushData = {
data : executedData ,
2021-02-13 11:46:46 -08:00
executionId ,
2021-02-09 14:32:40 -08:00
retryOf : execution . retryOf ,
} as IPushDataExecutionFinished ;
this . $store . commit ( 'finishActiveExecution' , pushData ) ;
this . $titleSet ( execution . workflowData . name , 'IDLE' ) ;
this . $store . commit ( 'setExecutingNode' , null ) ;
this . $store . commit ( 'setWorkflowExecutionData' , executedData ) ;
this . $store . commit ( 'removeActiveAction' , 'workflowRunning' ) ;
this . $showMessage ( {
title : 'Workflow finished executing' ,
message : 'Unable to stop operation in time. Workflow finished executing already.' ,
type : 'success' ,
} ) ;
} else {
this . $showError ( error , 'Problem stopping execution' , 'There was a problem stopping the execuction:' ) ;
}
2019-06-23 03:35:23 -07:00
}
this . stopExecutionInProgress = false ;
} ,
async stopWaitingForWebhook ( ) {
try {
2021-07-23 08:50:47 -07:00
await this . restApi ( ) . removeTestWebhook ( this . $store . getters . workflowId ) ;
2019-06-23 03:35:23 -07:00
} catch ( error ) {
this . $showError ( error , 'Problem deleting the test-webhook' , 'There was a problem deleting webhook:' ) ;
return ;
}
this . $showMessage ( {
2021-08-27 08:25:54 -07:00
title : 'Webhook deleted' ,
message : ` The webhook was deleted successfully ` ,
2019-06-23 03:35:23 -07:00
type : 'success' ,
} ) ;
} ,
/ * *
* This method gets called when data got pasted into the window
* /
async receivedCopyPasteData ( plainTextData : string ) : Promise < void > {
let workflowData : IWorkflowDataUpdate | undefined ;
// Check if it is an URL which could contain workflow data
if ( plainTextData . match ( /^http[s]?:\/\/.*\.json$/i ) ) {
// Pasted data points to a possible workflow JSON file
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
const importConfirm = await this . confirmMessage ( ` Import workflow from this URL:<br /><i> ${ plainTextData } <i> ` , 'Import Workflow from URL?' , 'warning' , 'Yes, import!' ) ;
if ( importConfirm === false ) {
return ;
}
workflowData = await this . getWorkflowDataFromUrl ( plainTextData ) ;
if ( workflowData === undefined ) {
return ;
}
} else {
// Pasted data is is possible workflow data
try {
// Check first if it is valid JSON
workflowData = JSON . parse ( plainTextData ) ;
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
} catch ( e ) {
// Is no valid JSON so ignore
return ;
}
}
2021-10-18 20:57:49 -07:00
this . $telemetry . track ( 'User pasted nodes' , {
workflow _id : this . $store . getters . workflowId ,
} ) ;
2019-06-23 03:35:23 -07:00
return this . importWorkflowData ( workflowData ! ) ;
} ,
// Returns the workflow data from a given URL. If no data gets found or
// data is invalid it returns undefined and displays an error message by itself.
async getWorkflowDataFromUrl ( url : string ) : Promise < IWorkflowDataUpdate | undefined > {
let workflowData : IWorkflowDataUpdate ;
this . startLoading ( ) ;
try {
workflowData = await this . restApi ( ) . getWorkflowFromUrl ( url ) ;
} catch ( error ) {
this . stopLoading ( ) ;
this . $showError ( error , 'Problem loading workflow' , 'There was a problem loading the workflow data from URL:' ) ;
return ;
}
this . stopLoading ( ) ;
2021-10-18 20:57:49 -07:00
this . $telemetry . track ( 'User imported workflow' , { source : 'url' , workflow _id : this . $store . getters . workflowId } ) ;
2019-06-23 03:35:23 -07:00
return workflowData ;
} ,
// Imports the given workflow data into the current workflow
async importWorkflowData ( workflowData : IWorkflowDataUpdate ) : Promise < void > {
// If it is JSON check if it looks on the first look like data we can use
if (
! workflowData . hasOwnProperty ( 'nodes' ) ||
! workflowData . hasOwnProperty ( 'connections' )
) {
return ;
}
try {
// By default we automatically deselect all the currently
// selected nodes and select the new ones
this . deselectAllNodes ( ) ;
2019-07-17 07:05:01 -07:00
// Fix the node position as it could be totally offscreen
// and the pasted nodes would so not be directly visible to
// the user
this . updateNodePositions ( workflowData , this . getNewNodePosition ( ) ) ;
const data = await this . addNodesToWorkflow ( workflowData ) ;
2019-06-23 03:35:23 -07:00
setTimeout ( ( ) => {
data . nodes ! . forEach ( ( node : INodeUi ) => {
this . nodeSelectedByName ( node . name ) ;
} ) ;
} ) ;
} catch ( error ) {
this . $showError ( error , 'Problem importing workflow' , 'There was a problem importing workflow data:' ) ;
}
} ,
closeNodeCreator ( ) {
this . createNodeActive = false ;
} ,
nodeTypeSelected ( nodeTypeName : string ) {
this . addNodeButton ( nodeTypeName ) ;
this . createNodeActive = false ;
} ,
2019-07-17 10:05:03 -07:00
nodeDeselectedByName ( nodeName : string ) {
const node = this . $store . getters . nodeByName ( nodeName ) ;
if ( node ) {
this . nodeDeselected ( node ) ;
}
} ,
2019-06-23 03:35:23 -07:00
nodeSelectedByName ( nodeName : string , setActive = false , deselectAllOthers ? : boolean ) {
if ( deselectAllOthers === true ) {
this . deselectAllNodes ( ) ;
}
const node = this . $store . getters . nodeByName ( nodeName ) ;
if ( node ) {
this . nodeSelected ( node ) ;
}
this . $store . commit ( 'setLastSelectedNode' , node . name ) ;
2019-12-10 06:39:14 -08:00
this . $store . commit ( 'setLastSelectedNodeOutputIndex' , null ) ;
2019-06-23 03:35:23 -07:00
if ( setActive === true ) {
this . $store . commit ( 'setActiveNode' , node . name ) ;
}
} ,
canUsePosition ( position1 : XYPositon , position2 : XYPositon ) {
if ( Math . abs ( position1 [ 0 ] - position2 [ 0 ] ) <= 100 ) {
if ( Math . abs ( position1 [ 1 ] - position2 [ 1 ] ) <= 50 ) {
return false ;
}
}
return true ;
} ,
getNewNodePosition ( newPosition ? : XYPositon , movePosition ? : XYPositon ) : XYPositon {
// TODO: Lates has to consider also the view position (that it creates the node where it is visible)
// Use the last click position as position for new node
if ( newPosition === undefined ) {
newPosition = this . lastClickPosition ;
}
// @ts-ignore
newPosition = newPosition . slice ( ) ;
if ( ! movePosition ) {
2019-07-31 23:26:43 -07:00
movePosition = [ 50 , 50 ] ;
2019-06-23 03:35:23 -07:00
}
let conflictFound = false ;
let i , node ;
do {
conflictFound = false ;
for ( i = 0 ; i < this . nodes . length ; i ++ ) {
node = this . nodes [ i ] ;
if ( ! this . canUsePosition ( node . position , newPosition ! ) ) {
conflictFound = true ;
break ;
}
}
if ( conflictFound === true ) {
newPosition ! [ 0 ] += movePosition [ 0 ] ;
newPosition ! [ 1 ] += movePosition [ 1 ] ;
}
} while ( conflictFound === true ) ;
return newPosition ! ;
} ,
getUniqueNodeName ( originalName : string , additinalUsedNames ? : string [ ] ) {
2019-07-17 10:58:54 -07:00
// Check if node-name is unique else find one that is
2019-06-23 03:35:23 -07:00
additinalUsedNames = additinalUsedNames || [ ] ;
2019-07-17 10:58:54 -07:00
// Get all the names of the current nodes
2019-06-23 03:35:23 -07:00
const nodeNames = this . $store . getters . allNodes . map ( ( node : INodeUi ) => {
return node . name ;
} ) ;
2019-08-09 09:47:33 -07:00
// Check first if the current name is already unique
if ( ! nodeNames . includes ( originalName ) && ! additinalUsedNames . includes ( originalName ) ) {
return originalName ;
}
2021-02-15 00:55:28 -08:00
const nameMatch = originalName . match ( /(.*\D+)(\d*)/ ) ;
2019-07-17 10:58:54 -07:00
let ignore , baseName , nameIndex , uniqueName ;
2019-06-23 03:35:23 -07:00
let index = 1 ;
2019-07-17 10:58:54 -07:00
if ( nameMatch === null ) {
// Name is only a number
index = parseInt ( originalName , 10 ) ;
baseName = '' ;
uniqueName = baseName + index ;
} else {
// Name is string or string/number combination
[ ignore , baseName , nameIndex ] = nameMatch ;
if ( nameIndex !== '' ) {
index = parseInt ( nameIndex , 10 ) ;
}
uniqueName = baseName ;
}
2019-06-23 03:35:23 -07:00
while (
2019-07-17 10:58:54 -07:00
nodeNames . includes ( uniqueName ) ||
additinalUsedNames . includes ( uniqueName )
2019-06-23 03:35:23 -07:00
) {
2019-07-17 10:58:54 -07:00
uniqueName = baseName + ( index ++ ) ;
2019-06-23 03:35:23 -07:00
}
return uniqueName ;
} ,
showMaxNodeTypeError ( nodeTypeData : INodeTypeDescription ) {
const maxNodes = nodeTypeData . maxNodes ;
this . $showMessage ( {
title : 'Could not create node!' ,
message : ` Node can not be created because in a workflow max. ${ maxNodes } ${ maxNodes === 1 ? 'node' : 'nodes' } of type " ${ nodeTypeData . displayName } " ${ maxNodes === 1 ? 'is' : 'are' } allowed! ` ,
type : 'error' ,
duration : 0 ,
} ) ;
} ,
async addNodeButton ( nodeTypeName : string ) {
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
const nodeTypeData : INodeTypeDescription | null = this . $store . getters . nodeType ( nodeTypeName ) ;
if ( nodeTypeData === null ) {
this . $showMessage ( {
title : 'Could not create node!' ,
message : ` Node of type " ${ nodeTypeName } " could not be created as it is not known. ` ,
type : 'error' ,
} ) ;
return ;
}
if ( nodeTypeData . maxNodes !== undefined && this . getNodeTypeCount ( nodeTypeName ) >= nodeTypeData . maxNodes ) {
this . showMaxNodeTypeError ( nodeTypeData ) ;
return ;
}
const newNodeData : INodeUi = {
name : nodeTypeData . defaults . name as string ,
type : nodeTypeData . name ,
typeVersion : nodeTypeData . version ,
position : [ 0 , 0 ] ,
parameters : { } ,
} ;
// Check if there is a last selected node
const lastSelectedNode = this . $store . getters . lastSelectedNode ;
2019-12-10 06:39:14 -08:00
const lastSelectedNodeOutputIndex = this . $store . getters . lastSelectedNodeOutputIndex ;
2019-06-23 03:35:23 -07:00
if ( lastSelectedNode ) {
// If a node is active then add the new node directly after the current one
// newNodeData.position = [activeNode.position[0], activeNode.position[1] + 60];
newNodeData . position = this . getNewNodePosition (
2020-09-11 07:59:41 -07:00
[ lastSelectedNode . position [ 0 ] + 200 , lastSelectedNode . position [ 1 ] ] ,
2019-12-29 13:02:21 -08:00
[ 100 , 0 ] ,
2019-06-23 03:35:23 -07:00
) ;
} else {
// If no node is active find a free spot
newNodeData . position = this . getNewNodePosition ( ) ;
}
// Check if node-name is unique else find one that is
newNodeData . name = this . getUniqueNodeName ( newNodeData . name ) ;
2020-05-27 16:32:49 -07:00
if ( nodeTypeData . webhooks && nodeTypeData . webhooks . length ) {
2020-09-02 00:16:16 -07:00
newNodeData . webhookId = uuidv4 ( ) ;
2020-05-27 16:32:49 -07:00
}
2019-06-23 03:35:23 -07:00
await this . addNodes ( [ newNodeData ] ) ;
2020-11-04 04:04:40 -08:00
this . $store . commit ( 'setStateDirty' , true ) ;
2021-05-05 17:46:33 -07:00
this . $externalHooks ( ) . run ( 'nodeView.addNodeButton' , { nodeTypeName } ) ;
2021-10-18 20:57:49 -07:00
this . $telemetry . trackNodesPanel ( 'nodeView.addNodeButton' , { node _type : nodeTypeName , workflow _id : this . $store . getters . workflowId } ) ;
2021-05-05 17:46:33 -07:00
2019-06-23 03:35:23 -07:00
// Automatically deselect all nodes and select the current one and also active
// current node
this . deselectAllNodes ( ) ;
setTimeout ( ( ) => {
this . nodeSelectedByName ( newNodeData . name , true ) ;
} ) ;
2019-12-10 06:39:14 -08:00
const outputIndex = lastSelectedNodeOutputIndex || 0 ;
2019-06-23 03:35:23 -07:00
if ( lastSelectedNode ) {
// If a node is last selected then connect between the active and its child ones
await Vue . nextTick ( ) ;
// Add connections of active node to newly created one
let connections = this . $store . getters . connectionsByNodeName (
2019-12-29 13:02:21 -08:00
lastSelectedNode . name ,
2019-06-23 03:35:23 -07:00
) ;
connections = JSON . parse ( JSON . stringify ( connections ) ) ;
for ( const type of Object . keys ( connections ) ) {
2019-12-10 06:39:14 -08:00
if ( outputIndex <= connections [ type ] . length ) {
connections [ type ] [ outputIndex ] . forEach ( ( connectionInfo : IConnection ) => {
2019-06-23 03:35:23 -07:00
// Remove currenct connection
const connectionDataDisonnect = [
{
node : lastSelectedNode . name ,
type ,
2019-12-10 06:39:14 -08:00
index : outputIndex ,
2019-06-23 03:35:23 -07:00
} ,
connectionInfo ,
] as [ IConnection , IConnection ] ;
this . _ _removeConnection ( connectionDataDisonnect , true ) ;
const connectionDataConnect = [
{
node : newNodeData . name ,
type ,
2019-12-10 06:39:14 -08:00
index : 0 ,
2019-06-23 03:35:23 -07:00
} ,
connectionInfo ,
] as [ IConnection , IConnection ] ;
this . _ _addConnection ( connectionDataConnect , true ) ;
} ) ;
}
}
// TODO: Check if new node has input
// TODO: disconnect
// Connect active node to the newly created one
const connectionData = [
{
node : lastSelectedNode . name ,
type : 'main' ,
2019-12-10 06:39:14 -08:00
index : outputIndex ,
2019-06-23 03:35:23 -07:00
} ,
{
node : newNodeData . name ,
type : 'main' ,
index : 0 ,
} ,
] as [ IConnection , IConnection ] ;
this . _ _addConnection ( connectionData , true ) ;
}
} ,
initNodeView ( ) {
2020-05-24 05:06:22 -07:00
const connectionOverlays : OverlaySpec [ ] = [ ] ;
if ( this . isReadOnly === false ) {
connectionOverlays . push . apply ( connectionOverlays , [
2019-06-23 03:35:23 -07:00
[
'Arrow' ,
{
location : 1 ,
foldback : 0.7 ,
width : 12 ,
} ,
] ,
[
'Label' ,
{
id : 'drop-add-node' ,
label : 'Drop connection<br />to create node' ,
cssClass : 'drop-add-node-label' ,
location : 0.5 ,
} ,
] ,
2020-05-24 05:06:22 -07:00
] ) ;
}
this . instance . importDefaults ( {
// notice the 'curviness' argument to this Bezier curve.
// the curves on this page are far smoother
// than the curves on the first demo, which use the default curviness value.
// Connector: ["Bezier", { curviness: 80 }],
Connector : [ 'Bezier' , { curviness : 40 } ] ,
// @ts-ignore
Endpoint : [ 'Dot' , { radius : 5 } ] ,
DragOptions : { cursor : 'pointer' , zIndex : 5000 } ,
PaintStyle : { strokeWidth : 2 , stroke : '#334455' } ,
EndpointStyle : { radius : 9 , fill : '#acd' , stroke : 'red' } ,
// EndpointStyle: {},
HoverPaintStyle : { stroke : '#ff6d5a' , lineWidth : 4 } ,
EndpointHoverStyle : { fill : '#ff6d5a' , stroke : '#acd' } ,
ConnectionOverlays : connectionOverlays ,
2019-06-23 03:35:23 -07:00
Container : '#node-view' ,
} ) ;
this . instance . bind ( 'connectionAborted' , ( info ) => {
// Get the node and set it as active that new nodes
// which get created get automatically connected
// to it.
const sourceNodeName = this . $store . getters . getNodeNameByIndex ( info . sourceId . slice ( NODE _NAME _PREFIX . length ) ) ;
this . $store . commit ( 'setLastSelectedNode' , sourceNodeName ) ;
2019-12-10 06:39:14 -08:00
const sourceInfo = info . getParameters ( ) ;
this . $store . commit ( 'setLastSelectedNodeOutputIndex' , sourceInfo . index ) ;
2019-06-23 03:35:23 -07:00
// Display the node-creator
this . createNodeActive = true ;
2021-10-18 20:57:49 -07:00
this . $externalHooks ( ) . run ( 'nodeView.createNodeActiveChanged' , { source : 'node_connection_drop' , createNodeActive : this . createNodeActive } ) ;
this . $telemetry . trackNodesPanel ( 'nodeView.createNodeActiveChanged' , { source : 'node_connection_drop' , workflow _id : this . $store . getters . workflowId , createNodeActive : this . createNodeActive } ) ;
2019-06-23 03:35:23 -07:00
} ) ;
this . instance . bind ( 'connection' , ( info : OnConnectionBindInfo ) => {
2019-06-27 02:27:02 -07:00
// @ts-ignore
const sourceInfo = info . sourceEndpoint . getParameters ( ) ;
// @ts-ignore
const targetInfo = info . targetEndpoint . getParameters ( ) ;
const sourceNodeName = this . $store . getters . getNodeNameByIndex ( sourceInfo . nodeIndex ) ;
const targetNodeName = this . $store . getters . getNodeNameByIndex ( targetInfo . nodeIndex ) ;
const sourceNode = this . $store . getters . nodeByName ( sourceNodeName ) ;
2019-08-02 06:56:05 -07:00
const targetNode = this . $store . getters . nodeByName ( targetNodeName ) ;
2019-06-27 02:27:02 -07:00
2019-06-23 03:35:23 -07:00
// TODO: That should happen after each move (only the setConnector part)
2019-07-25 09:50:45 -07:00
if ( info . sourceEndpoint . anchor . lastReturnValue [ 0 ] >= info . targetEndpoint . anchor . lastReturnValue [ 0 ] ) {
// When the source is before the target it will make sure that
2019-06-23 03:35:23 -07:00
// the connection is clearer visible
// Use the Flowchart connector if the source is underneath the target
// so that the connection is properly visible
info . connection . setConnector ( [ 'Flowchart' , { cornerRadius : 15 } ] ) ;
// TODO: Location should be dependent on distance. The closer together
// the further away from the center
info . connection . addOverlay ( [
'Arrow' ,
{
location : 0.55 ,
foldback : 0.7 ,
width : 12 ,
} ,
] ) ;
// Change also the color to give an additional visual hint
info . connection . setPaintStyle ( { strokeWidth : 2 , stroke : '#334455' } ) ;
2019-07-25 09:50:45 -07:00
} else if ( Math . abs ( info . sourceEndpoint . anchor . lastReturnValue [ 1 ] - info . targetEndpoint . anchor . lastReturnValue [ 1 ] ) < 30 ) {
2019-06-23 03:35:23 -07:00
info . connection . setConnector ( [ 'Straight' ] ) ;
}
// @ts-ignore
info . connection . removeOverlay ( 'drop-add-node' ) ;
2020-05-24 05:06:22 -07:00
if ( this . isReadOnly === false ) {
// Display the connection-delete button only on hover
let timer : NodeJS . Timeout | undefined ;
info . connection . bind ( 'mouseover' , ( connection : IConnection ) => {
if ( timer !== undefined ) {
clearTimeout ( timer ) ;
}
const overlay = info . connection . getOverlay ( 'remove-connection' ) ;
overlay . setVisible ( true ) ;
} ) ;
info . connection . bind ( 'mouseout' , ( connection : IConnection ) => {
timer = setTimeout ( ( ) => {
const overlay = info . connection . getOverlay ( 'remove-connection' ) ;
overlay . setVisible ( false ) ;
timer = undefined ;
} , 500 ) ;
} ) ;
// @ts-ignore
info . connection . addOverlay ( [
'Label' ,
{
id : 'remove-connection' ,
label : '<span class="delete-connection clickable" title="Delete Connection">x</span>' ,
cssClass : 'remove-connection-label' ,
visible : false ,
events : {
mousedown : ( ) => {
this . _ _removeConnectionByConnectionInfo ( info , true ) ;
} ,
2019-06-23 03:35:23 -07:00
} ,
} ,
2020-05-24 05:06:22 -07:00
] ) ;
}
2019-06-23 03:35:23 -07:00
2019-08-02 06:56:05 -07:00
// Display input names if they exist on connection
const targetNodeTypeData : INodeTypeDescription = this . $store . getters . nodeType ( targetNode . type ) ;
if ( targetNodeTypeData . inputNames !== undefined ) {
for ( const input of targetNodeTypeData . inputNames ) {
const inputName = targetNodeTypeData . inputNames [ targetInfo . index ] ;
if ( info . connection . getOverlay ( 'input-name-label' ) ) {
// Make sure that it does not get added multiple times
// continue;
info . connection . removeOverlay ( 'input-name-label' ) ;
}
// @ts-ignore
info . connection . addOverlay ( [
'Label' ,
{
id : 'input-name-label' ,
label : inputName ,
cssClass : 'connection-input-name-label' ,
location : 0.8 ,
} ,
] ) ;
}
}
2019-06-27 02:27:02 -07:00
// Display output names if they exist on connection
const sourceNodeTypeData : INodeTypeDescription = this . $store . getters . nodeType ( sourceNode . type ) ;
if ( sourceNodeTypeData . outputNames !== undefined ) {
for ( const output of sourceNodeTypeData . outputNames ) {
const outputName = sourceNodeTypeData . outputNames [ sourceInfo . index ] ;
if ( info . connection . getOverlay ( 'output-name-label' ) ) {
// Make sure that it does not get added multiple times
2019-08-02 06:56:05 -07:00
info . connection . removeOverlay ( 'output-name-label' ) ;
2019-06-27 02:27:02 -07:00
}
// @ts-ignore
info . connection . addOverlay ( [
'Label' ,
{
id : 'output-name-label' ,
label : outputName ,
cssClass : 'connection-output-name-label' ,
2019-08-02 06:56:05 -07:00
location : 0.2 ,
2019-06-27 02:27:02 -07:00
} ,
] ) ;
}
}
2019-08-02 06:56:05 -07:00
// When connection gets made the output and input name get displayed
// as overlay on the connection. So the ones on the endpoint can be hidden.
2019-06-23 03:35:23 -07:00
// @ts-ignore
2019-06-27 02:27:02 -07:00
const outputNameOverlay = info . connection . endpoints [ 0 ] . getOverlay ( 'output-name-label' ) ;
2019-06-27 04:00:52 -07:00
if ( ! [ null , undefined ] . includes ( outputNameOverlay ) ) {
2019-06-27 02:27:02 -07:00
outputNameOverlay . setVisible ( false ) ;
}
2019-06-23 03:35:23 -07:00
2019-08-02 06:56:05 -07:00
const inputNameOverlay = info . targetEndpoint . getOverlay ( 'input-name-label' ) ;
if ( ! [ null , undefined ] . includes ( inputNameOverlay ) ) {
inputNameOverlay . setVisible ( false ) ;
}
2019-06-23 03:35:23 -07:00
this . $store . commit ( 'addConnection' , {
connection : [
{
2019-06-27 02:27:02 -07:00
node : sourceNodeName ,
2019-06-23 03:35:23 -07:00
type : sourceInfo . type ,
index : sourceInfo . index ,
} ,
{
2019-06-27 02:27:02 -07:00
node : targetNodeName ,
2019-06-23 03:35:23 -07:00
type : targetInfo . type ,
index : targetInfo . index ,
} ,
] ,
2020-09-02 06:59:20 -07:00
setStateDirty : true ,
2019-06-23 03:35:23 -07:00
} ) ;
} ) ;
2019-08-02 08:06:06 -07:00
const updateConnectionDetach = ( sourceEndpoint : Endpoint , targetEndpoint : Endpoint , maxConnections : number ) => {
2019-08-02 06:56:05 -07:00
// If the source endpoint is not connected to anything else anymore
// display the output-name overlays on the endpoint if any exist
if ( sourceEndpoint !== undefined && sourceEndpoint . connections ! . length === maxConnections ) {
const outputNameOverlay = sourceEndpoint . getOverlay ( 'output-name-label' ) ;
if ( ! [ null , undefined ] . includes ( outputNameOverlay ) ) {
outputNameOverlay . setVisible ( true ) ;
}
}
if ( targetEndpoint !== undefined && targetEndpoint . connections ! . length === maxConnections ) {
const inputNameOverlay = targetEndpoint . getOverlay ( 'input-name-label' ) ;
if ( ! [ null , undefined ] . includes ( inputNameOverlay ) ) {
inputNameOverlay . setVisible ( true ) ;
}
}
2019-08-02 08:06:06 -07:00
} ;
2019-08-02 06:56:05 -07:00
2019-06-23 03:35:23 -07:00
this . instance . bind ( 'connectionMoved' , ( info ) => {
// When a connection gets moved from one node to another it for some reason
// calls the "connection" event but not the "connectionDetached" one. So we listen
// additionally to the "connectionMoved" event and then only delete the existing connection.
2019-08-02 06:56:05 -07:00
updateConnectionDetach ( info . originalSourceEndpoint , info . originalTargetEndpoint , 0 ) ;
2019-06-23 03:35:23 -07:00
// @ts-ignore
const sourceInfo = info . originalSourceEndpoint . getParameters ( ) ;
// @ts-ignore
const targetInfo = info . originalTargetEndpoint . getParameters ( ) ;
const connectionInfo = [
{
node : this . $store . getters . getNodeNameByIndex ( sourceInfo . nodeIndex ) ,
type : sourceInfo . type ,
index : sourceInfo . index ,
} ,
{
node : this . $store . getters . getNodeNameByIndex ( targetInfo . nodeIndex ) ,
type : targetInfo . type ,
index : targetInfo . index ,
} ,
] as [ IConnection , IConnection ] ;
this . _ _removeConnection ( connectionInfo , false ) ;
// Make sure to remove the overlay else after the second move
// it visibly stays behind free floating without a connection.
2019-06-27 02:27:02 -07:00
info . connection . removeOverlays ( ) ;
2019-06-23 03:35:23 -07:00
} ) ;
2019-06-27 02:27:02 -07:00
2019-06-23 03:35:23 -07:00
this . instance . bind ( 'connectionDetached' , ( info ) => {
2019-08-02 06:56:05 -07:00
updateConnectionDetach ( info . sourceEndpoint , info . targetEndpoint , 1 ) ;
2019-06-27 02:27:02 -07:00
info . connection . removeOverlays ( ) ;
2019-06-23 03:35:23 -07:00
this . _ _removeConnectionByConnectionInfo ( info , false ) ;
} ) ;
} ,
async newWorkflow ( ) : Promise < void > {
await this . resetWorkspace ( ) ;
2021-05-29 11:31:21 -07:00
await this . $store . dispatch ( 'workflows/setNewWorkflowName' ) ;
this . $store . commit ( 'setStateDirty' , false ) ;
2019-06-23 03:35:23 -07:00
2021-06-22 10:33:07 -07:00
await this . addNodes ( [ DEFAULT _START _NODE ] ) ;
2020-10-25 04:47:49 -07:00
this . $store . commit ( 'setStateDirty' , false ) ;
2021-06-22 10:33:07 -07:00
this . setZoomLevel ( 1 ) ;
2019-06-23 03:35:23 -07:00
} ,
async initView ( ) : Promise < void > {
if ( this . $route . params . action === 'workflowSave' ) {
// In case the workflow got saved we do not have to run init
// as only the route changed but all the needed data is already loaded
2020-09-01 07:06:08 -07:00
this . $store . commit ( 'setStateDirty' , false ) ;
2019-06-23 03:35:23 -07:00
return Promise . resolve ( ) ;
}
2021-06-22 10:33:07 -07:00
if ( this . blankRedirect ) {
this . blankRedirect = false ;
}
else if ( this . $route . name === 'WorkflowTemplate' ) {
const templateId = this . $route . params . id ;
await this . openWorkflowTemplate ( templateId ) ;
}
else if ( this . $route . name === 'ExecutionById' ) {
2019-06-23 03:35:23 -07:00
// Load an execution
const executionId = this . $route . params . id ;
await this . openExecution ( executionId ) ;
} else {
2020-10-25 04:47:49 -07:00
const result = this . $store . getters . getStateIsDirty ;
if ( result ) {
const importConfirm = await this . confirmMessage ( ` When you switch workflows your current workflow changes will be lost. ` , 'Save your Changes?' , 'warning' , 'Yes, switch workflows and forget changes' ) ;
if ( importConfirm === false ) {
return Promise . resolve ( ) ;
}
}
2019-06-23 03:35:23 -07:00
// Load a workflow
let workflowId = null as string | null ;
if ( this . $route . params . name ) {
workflowId = this . $route . params . name ;
}
if ( workflowId !== null ) {
2020-08-25 11:38:09 -07:00
const workflow = await this . restApi ( ) . getWorkflow ( workflowId ) ;
2021-05-29 11:31:21 -07:00
if ( ! workflow ) {
throw new Error ( 'Could not find workflow' ) ;
}
2020-08-25 11:38:09 -07:00
this . $titleSet ( workflow . name , 'IDLE' ) ;
2019-06-23 03:35:23 -07:00
// Open existing workflow
await this . openWorkflow ( workflowId ) ;
} else {
// Create new workflow
await this . newWorkflow ( ) ;
}
}
document . addEventListener ( 'keydown' , this . keyDown ) ;
document . addEventListener ( 'keyup' , this . keyUp ) ;
2020-07-09 13:54:50 -07:00
2020-07-20 07:57:58 -07:00
window . addEventListener ( "beforeunload" , ( e ) => {
2020-09-01 07:06:08 -07:00
if ( this . $store . getters . getStateIsDirty === true ) {
2020-07-20 07:57:58 -07:00
const confirmationMessage = 'It looks like you have been editing something. '
+ 'If you leave before saving, your changes will be lost.' ;
( e || window . event ) . returnValue = confirmationMessage ; //Gecko + IE
return confirmationMessage ; //Gecko + Webkit, Safari, Chrome etc.
2020-07-20 08:52:24 -07:00
} else {
2021-05-04 08:55:39 -07:00
this . startLoading ( 'Redirecting' ) ;
2020-07-20 08:52:24 -07:00
return ;
2020-07-09 13:54:50 -07:00
}
} ) ;
2019-06-23 03:35:23 -07:00
} ,
_ _addConnection ( connection : [ IConnection , IConnection ] , addVisualConnection = false ) {
if ( addVisualConnection === true ) {
const uuid : [ string , string ] = [
2019-07-25 09:50:45 -07:00
` ${ this . getNodeIndex ( connection [ 0 ] . node ) } -output ${ connection [ 0 ] . index } ` ,
` ${ this . getNodeIndex ( connection [ 1 ] . node ) } -input ${ connection [ 1 ] . index } ` ,
2019-06-23 03:35:23 -07:00
] ;
// Create connections in DOM
// @ts-ignore
this . instance . connect ( {
uuids : uuid ,
2020-05-24 05:06:22 -07:00
detachable : ! this . isReadOnly ,
2019-06-23 03:35:23 -07:00
} ) ;
} else {
2020-09-09 05:28:13 -07:00
const connectionProperties = { connection , setStateDirty : false } ;
2019-06-23 03:35:23 -07:00
// When nodes get connected it gets saved automatically to the storage
// so if we do not connect we have to save the connection manually
2020-09-09 05:28:13 -07:00
this . $store . commit ( 'addConnection' , connectionProperties ) ;
2019-06-23 03:35:23 -07:00
}
} ,
_ _removeConnection ( connection : [ IConnection , IConnection ] , removeVisualConnection = false ) {
if ( removeVisualConnection === true ) {
// @ts-ignore
const connections = this . instance . getConnections ( {
source : NODE _NAME _PREFIX + this . getNodeIndex ( connection [ 0 ] . node ) ,
target : NODE _NAME _PREFIX + this . getNodeIndex ( connection [ 1 ] . node ) ,
} ) ;
// @ts-ignore
connections . forEach ( ( connectionInstance ) => {
this . instance . deleteConnection ( connectionInstance ) ;
} ) ;
}
this . $store . commit ( 'removeConnection' , { connection } ) ;
} ,
_ _removeConnectionByConnectionInfo ( info : OnConnectionBindInfo , removeVisualConnection = false ) {
// @ts-ignore
const sourceInfo = info . sourceEndpoint . getParameters ( ) ;
// @ts-ignore
const targetInfo = info . targetEndpoint . getParameters ( ) ;
const connectionInfo = [
{
node : this . $store . getters . getNodeNameByIndex ( sourceInfo . nodeIndex ) ,
type : sourceInfo . type ,
index : sourceInfo . index ,
} ,
{
node : this . $store . getters . getNodeNameByIndex ( targetInfo . nodeIndex ) ,
type : targetInfo . type ,
index : targetInfo . index ,
} ,
] as [ IConnection , IConnection ] ;
this . _ _removeConnection ( connectionInfo , removeVisualConnection ) ;
} ,
async duplicateNode ( nodeName : string ) {
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
const node = this . $store . getters . nodeByName ( nodeName ) ;
const nodeTypeData : INodeTypeDescription = this . $store . getters . nodeType ( node . type ) ;
if ( nodeTypeData . maxNodes !== undefined && this . getNodeTypeCount ( node . type ) >= nodeTypeData . maxNodes ) {
this . showMaxNodeTypeError ( nodeTypeData ) ;
return ;
}
// Deep copy the data so that data on lower levels of the node-properties do
// not share objects
const newNodeData = JSON . parse ( JSON . stringify ( this . getNodeDataToSave ( node ) ) ) ;
// Check if node-name is unique else find one that is
newNodeData . name = this . getUniqueNodeName ( newNodeData . name ) ;
newNodeData . position = this . getNewNodePosition (
2019-07-25 22:17:40 -07:00
[ node . position [ 0 ] , node . position [ 1 ] + 150 ] ,
2019-12-29 13:02:21 -08:00
[ 0 , 150 ] ,
2019-06-23 03:35:23 -07:00
) ;
2020-10-04 08:22:11 -07:00
if ( newNodeData . webhookId ) {
// Make sure that the node gets a new unique webhook-ID
newNodeData . webhookId = uuidv4 ( ) ;
}
2019-06-23 03:35:23 -07:00
await this . addNodes ( [ newNodeData ] ) ;
2020-11-04 04:04:40 -08:00
this . $store . commit ( 'setStateDirty' , true ) ;
2019-06-23 03:35:23 -07:00
// Automatically deselect all nodes and select the current one and also active
// current node
this . deselectAllNodes ( ) ;
setTimeout ( ( ) => {
this . nodeSelectedByName ( newNodeData . name , true ) ;
} ) ;
2021-10-18 20:57:49 -07:00
this . $telemetry . track ( 'User duplicated node' , { node _type : node . type , workflow _id : this . $store . getters . workflowId } ) ;
2019-06-23 03:35:23 -07:00
} ,
removeNode ( nodeName : string ) {
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
const node = this . $store . getters . nodeByName ( nodeName ) ;
// "requiredNodeTypes" are also defined in cli/commands/run.ts
2021-10-18 20:57:49 -07:00
const requiredNodeTypes = [ START _NODE _TYPE ] ;
2019-06-23 03:35:23 -07:00
if ( requiredNodeTypes . includes ( node . type ) ) {
// The node is of the required type so check first
// if any node of that type would be left when the
// current one would get deleted.
let deleteAllowed = false ;
for ( const checkNode of this . nodes ) {
if ( checkNode . name === node . name ) {
continue ;
}
if ( requiredNodeTypes . includes ( checkNode . type ) ) {
deleteAllowed = true ;
break ;
}
}
if ( deleteAllowed === false ) {
return ;
}
}
const nodeIndex = this . $store . getters . getNodeIndex ( nodeName ) ;
const nodeIdName = ` node- ${ nodeIndex } ` ;
// Suspend drawing
this . instance . setSuspendDrawing ( true ) ;
// Remove all endpoints and the connections in jsplumb
this . instance . removeAllEndpoints ( nodeIdName ) ;
// Remove the draggable
// @ts-ignore
this . instance . destroyDraggable ( nodeIdName ) ;
// Remove the connections in data
this . $store . commit ( 'removeAllNodeConnection' , node ) ;
this . $store . commit ( 'removeNode' , node ) ;
// Now it can draw again
this . instance . setSuspendDrawing ( false , true ) ;
// Remove node from selected index if found in it
this . $store . commit ( 'removeNodeFromSelection' , node ) ;
// Remove from node index
if ( nodeIndex !== - 1 ) {
this . $store . commit ( 'setNodeIndex' , { index : nodeIndex , name : null } ) ;
}
} ,
valueChanged ( parameterData : IUpdateInformation ) {
if ( parameterData . name === 'name' && parameterData . oldValue ) {
// The name changed so we have to take care that
// the connections get changed.
this . renameNode ( parameterData . oldValue as string , parameterData . value as string ) ;
}
} ,
async renameNodePrompt ( currentName : string ) {
try {
2019-07-24 06:21:44 -07:00
const promptResponsePromise = this . $prompt ( 'New Name:' , ` Rename Node: " ${ currentName } " ` , {
customClass : 'rename-prompt' ,
2019-06-23 03:35:23 -07:00
confirmButtonText : 'Rename' ,
cancelButtonText : 'Cancel' ,
inputErrorMessage : 'Invalid Name' ,
inputValue : currentName ,
} ) ;
2019-07-24 06:21:44 -07:00
// Wait till it had time to display
await Vue . nextTick ( ) ;
// Get the input and select the text in it
const nameInput = document . querySelector ( '.rename-prompt .el-input__inner' ) as HTMLInputElement | undefined ;
if ( nameInput ) {
nameInput . focus ( ) ;
nameInput . select ( ) ;
}
2019-12-29 13:02:21 -08:00
const promptResponse = await promptResponsePromise as MessageBoxInputData ;
2019-07-24 06:21:44 -07:00
2019-06-23 03:35:23 -07:00
this . renameNode ( currentName , promptResponse . value ) ;
} catch ( e ) { }
} ,
async renameNode ( currentName : string , newName : string ) {
if ( currentName === newName ) {
return ;
}
// Check if node-name is unique else find one that is
newName = this . getUniqueNodeName ( newName ) ;
// Rename the node and update the connections
2019-08-09 09:47:33 -07:00
const workflow = this . getWorkflow ( undefined , undefined , true ) ;
2019-06-23 03:35:23 -07:00
workflow . renameNode ( currentName , newName ) ;
// Update also last selected node and exeuction data
this . $store . commit ( 'renameNodeSelectedAndExecution' , { old : currentName , new : newName } ) ;
// Reset all nodes and connections to load the new ones
if ( this . instance ) {
// On first load it does not exist
this . instance . deleteEveryEndpoint ( ) ;
}
this . $store . commit ( 'removeAllConnections' ) ;
2020-09-01 07:06:08 -07:00
this . $store . commit ( 'removeAllNodes' , { setStateDirty : true } ) ;
2019-06-23 03:35:23 -07:00
// Wait a tick that the old nodes had time to get removed
await Vue . nextTick ( ) ;
// Add the new updated nodes
await this . addNodes ( Object . values ( workflow . nodes ) , workflow . connectionsBySourceNode ) ;
2019-07-24 06:04:24 -07:00
// Make sure that the node is selected again
this . deselectAllNodes ( ) ;
this . nodeSelectedByName ( newName ) ;
2019-06-23 03:35:23 -07:00
} ,
2021-10-13 15:21:00 -07:00
matchCredentials ( node : INodeUi ) {
if ( ! node . credentials ) {
return ;
}
Object . entries ( node . credentials ) . forEach ( ( [ nodeCredentialType , nodeCredentials ] : [ string , INodeCredentialsDetails ] ) => {
const credentialOptions = this . $store . getters [ 'credentials/getCredentialsByType' ] ( nodeCredentialType ) as ICredentialsResponse [ ] ;
// Check if workflows applies old credentials style
if ( typeof nodeCredentials === 'string' ) {
nodeCredentials = {
id : null ,
name : nodeCredentials ,
} ;
this . credentialsUpdated = true ;
}
if ( nodeCredentials . id ) {
// Check whether the id is matching with a credential
2021-11-04 19:23:10 -07:00
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 ,
) ;
2021-10-13 15:21:00 -07:00
if ( credentialsForId ) {
2021-11-04 19:23:10 -07:00
if ( credentialsForId . name !== nodeCredentials . name || typeof nodeCredentials . id === 'number' ) {
node . credentials ! [ nodeCredentialType ] = { id : credentialsForId . id , name : credentialsForId . name } ;
2021-10-13 15:21:00 -07:00
this . credentialsUpdated = 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 ;
this . credentialsUpdated = true ;
}
} ) ;
} ,
2019-06-23 03:35:23 -07:00
async addNodes ( nodes : INodeUi [ ] , connections ? : IConnections ) {
if ( ! nodes || ! nodes . length ) {
return ;
}
2020-10-22 08:24:35 -07:00
// 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.
2021-09-21 10:38:24 -07:00
await this . loadNodesProperties ( nodes . map ( node => ( { name : node . type , version : node . typeVersion } ) ) ) ;
2020-10-22 08:24:35 -07:00
2019-06-23 03:35:23 -07:00
// Add the node to the node-list
let nodeType : INodeTypeDescription | null ;
let foundNodeIssues : INodeIssues | null ;
nodes . forEach ( ( node ) => {
2021-09-21 10:38:24 -07:00
nodeType = this . $store . getters . nodeType ( node . type , node . typeVersion ) ;
2019-06-23 03:35:23 -07:00
// Make sure that some properties always exist
if ( ! node . hasOwnProperty ( 'disabled' ) ) {
node . disabled = false ;
}
if ( ! node . hasOwnProperty ( 'color' ) ) {
// If no color is defined set the default color of the node type
if ( nodeType && nodeType . defaults . color ) {
node . color = nodeType . defaults . color as string ;
}
}
if ( ! node . hasOwnProperty ( 'parameters' ) ) {
node . parameters = { } ;
}
// Load the defaul parameter values because only values which differ
// from the defaults get saved
if ( nodeType !== null ) {
let nodeParameters = null ;
try {
2019-07-14 05:10:16 -07:00
nodeParameters = NodeHelpers . getNodeParameters ( nodeType . properties , node . parameters , true , false ) ;
2019-06-23 03:35:23 -07:00
} catch ( e ) {
2019-09-20 05:07:02 -07:00
console . error ( ` There was a problem loading the node-parameters of node: " ${ node . name } " ` ) ; // eslint-disable-line no-console
console . error ( e ) ; // eslint-disable-line no-console
2019-06-23 03:35:23 -07:00
}
node . parameters = nodeParameters !== null ? nodeParameters : { } ;
2020-05-30 16:03:58 -07:00
// if it's a webhook and the path is empty set the UUID as the default path
2021-10-18 20:57:49 -07:00
if ( node . type === WEBHOOK _NODE _TYPE && node . parameters . path === '' ) {
2020-06-10 07:17:16 -07:00
node . parameters . path = node . webhookId as string ;
2020-05-30 16:03:58 -07:00
}
2019-06-23 03:35:23 -07:00
}
2021-10-13 15:21:00 -07:00
// check and match credentials, apply new format if old is used
this . matchCredentials ( node ) ;
2019-06-23 03:35:23 -07:00
foundNodeIssues = this . getNodeIssues ( nodeType , node ) ;
if ( foundNodeIssues !== null ) {
node . issues = foundNodeIssues ;
}
this . $store . commit ( 'addNode' , node ) ;
} ) ;
// Wait for the node to be rendered
await Vue . nextTick ( ) ;
// Suspend drawing
this . instance . setSuspendDrawing ( true ) ;
// Load the connections
if ( connections !== undefined ) {
let connectionData ;
for ( const sourceNode of Object . keys ( connections ) ) {
for ( const type of Object . keys ( connections [ sourceNode ] ) ) {
for ( let sourceIndex = 0 ; sourceIndex < connections [ sourceNode ] [ type ] . length ; sourceIndex ++ ) {
connections [ sourceNode ] [ type ] [ sourceIndex ] . forEach ( (
2019-12-29 13:02:21 -08:00
targetData ,
2019-06-23 03:35:23 -07:00
) => {
connectionData = [
{
node : sourceNode ,
type ,
index : sourceIndex ,
} ,
{
node : targetData . node ,
type : targetData . type ,
index : targetData . index ,
} ,
] as [ IConnection , IConnection ] ;
this . _ _addConnection ( connectionData , true ) ;
} ) ;
}
}
}
}
// Now it can draw again
this . instance . setSuspendDrawing ( false , true ) ;
} ,
async addNodesToWorkflow ( data : IWorkflowDataUpdate ) : Promise < IWorkflowDataUpdate > {
// Because nodes with the same name maybe already exist, it could
// be needed that they have to be renamed. Also could it be possible
// that nodes are not allowd to be created because they have a create
// limit set. So we would then link the new nodes with the already existing ones.
// In this object all that nodes get saved in the format:
// old-name -> new-name
const nodeNameTable : {
[ key : string ] : string ;
} = { } ;
const newNodeNames : string [ ] = [ ] ;
if ( ! data . nodes ) {
// No nodes to add
throw new Error ( 'No nodes given to add!' ) ;
}
// Get how many of the nodes of the types which have
// a max limit set already exist
const nodeTypesCount = this . getNodeTypesMaxCount ( ) ;
2019-08-09 09:47:33 -07:00
let oldName : string ;
let newName : string ;
2019-06-23 03:35:23 -07:00
const createNodes : INode [ ] = [ ] ;
2020-10-22 08:24:35 -07:00
2021-09-21 10:38:24 -07:00
await this . loadNodesProperties ( data . nodes . map ( node => ( { name : node . type , version : node . typeVersion } ) ) ) ;
2020-10-23 04:44:34 -07:00
2019-06-23 03:35:23 -07:00
data . nodes . forEach ( node => {
if ( nodeTypesCount [ node . type ] !== undefined ) {
if ( nodeTypesCount [ node . type ] . exist >= nodeTypesCount [ node . type ] . max ) {
// Node is not allowed to be created so
// do not add it to the create list but
// add the name of the existing node
// that this one gets linked up instead.
nodeNameTable [ node . name ] = nodeTypesCount [ node . type ] . nodeNames [ 0 ] ;
return ;
} else {
// Node can be created but increment the
// counter in case multiple ones are
// supposed to be created
nodeTypesCount [ node . type ] . exist += 1 ;
}
}
oldName = node . name ;
2019-08-09 09:47:33 -07:00
newName = this . getUniqueNodeName ( node . name , newNodeNames ) ;
2019-06-23 03:35:23 -07:00
2019-08-09 09:47:33 -07:00
newNodeNames . push ( newName ) ;
nodeNameTable [ oldName ] = newName ;
2019-06-23 03:35:23 -07:00
createNodes . push ( node ) ;
} ) ;
2019-08-09 09:47:33 -07:00
// Get only the connections of the nodes that get created
const newConnections : IConnections = { } ;
const currentConnections = data . connections ! ;
const createNodeNames = createNodes . map ( ( node ) => node . name ) ;
let sourceNode , type , sourceIndex , connectionIndex , connectionData ;
for ( sourceNode of Object . keys ( currentConnections ) ) {
if ( ! createNodeNames . includes ( sourceNode ) ) {
// Node does not get created so skip output connections
continue ;
2019-06-23 03:35:23 -07:00
}
2019-08-09 09:47:33 -07:00
const connection : INodeConnections = { } ;
for ( type of Object . keys ( currentConnections [ sourceNode ] ) ) {
connection [ type ] = [ ] ;
for ( sourceIndex = 0 ; sourceIndex < currentConnections [ sourceNode ] [ type ] . length ; sourceIndex ++ ) {
const nodeSourceConnections = [ ] ;
2021-05-26 06:34:20 -07:00
if ( currentConnections [ sourceNode ] [ type ] [ sourceIndex ] ) {
for ( connectionIndex = 0 ; connectionIndex < currentConnections [ sourceNode ] [ type ] [ sourceIndex ] . length ; connectionIndex ++ ) {
connectionData = currentConnections [ sourceNode ] [ type ] [ sourceIndex ] [ connectionIndex ] ;
if ( ! createNodeNames . includes ( connectionData . node ) ) {
// Node does not get created so skip input connection
continue ;
}
nodeSourceConnections . push ( connectionData ) ;
// Add connection
2019-06-23 03:35:23 -07:00
}
}
2019-08-09 09:47:33 -07:00
connection [ type ] . push ( nodeSourceConnections ) ;
2019-06-23 03:35:23 -07:00
}
}
2019-08-09 09:47:33 -07:00
newConnections [ sourceNode ] = connection ;
2019-06-23 03:35:23 -07:00
}
2019-08-09 09:47:33 -07:00
// Create a workflow with the new nodes and connections that we can use
// the rename method
const tempWorkflow : Workflow = this . getWorkflow ( createNodes , newConnections ) ;
2019-06-23 03:35:23 -07:00
2019-08-09 09:47:33 -07:00
// Rename all the nodes of which the name changed
for ( oldName in nodeNameTable ) {
if ( oldName === nodeNameTable [ oldName ] ) {
// Name did not change so skip
continue ;
}
tempWorkflow . renameNode ( oldName , nodeNameTable [ oldName ] ) ;
}
// Add the nodes with the changed node names, expressions and connections
await this . addNodes ( Object . values ( tempWorkflow . nodes ) , tempWorkflow . connectionsBySourceNode ) ;
2020-11-04 04:04:40 -08:00
this . $store . commit ( 'setStateDirty' , true ) ;
2019-08-09 09:47:33 -07:00
return {
nodes : Object . values ( tempWorkflow . nodes ) ,
connections : tempWorkflow . connectionsBySourceNode ,
} ;
2019-06-23 03:35:23 -07:00
} ,
getSelectedNodesToSave ( ) : Promise < IWorkflowData > {
const data : IWorkflowData = {
nodes : [ ] ,
connections : { } ,
} ;
// Get data of all the selected noes
let nodeData ;
const exportNodeNames : string [ ] = [ ] ;
for ( const node of this . $store . getters . getSelectedNodes ) {
try {
nodeData = this . getNodeDataToSave ( node ) ;
exportNodeNames . push ( node . name ) ;
} catch ( e ) {
return Promise . reject ( e ) ;
}
data . nodes . push ( nodeData ) ;
}
// Get only connections of exported nodes and ignore all other ones
let connectionToKeep ,
connections : INodeConnections ,
type : string ,
connectionIndex : number ,
sourceIndex : number ,
connectionData : IConnection ,
typeConnections : INodeConnections ;
data . nodes . forEach ( ( node ) => {
connections = this . $store . getters . connectionsByNodeName ( node . name ) ;
if ( Object . keys ( connections ) . length === 0 ) {
return ;
}
// Keep only the connection to node which get also exported
// @ts-ignore
typeConnections = { } ;
for ( type of Object . keys ( connections ) ) {
for ( sourceIndex = 0 ; sourceIndex < connections [ type ] . length ; sourceIndex ++ ) {
connectionToKeep = [ ] ;
for ( connectionIndex = 0 ; connectionIndex < connections [ type ] [ sourceIndex ] . length ; connectionIndex ++ ) {
connectionData = connections [ type ] [ sourceIndex ] [ connectionIndex ] ;
if ( exportNodeNames . indexOf ( connectionData . node ) !== - 1 ) {
connectionToKeep . push ( connectionData ) ;
}
}
if ( connectionToKeep . length ) {
if ( ! typeConnections . hasOwnProperty ( type ) ) {
typeConnections [ type ] = [ ] ;
}
typeConnections [ type ] [ sourceIndex ] = connectionToKeep ;
}
}
}
if ( Object . keys ( typeConnections ) . length ) {
data . connections [ node . name ] = typeConnections ;
}
} ) ;
return Promise . resolve ( data ) ;
} ,
resetWorkspace ( ) {
// Reset nodes
if ( this . instance ) {
// On first load it does not exist
this . instance . deleteEveryEndpoint ( ) ;
}
if ( this . executionWaitingForWebhook === true ) {
// Make sure that if there is a waiting test-webhook that
// it gets removed
this . restApi ( ) . removeTestWebhook ( this . $store . getters . workflowId )
. catch ( ( ) => {
// Ignore all errors
} ) ;
}
2020-09-01 07:06:08 -07:00
this . $store . commit ( 'removeAllConnections' , { setStateDirty : false } ) ;
this . $store . commit ( 'removeAllNodes' , { setStateDirty : false } ) ;
2019-06-23 03:35:23 -07:00
// Reset workflow execution data
this . $store . commit ( 'setWorkflowExecutionData' , null ) ;
this . $store . commit ( 'resetAllNodesIssues' ) ;
// vm.$forceUpdate();
this . $store . commit ( 'setActive' , false ) ;
this . $store . commit ( 'setWorkflowId' , PLACEHOLDER _EMPTY _WORKFLOW _ID ) ;
2020-09-09 05:28:13 -07:00
this . $store . commit ( 'setWorkflowName' , { newName : '' , setStateDirty : false } ) ;
2019-06-23 03:35:23 -07:00
this . $store . commit ( 'setWorkflowSettings' , { } ) ;
2021-05-29 11:31:21 -07:00
this . $store . commit ( 'setWorkflowTagIds' , [ ] ) ;
2019-06-23 03:35:23 -07:00
this . $store . commit ( 'setActiveExecutionId' , null ) ;
this . $store . commit ( 'setExecutingNode' , null ) ;
this . $store . commit ( 'removeActiveAction' , 'workflowRunning' ) ;
this . $store . commit ( 'setExecutionWaitingForWebhook' , false ) ;
this . $store . commit ( 'resetNodeIndex' ) ;
this . $store . commit ( 'resetSelectedNodes' ) ;
2020-09-09 05:28:13 -07:00
this . $store . commit ( 'setNodeViewOffsetPosition' , { newOffset : [ 0 , 0 ] , setStateDirty : false } ) ;
2019-06-23 03:35:23 -07:00
return Promise . resolve ( ) ;
} ,
async loadActiveWorkflows ( ) : Promise < void > {
const activeWorkflows = await this . restApi ( ) . getActiveWorkflows ( ) ;
this . $store . commit ( 'setActiveWorkflows' , activeWorkflows ) ;
} ,
async loadSettings ( ) : Promise < void > {
2021-10-18 20:57:49 -07:00
await this . $store . dispatch ( 'settings/getSettings' ) ;
2019-06-23 03:35:23 -07:00
} ,
async loadNodeTypes ( ) : Promise < void > {
const nodeTypes = await this . restApi ( ) . getNodeTypes ( ) ;
this . $store . commit ( 'setNodeTypes' , nodeTypes ) ;
} ,
async loadCredentialTypes ( ) : Promise < void > {
2021-09-11 01:15:36 -07:00
await this . $store . dispatch ( 'credentials/fetchCredentialTypes' ) ;
2019-06-23 03:35:23 -07:00
} ,
async loadCredentials ( ) : Promise < void > {
2021-09-11 01:15:36 -07:00
await this . $store . dispatch ( 'credentials/fetchAllCredentials' ) ;
2019-06-23 03:35:23 -07:00
} ,
2021-09-21 10:38:24 -07:00
async loadNodesProperties ( nodeInfos : INodeTypeNameVersion [ ] ) : Promise < void > {
const allNodes : INodeTypeDescription [ ] = this . $store . getters . allNodeTypes ;
const nodesToBeFetched : INodeTypeNameVersion [ ] = [ ] ;
allNodes . forEach ( node => {
if ( ! ! nodeInfos . find ( n => n . name === node . name && n . version === node . version ) && ! node . hasOwnProperty ( 'properties' ) ) {
nodesToBeFetched . push ( {
name : node . name ,
version : node . version ,
} ) ;
}
} ) ;
2020-10-22 08:24:35 -07:00
if ( nodesToBeFetched . length > 0 ) {
// Only call API if node information is actually missing
this . startLoading ( ) ;
const nodeInfo = await this . restApi ( ) . getNodesInformation ( nodesToBeFetched ) ;
this . $store . commit ( 'updateNodeTypes' , nodeInfo ) ;
this . stopLoading ( ) ;
}
} ,
2019-06-23 03:35:23 -07:00
} ,
2021-07-22 01:22:17 -07:00
2019-06-23 03:35:23 -07:00
async mounted ( ) {
this . $root . $on ( 'importWorkflowData' , async ( data : IDataObject ) => {
2021-07-23 08:50:47 -07:00
await this . importWorkflowData ( data . data as IWorkflowDataUpdate ) ;
2019-06-23 03:35:23 -07:00
} ) ;
2021-06-04 14:13:42 -07:00
this . $root . $on ( 'newWorkflow' , this . newWorkflow ) ;
2019-06-23 03:35:23 -07:00
this . $root . $on ( 'importWorkflowUrl' , async ( data : IDataObject ) => {
const workflowData = await this . getWorkflowDataFromUrl ( data . url as string ) ;
if ( workflowData !== undefined ) {
2021-07-23 08:50:47 -07:00
await this . importWorkflowData ( workflowData ) ;
2019-06-23 03:35:23 -07:00
}
} ) ;
this . startLoading ( ) ;
const loadPromises = [
this . loadActiveWorkflows ( ) ,
this . loadCredentials ( ) ,
this . loadCredentialTypes ( ) ,
this . loadNodeTypes ( ) ,
this . loadSettings ( ) ,
] ;
try {
await Promise . all ( loadPromises ) ;
} catch ( error ) {
this . $showError ( error , 'Init Problem' , 'There was a problem loading init data:' ) ;
return ;
}
this . instance . ready ( async ( ) => {
try {
this . initNodeView ( ) ;
await this . initView ( ) ;
} catch ( error ) {
this . $showError ( error , 'Init Problem' , 'There was a problem initializing the workflow:' ) ;
}
this . stopLoading ( ) ;
2021-07-22 01:22:17 -07:00
setTimeout ( ( ) => {
this . checkForNewVersions ( ) ;
} , 0 ) ;
2019-06-23 03:35:23 -07:00
} ) ;
2021-01-19 14:48:30 -08:00
this . $externalHooks ( ) . run ( 'nodeView.mount' ) ;
2019-06-23 03:35:23 -07:00
} ,
destroyed ( ) {
this . resetWorkspace ( ) ;
} ,
} ) ;
< / script >
< style scoped lang = "scss" >
. zoom - menu {
2021-05-29 11:31:21 -07:00
$ -- zoom - menu - margin : 5 ;
2019-06-23 03:35:23 -07:00
position : fixed ;
2021-05-29 11:31:21 -07:00
left : $ -- sidebar - width + $ -- zoom - menu - margin ;
2019-06-23 03:35:23 -07:00
width : 200 px ;
bottom : 45 px ;
line - height : 25 px ;
z - index : 18 ;
color : # 444 ;
padding - right : 5 px ;
2021-05-29 11:31:21 -07:00
2021-10-05 11:33:25 -07:00
@ media ( max - width : $ -- breakpoint - 2 xs ) {
bottom : 90 px ;
}
2021-05-29 11:31:21 -07:00
& . expanded {
left : $ -- sidebar - expanded - width + $ -- zoom - menu - margin ;
}
2019-06-23 03:35:23 -07:00
}
. node - creator - button {
position : fixed ;
text - align : center ;
top : 80 px ;
right : 20 px ;
z - index : 10 ;
}
. node - creator - button button {
position : relative ;
}
. node - view - root {
position : absolute ;
width : 100 % ;
height : 100 % ;
left : 0 ;
top : 0 ;
overflow : hidden ;
}
. node - view - wrapper {
position : fixed ;
width : 100 % ;
height : 100 % ;
}
. node - view {
position : relative ;
width : 100 % ;
height : 100 % ;
2021-02-28 09:08:14 -08:00
transform - origin : 0 0 ;
2019-06-23 03:35:23 -07:00
}
. node - view - background {
position : absolute ;
width : 10000 px ;
height : 10000 px ;
top : - 5000 px ;
left : - 5000 px ;
background - size : 50 px 50 px ;
background - image : linear - gradient ( to right , # eeeefe 1 px , transparent 1 px ) , linear - gradient ( to bottom , # eeeefe 1 px , transparent 1 px ) ;
}
. move - active {
cursor : grab ;
cursor : - moz - grab ;
cursor : - webkit - grab ;
touch - action : none ;
}
. move - in - process {
cursor : grabbing ;
cursor : - moz - grabbing ;
cursor : - webkit - grabbing ;
touch - action : none ;
}
. workflow - execute - wrapper {
position : fixed ;
line - height : 65 px ;
left : calc ( 50 % - 150 px ) ;
bottom : 30 px ;
width : 300 px ;
text - align : center ;
2021-08-29 04:36:17 -07:00
> * {
margin - inline - end : 0.625 rem ;
2019-06-23 03:35:23 -07:00
}
}
/* Makes sure that when selected with mouse it does not select text */
. do - not - select * ,
. jtk - drag - select * {
- webkit - touch - callout : none ;
- webkit - user - select : none ;
- khtml - user - select : none ;
- moz - user - select : none ;
- ms - user - select : none ;
user - select : none ;
}
< / style >
< style lang = "scss" >
2019-08-02 06:56:05 -07:00
. connection - input - name - label ,
2019-06-27 02:27:02 -07:00
. connection - output - name - label {
2019-08-10 00:59:15 -07:00
border - radius : 7 px ;
background - color : rgba ( $ -- custom - node - view - background , 0.8 ) ;
2019-08-02 06:56:05 -07:00
font - size : 0.7 em ;
2019-08-10 00:59:15 -07:00
line - height : 1.3 em ;
padding : 2 px 3 px ;
white - space : nowrap ;
2019-06-27 02:27:02 -07:00
}
2019-07-25 12:57:27 -07:00
. delete - connection {
font - weight : 500 ;
}
2019-06-23 03:35:23 -07:00
. remove - connection - label {
font - size : 12 px ;
color : # fff ;
line - height : 13 px ;
border - radius : 15 px ;
height : 15 px ;
background - color : # 334455 ;
position : relative ;
height : 15 px ;
width : 15 px ;
text - align : center ;
& : hover {
background - color : $ -- color - primary ;
font - size : 20 px ;
line - height : 17 px ;
height : 20 px ;
width : 20 px ;
}
}
. drop - add - node - label {
color : # 555 ;
font - weight : 600 ;
font - size : 0.8 em ;
text - align : center ;
background - color : # ffffff55 ;
}
2019-08-02 06:56:05 -07:00
. node - input - endpoint - label ,
. node - output - endpoint - label {
2019-06-27 02:27:02 -07:00
background - color : $ -- custom - node - view - background ;
2019-08-10 00:59:15 -07:00
border - radius : 7 px ;
font - size : 0.7 em ;
padding : 2 px ;
white - space : nowrap ;
2019-06-23 03:35:23 -07:00
}
2019-08-02 06:56:05 -07:00
. node - input - endpoint - label {
text - align : right ;
}
2019-06-23 03:35:23 -07:00
. button - white {
border : none ;
padding : 0.3 em ;
margin : 0 0.1 em ;
border - radius : 3 px ;
font - size : 1.2 em ;
background : # fff ;
width : 40 px ;
height : 40 px ;
color : # 666 ;
cursor : pointer ;
& : hover {
transform : scale ( 1.1 ) ;
}
}
< / style >