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"
2021-11-19 01:17:13 -08:00
@ moved = "onNodeMoved"
@ run = "onNodeRun"
2019-06-23 03:35:23 -07:00
: 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"
2021-11-19 01:17:13 -08:00
: hideActions = "pullConnActive"
2019-06-23 03:35:23 -07:00
> < / node >
< / div >
< / div >
< DataDisplay @valueChanged ="valueChanged" / >
2021-11-19 01:17:13 -08:00
< div v-if ="!createNodeActive && !isReadOnly" class="node-creator-button" title="Add Node" @click="() => openNodeCreator('add_node_button')" >
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 {
2021-12-03 09:53:55 -08:00
Connection , Endpoint , N8nPlusEndpoint ,
2020-05-24 05:06:22 -07:00
} from 'jsplumb' ;
2019-12-29 13:02:21 -08:00
import { MessageBoxInputData } from 'element-ui/types/message-box' ;
2021-11-19 01:17:13 -08:00
import { jsPlumb , OnConnectionBindInfo } from 'jsplumb' ;
2021-12-03 09:53:55 -08:00
import { NODE _NAME _PREFIX , NODE _OUTPUT _DEFAULT _KEY , 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-11-19 01:17:13 -08:00
import * as CanvasHelpers from './canvasHelpers' ;
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 ,
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-11-19 01:17:13 -08:00
ITaskData ,
2021-10-13 15:21:00 -07:00
INodeCredentialsDetails ,
2019-06-23 03:35:23 -07:00
} from 'n8n-workflow' ;
import {
2021-10-13 15:21:00 -07:00
ICredentialsResponse ,
2019-06-23 03:35:23 -07:00
IExecutionResponse ,
IWorkflowDb ,
IWorkflowData ,
INodeUi ,
IUpdateInformation ,
IWorkflowDataUpdate ,
2021-11-19 01:17:13 -08:00
XYPosition ,
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-19 01:17:13 -08:00
import '../plugins/N8nCustomConnectorType' ;
2021-12-03 09:53:55 -08:00
import '../plugins/PlusEndpointType' ;
2021-06-22 10:33:07 -07:00
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
} ,
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' ,
] ) ,
2019-06-23 03:35:23 -07:00
activeNode ( ) : INodeUi | null {
return this . $store . getters . activeNode ;
} ,
executionWaitingForWebhook ( ) : boolean {
return this . $store . getters . executionWaitingForWebhook ;
} ,
2021-11-19 01:17:13 -08:00
lastSelectedNode ( ) : INodeUi | null {
2019-06-23 03:35:23 -07:00
return this . $store . getters . lastSelectedNode ;
} ,
nodes ( ) : INodeUi [ ] {
return this . $store . getters . allNodes ;
} ,
runButtonText ( ) : string {
if ( this . workflowRunning === false ) {
return 'Execute Workflow' ;
}
if ( this . executionWaitingForWebhook === true ) {
:sparkles: Improve Waiting Webhook call state in WF Canvas (#2430)
* N8N-2586 Improve Waiting Webhook call state in WF Canvas
* N8N-2586 Added watcher for showing Webhook's Node Tooltip on execution
* N8N-2586 Show helping tooltip for trigger node if wokrflow is running, it is a trigger node, if it is only one trigger node in WF
* N8N-2586 Rework/Move logic to computed property, Created getter for ActveTriggerNodesInWokrflow, Add style to trigger node's tooltip, remove comments
* N8N-2586 Added EventTriggerDescription prop in INodeTypeDescription Interface, Updated Logic for TriggerNode Tooltip based on the new prop
* N8N-2586 Add new use cases/watcher to show Trigger Nodes Tooltip / If has issues, if paused, if wokrlfow is running, Refactor Getter
* N8N-2586 Added z-index to tooltip, Added new Scenario for Tooltip if it is Draged&Droped on the WF
* N8N-2586 Refactor computed property for draged nodes
* N8N-2586 Fixed Conflicts
* N8N-2586 Fixed Tooltip
* N8N-2586 Dont show tooltip on core trigger nodes that execute automatically
* N8N-2586 Fixed Webhook tooltip when adding/deleting canvas during WF execution
* N8N-2586 Updated Logic, Simplify the code
* N8N-2586 Simplify Code
* N8N-2586 Added check for nodetype
* update dragging to use local state
* N8N-2586 Added eventTriggerDescription to Interval Node
* add comment, use new getter
* update to always check
Co-authored-by: Mutasem <mutdmour@gmail.com>
2021-11-25 14:33:41 -08:00
return 'Waiting for Trigger Event' ;
2019-06-23 03:35:23 -07:00
}
return 'Executing Workflow' ;
} ,
workflowStyle ( ) : object {
const offsetPosition = this . $store . getters . getNodeViewOffsetPosition ;
return {
left : offsetPosition [ 0 ] + 'px' ,
top : offsetPosition [ 1 ] + 'px' ,
} ;
} ,
backgroundStyle ( ) : object {
2021-11-19 01:17:13 -08:00
return CanvasHelpers . getBackgroundStyles ( this . nodeViewScale , this . $store . getters . getNodeViewOffsetPosition ) ;
2019-06-23 03:35:23 -07:00
} ,
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 ( ) ,
2021-11-19 01:17:13 -08:00
lastSelectedConnection : null as null | Connection ,
lastClickPosition : [ 450 , 450 ] as XYPosition ,
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 ,
2021-11-19 01:17:13 -08:00
newNodeInsertPosition : null as XYPosition | null ,
pullConnActiveNodeName : null as string | null ,
pullConnActive : false ,
dropPrevented : 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 ( ) ;
} ,
2021-11-19 01:17:13 -08:00
openNodeCreator ( source : string ) {
2019-06-23 03:35:23 -07:00
this . createNodeActive = true ;
2021-11-19 01:17:13 -08:00
this . $externalHooks ( ) . run ( 'nodeView.createNodeActiveChanged' , { source , createNodeActive : this . createNodeActive } ) ;
this . $telemetry . trackNodesPanel ( 'nodeView.createNodeActiveChanged' , { source , 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
2021-12-03 09:53:55 -08:00
if ( data . finished !== true && data && data . data && data . data . resultData && data . data . resultData . error ) {
2021-07-10 02:34:41 -07:00
// 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 ) ;
2021-11-19 01:17:13 -08:00
const leftmostTop = CanvasHelpers . getLeftmostTopNode ( nodes ) ;
2021-06-22 10:33:07 -07:00
2021-11-19 01:17:13 -08:00
const diffX = CanvasHelpers . DEFAULT _START _POSITION _X - leftmostTop . position [ 0 ] ;
const diffY = CanvasHelpers . DEFAULT _START _POSITION _Y - leftmostTop . position [ 1 ] ;
2021-06-22 10:33:07 -07:00
data . workflow . nodes . map ( ( node ) => {
2021-11-19 01:17:13 -08:00
node . position [ 0 ] += diffX + ( hasStartNode ? 0 : CanvasHelpers . NODE _SIZE * 2 ) ;
2021-06-22 10:33:07 -07:00
node . position [ 1 ] += diffY ;
} ) ;
if ( ! hasStartNode ) {
2021-11-19 01:17:13 -08:00
data . workflow . nodes . push ( { ... CanvasHelpers . DEFAULT _START _NODE } ) ;
2021-06-22 10:33:07 -07:00
}
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
2021-11-19 01:17:13 -08:00
this . lastClickPosition = this . getMousePositionWithinNodeView ( e ) ;
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
2021-11-19 01:17:13 -08:00
const lastSelectedNode = this . lastSelectedNode ;
2019-06-23 03:35:23 -07:00
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
2021-11-19 01:17:13 -08:00
const lastSelectedNode = this . lastSelectedNode ;
2019-06-23 03:35:23 -07:00
if ( lastSelectedNode === null ) {
return ;
}
2021-11-19 01:17:13 -08:00
const connections = this . $store . getters . outgoingConnectionsByNodeName ( lastSelectedNode . name ) ;
2019-06-23 03:35:23 -07:00
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
2021-11-19 01:17:13 -08:00
const lastSelectedNode = this . lastSelectedNode ;
2019-06-23 03:35:23 -07:00
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
2021-11-19 01:17:13 -08:00
const lastSelectedNode = this . lastSelectedNode ;
2019-06-23 03:35:23 -07:00
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 ;
2021-11-19 01:17:13 -08:00
const connectionsParent = this . $store . getters . outgoingConnectionsByNodeName ( parentNode ) ;
2019-06-23 03:35:23 -07:00
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 ;
}
2021-11-19 01:17:13 -08:00
siblingNode = this . $store . getters . getNodeByName ( ouputConnection . node ) ;
2019-06-23 03:35:23 -07:00
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 ( ) {
2021-11-19 01:17:13 -08:00
const lastSelectedNode = this . lastSelectedNode ;
2019-07-17 09:44:05 -07:00
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 ( ) {
2021-11-19 01:17:13 -08:00
const lastSelectedNode = this . lastSelectedNode ;
2019-07-17 09:44:05 -07:00
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 ) ;
} ,
2021-11-19 01:17:13 -08:00
pushDownstreamNodes ( sourceNodeName : string , margin : number ) {
const sourceNode = this . $store . getters . nodesByName [ sourceNodeName ] ;
const workflow = this . getWorkflow ( ) ;
const childNodes = workflow . getChildNodes ( sourceNodeName ) ;
for ( const nodeName of childNodes ) {
const node = this . $store . getters . nodesByName [ nodeName ] as INodeUi ;
if ( node . position [ 0 ] < sourceNode . position [ 0 ] ) {
continue ;
}
const updateInformation = {
name : nodeName ,
properties : {
position : [ node . position [ 0 ] + margin , node . position [ 1 ] ] ,
} ,
} ;
this . $store . commit ( 'updateNodeProperties' , updateInformation ) ;
this . onNodeMoved ( node ) ;
}
} ,
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 ( ) {
2021-11-19 01:17:13 -08:00
const { scale , offset } = CanvasHelpers . scaleReset ( { scale : this . nodeViewScale , offset : this . $store . getters . getNodeViewOffsetPosition } ) ;
2021-06-23 03:49:34 -07:00
this . setZoomLevel ( scale ) ;
this . $store . commit ( 'setNodeViewOffsetPosition' , { newOffset : offset } ) ;
} ,
zoomIn ( ) {
2021-11-19 01:17:13 -08:00
const { scale , offset : [ xOffset , yOffset ] } = CanvasHelpers . scaleBigger ( { scale : this . nodeViewScale , offset : this . $store . getters . getNodeViewOffsetPosition } ) ;
2021-06-23 03:49:34 -07:00
this . setZoomLevel ( scale ) ;
this . $store . commit ( 'setNodeViewOffsetPosition' , { newOffset : [ xOffset , yOffset ] } ) ;
} ,
zoomOut ( ) {
2021-11-19 01:17:13 -08:00
const { scale , offset : [ xOffset , yOffset ] } = CanvasHelpers . scaleSmaller ( { scale : this . nodeViewScale , offset : this . $store . getters . getNodeViewOffsetPosition } ) ;
2021-06-23 03:49:34 -07:00
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-11-19 01:17:13 -08:00
const { zoomLevel , offset } = CanvasHelpers . getZoomToFit ( nodes ) ;
2021-06-22 10:33:07 -07:00
this . setZoomLevel ( zoomLevel ) ;
2021-11-19 01:17:13 -08:00
this . $store . commit ( 'setNodeViewOffsetPosition' , { newOffset : offset } ) ;
2021-06-22 10:33:07 -07:00
} ,
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
2021-11-19 01:17:13 -08:00
this . updateNodePositions ( workflowData , CanvasHelpers . getNewNodePosition ( this . nodes , this . lastClickPosition ) ) ;
2019-07-17 07:05:01 -07:00
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 ) {
2021-11-19 01:17:13 -08:00
const node = this . $store . getters . getNodeByName ( nodeName ) ;
2019-07-17 10:05:03 -07:00
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 ( ) ;
}
2021-11-19 01:17:13 -08:00
const node = this . $store . getters . getNodeByName ( nodeName ) ;
2019-06-23 03:35:23 -07:00
if ( node ) {
this . nodeSelected ( node ) ;
}
this . $store . commit ( 'setLastSelectedNode' , node . name ) ;
2019-12-10 06:39:14 -08:00
this . $store . commit ( 'setLastSelectedNodeOutputIndex' , null ) ;
2021-11-19 01:17:13 -08:00
this . lastSelectedConnection = null ;
this . newNodeInsertPosition = null ;
2019-06-23 03:35:23 -07:00
if ( setActive === true ) {
this . $store . commit ( 'setActiveNode' , node . name ) ;
}
} ,
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 ,
} ) ;
} ,
2021-11-19 01:17:13 -08:00
async injectNode ( nodeTypeName : string ) {
2019-06-23 03:35:23 -07:00
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 : { } ,
} ;
2021-11-19 01:17:13 -08:00
// when pulling new connection from node or injecting into a connection
const lastSelectedNode = this . lastSelectedNode ;
2019-06-23 03:35:23 -07:00
if ( lastSelectedNode ) {
2021-11-19 01:17:13 -08:00
const lastSelectedConnection = this . lastSelectedConnection ;
if ( lastSelectedConnection ) { // set when injecting into a connection
const [ diffX ] = CanvasHelpers . getConnectorLengths ( lastSelectedConnection ) ;
if ( diffX <= CanvasHelpers . MAX _X _TO _PUSH _DOWNSTREAM _NODES ) {
this . pushDownstreamNodes ( lastSelectedNode . name , CanvasHelpers . PUSH _NODES _OFFSET ) ;
}
}
// set when pulling connections
if ( this . newNodeInsertPosition ) {
newNodeData . position = CanvasHelpers . getNewNodePosition ( this . nodes , [ this . newNodeInsertPosition [ 0 ] + CanvasHelpers . GRID _SIZE , this . newNodeInsertPosition [ 1 ] - CanvasHelpers . NODE _SIZE / 2 ] ) ;
this . newNodeInsertPosition = null ;
}
else {
let yOffset = 0 ;
if ( lastSelectedConnection ) {
const sourceNodeType = this . $store . getters . nodeType ( lastSelectedNode . type ) as INodeTypeDescription | null ;
const offsets = [ [ - 100 , 100 ] , [ - 140 , 0 , 140 ] , [ - 240 , - 100 , 100 , 240 ] ] ;
if ( sourceNodeType && sourceNodeType . outputs . length > 1 ) {
const offset = offsets [ sourceNodeType . outputs . length - 2 ] ;
const sourceOutputIndex = lastSelectedConnection . _ _meta ? lastSelectedConnection . _ _meta . sourceOutputIndex : 0 ;
yOffset = offset [ sourceOutputIndex ] ;
}
}
// 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 = CanvasHelpers . getNewNodePosition (
this . nodes ,
[ lastSelectedNode . position [ 0 ] + CanvasHelpers . PUSH _NODES _OFFSET , lastSelectedNode . position [ 1 ] + yOffset ] ,
[ 100 , 0 ] ,
) ;
}
2019-06-23 03:35:23 -07:00
} else {
// If no node is active find a free spot
2021-11-19 01:17:13 -08:00
newNodeData . position = CanvasHelpers . getNewNodePosition ( this . nodes , this . lastClickPosition ) ;
2019-06-23 03:35:23 -07:00
}
// Check if node-name is unique else find one that is
2021-11-19 01:17:13 -08:00
newNodeData . name = CanvasHelpers . getUniqueNodeName ( this . $store . getters . allNodes , newNodeData . name ) ;
2019-06-23 03:35:23 -07:00
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 ) ;
} ) ;
2021-11-19 01:17:13 -08:00
return newNodeData ;
} ,
getConnection ( sourceNodeName : string , sourceNodeOutputIndex : number , targetNodeName : string , targetNodeOuputIndex : number ) : IConnection | undefined {
const nodeConnections = ( this . $store . getters . outgoingConnectionsByNodeName ( sourceNodeName ) as INodeConnections ) . main ;
if ( nodeConnections ) {
const connections : IConnection [ ] | null = nodeConnections [ sourceNodeOutputIndex ] ;
if ( connections ) {
return connections . find ( ( connection : IConnection ) => connection . node === targetNodeName && connection . index === targetNodeOuputIndex ) ;
}
}
return undefined ;
} ,
connectTwoNodes ( sourceNodeName : string , sourceNodeOutputIndex : number , targetNodeName : string , targetNodeOuputIndex : number ) {
if ( this . getConnection ( sourceNodeName , sourceNodeOutputIndex , targetNodeName , targetNodeOuputIndex ) ) {
return ;
}
const connectionData = [
{
node : sourceNodeName ,
type : 'main' ,
index : sourceNodeOutputIndex ,
} ,
{
node : targetNodeName ,
type : 'main' ,
index : targetNodeOuputIndex ,
} ,
] as [ IConnection , IConnection ] ;
this . _ _addConnection ( connectionData , true ) ;
} ,
async addNodeButton ( nodeTypeName : string ) {
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
const lastSelectedConnection = this . lastSelectedConnection ;
const lastSelectedNode = this . lastSelectedNode ;
const lastSelectedNodeOutputIndex = this . $store . getters . lastSelectedNodeOutputIndex ;
const newNodeData = await this . injectNode ( nodeTypeName ) ;
if ( ! newNodeData ) {
return ;
}
2019-12-10 06:39:14 -08:00
const outputIndex = lastSelectedNodeOutputIndex || 0 ;
2021-11-19 01:17:13 -08:00
// If a node is last selected then connect between the active and its child ones
2019-06-23 03:35:23 -07:00
if ( lastSelectedNode ) {
await Vue . nextTick ( ) ;
2021-11-19 01:17:13 -08:00
if ( lastSelectedConnection && lastSelectedConnection . _ _meta ) {
this . _ _deleteJSPlumbConnection ( lastSelectedConnection ) ;
const targetNodeName = lastSelectedConnection . _ _meta . targetNodeName ;
const targetOutputIndex = lastSelectedConnection . _ _meta . targetOutputIndex ;
this . connectTwoNodes ( newNodeData . name , 0 , targetNodeName , targetOutputIndex ) ;
2019-06-23 03:35:23 -07:00
}
// Connect active node to the newly created one
2021-11-19 01:17:13 -08:00
this . connectTwoNodes ( lastSelectedNode . name , outputIndex , newNodeData . name , 0 ) ;
2019-06-23 03:35:23 -07:00
}
} ,
initNodeView ( ) {
2020-05-24 05:06:22 -07:00
this . instance . importDefaults ( {
2021-11-19 01:17:13 -08:00
Connector : CanvasHelpers . CONNECTOR _FLOWCHART _TYPE ,
2020-05-24 05:06:22 -07:00
Endpoint : [ 'Dot' , { radius : 5 } ] ,
DragOptions : { cursor : 'pointer' , zIndex : 5000 } ,
2021-11-19 01:17:13 -08:00
PaintStyle : CanvasHelpers . CONNECTOR _PAINT _STYLE _DEFAULT ,
HoverPaintStyle : CanvasHelpers . CONNECTOR _PAINT _STYLE _PRIMARY ,
ConnectionOverlays : CanvasHelpers . CONNECTOR _ARROW _OVERLAYS ,
2019-06-23 03:35:23 -07:00
Container : '#node-view' ,
} ) ;
2021-11-19 01:17:13 -08:00
const insertNodeAfterSelected = ( info : { sourceId : string , index : number , eventSource : string , connection ? : Connection } ) => {
2019-06-23 03:35:23 -07:00
// 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 ) ;
2021-11-19 01:17:13 -08:00
this . $store . commit ( 'setLastSelectedNodeOutputIndex' , info . index ) ;
this . newNodeInsertPosition = null ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
if ( info . connection ) {
this . lastSelectedConnection = info . connection ;
}
2019-12-10 06:39:14 -08:00
2021-11-19 01:17:13 -08:00
this . openNodeCreator ( info . eventSource ) ;
} ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
this . instance . bind ( 'connectionAborted' , ( connection ) => {
try {
if ( this . dropPrevented ) {
this . dropPrevented = false ;
return ;
}
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
if ( this . pullConnActiveNodeName ) {
const sourceNodeName = this . $store . getters . getNodeNameByIndex ( connection . sourceId . slice ( NODE _NAME _PREFIX . length ) ) ;
const outputIndex = connection . getParameters ( ) . index ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
this . connectTwoNodes ( sourceNodeName , outputIndex , this . pullConnActiveNodeName , 0 ) ;
this . pullConnActiveNodeName = null ;
return ;
}
insertNodeAfterSelected ( {
sourceId : connection . sourceId ,
index : connection . getParameters ( ) . index ,
eventSource : 'node_connection_drop' ,
2020-05-24 05:06:22 -07:00
} ) ;
2021-11-19 01:17:13 -08:00
} catch ( e ) {
console . error ( e ) ; // eslint-disable-line no-console
}
} ) ;
2020-05-24 05:06:22 -07:00
2021-11-19 01:17:13 -08:00
this . instance . bind ( 'beforeDrop' , ( info ) => {
try {
const sourceInfo = info . connection . endpoints [ 0 ] . getParameters ( ) ;
2020-05-24 05:06:22 -07:00
// @ts-ignore
2021-11-19 01:17:13 -08:00
const targetInfo = info . dropEndpoint . getParameters ( ) ;
const sourceNodeName = this . $store . getters . getNodeNameByIndex ( sourceInfo . nodeIndex ) ;
const targetNodeName = this . $store . getters . getNodeNameByIndex ( targetInfo . nodeIndex ) ;
// check for duplicates
if ( this . getConnection ( sourceNodeName , sourceInfo . index , targetNodeName , targetInfo . index ) ) {
this . dropPrevented = true ;
this . pullConnActiveNodeName = null ;
return false ;
}
return true ;
} catch ( e ) {
console . error ( e ) ; // eslint-disable-line no-console
return true ;
2020-05-24 05:06:22 -07:00
}
2021-11-19 01:17:13 -08:00
} ) ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
// only one set of visible actions should be visible at the same time
let activeConnection : null | Connection = null ;
2019-08-02 06:56:05 -07:00
2021-11-19 01:17:13 -08:00
this . instance . bind ( 'connection' , ( info : OnConnectionBindInfo ) => {
try {
const sourceInfo = info . sourceEndpoint . getParameters ( ) ;
const targetInfo = info . targetEndpoint . getParameters ( ) ;
2019-08-02 06:56:05 -07:00
2021-11-19 01:17:13 -08:00
const sourceNodeName = this . $store . getters . getNodeNameByIndex ( sourceInfo . nodeIndex ) ;
const targetNodeName = this . $store . getters . getNodeNameByIndex ( targetInfo . nodeIndex ) ;
info . connection . _ _meta = {
sourceNodeName ,
sourceOutputIndex : sourceInfo . index ,
targetNodeName ,
targetOutputIndex : targetInfo . index ,
} ;
CanvasHelpers . resetConnection ( info . connection ) ;
if ( this . isReadOnly === false ) {
let exitTimer : NodeJS . Timeout | undefined ;
let enterTimer : NodeJS . Timeout | undefined ;
info . connection . bind ( 'mouseover' , ( connection : Connection ) => {
try {
if ( exitTimer !== undefined ) {
clearTimeout ( exitTimer ) ;
exitTimer = undefined ;
}
if ( enterTimer ) {
return ;
}
if ( ! info . connection || info . connection === activeConnection ) {
return ;
}
CanvasHelpers . hideConnectionActions ( activeConnection ) ;
enterTimer = setTimeout ( ( ) => {
enterTimer = undefined ;
if ( info . connection ) {
activeConnection = info . connection ;
CanvasHelpers . showConectionActions ( info . connection ) ;
}
} , 150 ) ;
} catch ( e ) {
console . error ( e ) ; // eslint-disable-line no-console
}
} ) ;
info . connection . bind ( 'mouseout' , ( connection : Connection ) => {
try {
if ( exitTimer ) {
return ;
}
if ( enterTimer ) {
clearTimeout ( enterTimer ) ;
enterTimer = undefined ;
}
if ( ! info . connection || activeConnection !== info . connection ) {
return ;
}
exitTimer = setTimeout ( ( ) => {
exitTimer = undefined ;
if ( info . connection && activeConnection === info . connection ) {
CanvasHelpers . hideConnectionActions ( activeConnection ) ;
activeConnection = null ;
}
} , 500 ) ;
} catch ( e ) {
console . error ( e ) ; // eslint-disable-line no-console
}
} ) ;
CanvasHelpers . addConnectionActionsOverlay ( info . connection ,
( ) => {
activeConnection = null ;
this . _ _deleteJSPlumbConnection ( info . connection ) ;
2019-08-02 06:56:05 -07:00
} ,
2021-11-19 01:17:13 -08:00
( ) => {
setTimeout ( ( ) => {
insertNodeAfterSelected ( {
sourceId : info . sourceId ,
index : sourceInfo . index ,
connection : info . connection ,
eventSource : 'node_connection_action' ,
} ) ;
} , 150 ) ;
} ) ;
2019-08-02 06:56:05 -07:00
}
2021-11-19 01:17:13 -08:00
CanvasHelpers . moveBackInputLabelPosition ( info . targetEndpoint ) ;
2019-06-27 02:27:02 -07:00
2021-11-19 01:17:13 -08:00
this . $store . commit ( 'addConnection' , {
connection : [
2019-06-27 02:27:02 -07:00
{
2021-11-19 01:17:13 -08:00
node : sourceNodeName ,
type : sourceInfo . type ,
index : sourceInfo . index ,
2019-06-27 02:27:02 -07:00
} ,
2021-11-19 01:17:13 -08:00
{
node : targetNodeName ,
type : targetInfo . type ,
index : targetInfo . index ,
} ,
] ,
setStateDirty : true ,
} ) ;
} catch ( e ) {
console . error ( e ) ; // eslint-disable-line no-console
2019-06-27 02:27:02 -07:00
}
2021-11-19 01:17:13 -08:00
} ) ;
2019-06-27 02:27:02 -07:00
2021-11-19 01:17:13 -08:00
this . instance . bind ( 'connectionMoved' , ( info ) => {
try {
// 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-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
CanvasHelpers . resetInputLabelPosition ( info . originalTargetEndpoint ) ;
// @ts-ignore
const sourceInfo = info . originalSourceEndpoint . getParameters ( ) ;
// @ts-ignore
const targetInfo = info . originalTargetEndpoint . getParameters ( ) ;
const connectionInfo = [
2019-06-23 03:35:23 -07:00
{
2021-11-19 01:17:13 -08:00
node : this . $store . getters . getNodeNameByIndex ( sourceInfo . nodeIndex ) ,
2019-06-23 03:35:23 -07:00
type : sourceInfo . type ,
index : sourceInfo . index ,
} ,
{
2021-11-19 01:17:13 -08:00
node : this . $store . getters . getNodeNameByIndex ( targetInfo . nodeIndex ) ,
2019-06-23 03:35:23 -07:00
type : targetInfo . type ,
index : targetInfo . index ,
} ,
2021-11-19 01:17:13 -08:00
] as [ IConnection , IConnection ] ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
this . _ _removeConnection ( connectionInfo , false ) ;
} catch ( e ) {
console . error ( e ) ; // eslint-disable-line no-console
2019-08-02 06:56:05 -07:00
}
2021-11-19 01:17:13 -08:00
} ) ;
this . instance . bind ( 'connectionDetached' , ( info ) => {
try {
CanvasHelpers . resetInputLabelPosition ( info . targetEndpoint ) ;
info . connection . removeOverlays ( ) ;
this . _ _removeConnectionByConnectionInfo ( info , false ) ;
if ( this . pullConnActiveNodeName ) { // establish new connection when dragging connection from one node to another
const sourceNodeName = this . $store . getters . getNodeNameByIndex ( info . connection . sourceId . slice ( NODE _NAME _PREFIX . length ) ) ;
const outputIndex = info . connection . getParameters ( ) . index ;
this . connectTwoNodes ( sourceNodeName , outputIndex , this . pullConnActiveNodeName , 0 ) ;
this . pullConnActiveNodeName = null ;
2019-08-02 06:56:05 -07:00
}
2021-11-19 01:17:13 -08:00
} catch ( e ) {
console . error ( e ) ; // eslint-disable-line no-console
2019-08-02 06:56:05 -07:00
}
2021-11-19 01:17:13 -08:00
} ) ;
2019-08-02 06:56:05 -07:00
2021-11-19 01:17:13 -08:00
// @ts-ignore
this . instance . bind ( 'connectionDrag' , ( connection : Connection ) => {
try {
this . pullConnActiveNodeName = null ;
this . pullConnActive = true ;
this . newNodeInsertPosition = null ;
CanvasHelpers . resetConnection ( connection ) ;
const nodes = [ ... document . querySelectorAll ( '.node-default' ) ] ;
2021-11-25 09:41:49 -08:00
const onMouseMove = ( e : MouseEvent | TouchEvent ) => {
2021-11-19 01:17:13 -08:00
if ( ! connection ) {
return ;
}
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
const element = document . querySelector ( '.jtk-endpoint.dropHover' ) ;
if ( element ) {
// @ts-ignore
CanvasHelpers . showDropConnectionState ( connection , element . _jsPlumb ) ;
return ;
}
2019-08-02 06:56:05 -07:00
2021-11-19 01:17:13 -08:00
const inputMargin = 24 ;
const intersecting = nodes . find ( ( element : Element ) => {
const { top , left , right , bottom } = element . getBoundingClientRect ( ) ;
2021-11-25 09:41:49 -08:00
const [ x , y ] = CanvasHelpers . getMousePosition ( e ) ;
if ( top <= y && bottom >= y && ( left - inputMargin ) <= x && right >= x ) {
2021-11-19 01:17:13 -08:00
const nodeName = ( element as HTMLElement ) . dataset [ 'name' ] as string ;
const node = this . $store . getters . getNodeByName ( nodeName ) as INodeUi | null ;
if ( node ) {
const nodeType = this . $store . getters . nodeType ( node . type ) as INodeTypeDescription | null ;
if ( nodeType && nodeType . inputs && nodeType . inputs . length === 1 ) {
this . pullConnActiveNodeName = node . name ;
const endpoint = this . instance . getEndpoint ( this . getInputEndpointUUID ( nodeName , 0 ) ) ;
CanvasHelpers . showDropConnectionState ( connection , endpoint ) ;
return true ;
}
}
}
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
return false ;
} ) ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
if ( ! intersecting ) {
CanvasHelpers . showPullConnectionState ( connection ) ;
this . pullConnActiveNodeName = null ;
}
} ;
2019-06-23 03:35:23 -07:00
2021-11-25 09:41:49 -08:00
const onMouseUp = ( e : MouseEvent | TouchEvent ) => {
2021-11-19 01:17:13 -08:00
this . pullConnActive = false ;
this . newNodeInsertPosition = this . getMousePositionWithinNodeView ( e ) ;
CanvasHelpers . resetConnectionAfterPull ( connection ) ;
window . removeEventListener ( 'mousemove' , onMouseMove ) ;
window . removeEventListener ( 'mouseup' , onMouseUp ) ;
} ;
2019-06-27 02:27:02 -07:00
2021-11-19 01:17:13 -08:00
window . addEventListener ( 'mousemove' , onMouseMove ) ;
2021-11-25 09:41:49 -08:00
window . addEventListener ( 'touchmove' , onMouseMove ) ;
2021-11-19 01:17:13 -08:00
window . addEventListener ( 'mouseup' , onMouseUp ) ;
2021-11-25 09:41:49 -08:00
window . addEventListener ( 'touchend' , onMouseMove ) ;
2021-11-19 01:17:13 -08:00
} catch ( e ) {
console . error ( e ) ; // eslint-disable-line no-console
}
2019-06-23 03:35:23 -07:00
} ) ;
2021-12-03 09:53:55 -08:00
// @ts-ignore
this . instance . bind ( ( 'plusEndpointClick' ) , ( endpoint : Endpoint ) => {
if ( endpoint && endpoint . _ _meta ) {
insertNodeAfterSelected ( {
sourceId : endpoint . _ _meta . nodeId ,
index : endpoint . _ _meta . index ,
eventSource : 'plus_endpoint' ,
} ) ;
}
} ) ;
2019-06-23 03:35:23 -07:00
} ,
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-11-19 01:17:13 -08:00
await this . addNodes ( [ { ... CanvasHelpers . DEFAULT _START _NODE } ] ) ;
this . nodeSelectedByName ( CanvasHelpers . DEFAULT _START _NODE . name , false ) ;
2021-11-15 08:31:00 -08:00
2020-10-25 04:47:49 -07:00
this . $store . commit ( 'setStateDirty' , false ) ;
2021-06-22 10:33:07 -07:00
this . setZoomLevel ( 1 ) ;
2021-11-19 01:17:13 -08:00
setTimeout ( ( ) => {
this . $store . commit ( 'setNodeViewOffsetPosition' , { newOffset : [ 0 , 0 ] } ) ;
} , 0 ) ;
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 ) {
2021-12-03 06:37:04 -08:00
this . $router . push ( {
name : "NodeViewNew" ,
} ) ;
this . $showMessage ( {
title : 'Error' ,
message : 'Could not find workflow' ,
type : 'error' ,
} ) ;
} else {
this . $titleSet ( workflow . name , 'IDLE' ) ;
// Open existing workflow
await this . openWorkflow ( workflowId ) ;
2021-05-29 11:31:21 -07:00
}
2019-06-23 03:35:23 -07:00
} 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
} ,
2021-11-19 01:17:13 -08:00
getOutputEndpointUUID ( nodeName : string , index : number ) {
return CanvasHelpers . getOutputEndpointUUID ( this . getNodeIndex ( nodeName ) , index ) ;
} ,
getInputEndpointUUID ( nodeName : string , index : number ) {
return CanvasHelpers . getInputEndpointUUID ( this . getNodeIndex ( nodeName ) , index ) ;
} ,
2019-06-23 03:35:23 -07:00
_ _addConnection ( connection : [ IConnection , IConnection ] , addVisualConnection = false ) {
if ( addVisualConnection === true ) {
const uuid : [ string , string ] = [
2021-11-19 01:17:13 -08:00
this . getOutputEndpointUUID ( connection [ 0 ] . node , connection [ 0 ] . index ) ,
this . getInputEndpointUUID ( connection [ 1 ] . node , 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 ) => {
2021-11-19 01:17:13 -08:00
this . _ _deleteJSPlumbConnection ( connectionInstance ) ;
2019-06-23 03:35:23 -07:00
} ) ;
}
this . $store . commit ( 'removeConnection' , { connection } ) ;
} ,
2021-11-19 01:17:13 -08:00
_ _deleteJSPlumbConnection ( connection : Connection ) {
// Make sure to remove the overlay else after the second move
// it visibly stays behind free floating without a connection.
connection . removeOverlays ( ) ;
2021-12-03 09:53:55 -08:00
const sourceEndpoint = connection . endpoints && connection . endpoints [ 0 ] ;
2021-11-19 01:17:13 -08:00
this . pullConnActiveNodeName = null ; // prevent new connections when connectionDetached is triggered
this . instance . deleteConnection ( connection ) ; // on delete, triggers connectionDetached event which applies mutation to store
2021-12-03 09:53:55 -08:00
if ( sourceEndpoint ) {
const endpoints = this . instance . getEndpoints ( sourceEndpoint . elementId ) ;
endpoints . forEach ( ( endpoint : Endpoint ) => endpoint . repaint ( ) ) ; // repaint both circle and plus endpoint
}
2021-11-19 01:17:13 -08:00
} ,
2019-06-23 03:35:23 -07:00
_ _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 ] ;
2021-11-19 01:17:13 -08:00
if ( removeVisualConnection ) {
this . _ _deleteJSPlumbConnection ( info . connection ) ;
}
this . $store . commit ( 'removeConnection' , { connection : connectionInfo } ) ;
2019-06-23 03:35:23 -07:00
} ,
async duplicateNode ( nodeName : string ) {
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
2021-11-19 01:17:13 -08:00
const node = this . $store . getters . getNodeByName ( nodeName ) ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
const nodeTypeData : INodeTypeDescription | null = this . $store . getters . nodeType ( node . type ) ;
if ( nodeTypeData && nodeTypeData . maxNodes !== undefined && this . getNodeTypeCount ( node . type ) >= nodeTypeData . maxNodes ) {
2019-06-23 03:35:23 -07:00
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
2021-11-19 01:17:13 -08:00
newNodeData . name = CanvasHelpers . getUniqueNodeName ( this . $store . getters . allNodes , newNodeData . name ) ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
newNodeData . position = CanvasHelpers . getNewNodePosition (
this . nodes ,
[ node . position [ 0 ] , node . position [ 1 ] + 140 ] ,
[ 0 , 140 ] ,
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
} ,
2021-11-19 01:17:13 -08:00
getJSPlumbConnection ( sourceNodeName : string , sourceOutputIndex : number , targetNodeName : string , targetInputIndex : number ) : Connection | undefined {
const sourceIndex = this . getNodeIndex ( sourceNodeName ) ;
const sourceId = ` ${ NODE _NAME _PREFIX } ${ sourceIndex } ` ;
const targetIndex = this . getNodeIndex ( targetNodeName ) ;
const targetId = ` ${ NODE _NAME _PREFIX } ${ targetIndex } ` ;
const sourceEndpoint = CanvasHelpers . getOutputEndpointUUID ( sourceIndex , sourceOutputIndex ) ;
const targetEndpoint = CanvasHelpers . getInputEndpointUUID ( targetIndex , targetInputIndex ) ;
// @ts-ignore
const connections = this . instance . getConnections ( {
source : sourceId ,
target : targetId ,
} ) as Connection [ ] ;
return connections . find ( ( connection : Connection ) => {
const uuids = connection . getUuids ( ) ;
return uuids [ 0 ] === sourceEndpoint && uuids [ 1 ] === targetEndpoint ;
} ) ;
} ,
2021-12-03 09:53:55 -08:00
getJSPlumbEndpoints ( nodeName : string ) : Endpoint [ ] {
const nodeIndex = this . getNodeIndex ( nodeName ) ;
const nodeId = ` ${ NODE _NAME _PREFIX } ${ nodeIndex } ` ;
return this . instance . getEndpoints ( nodeId ) ;
} ,
getPlusEndpoint ( nodeName : string , outputIndex : number ) : Endpoint | undefined {
const endpoints = this . getJSPlumbEndpoints ( nodeName ) ;
// @ts-ignore
return endpoints . find ( ( endpoint : Endpoint ) => endpoint . type === 'N8nPlus' && endpoint . _ _meta && endpoint . _ _meta . index === outputIndex ) ;
} ,
2021-11-19 01:17:13 -08:00
getIncomingOutgoingConnections ( nodeName : string ) : { incoming : Connection [ ] , outgoing : Connection [ ] } {
const name = ` ${ NODE _NAME _PREFIX } ${ this . $store . getters . getNodeIndex ( nodeName ) } ` ;
// @ts-ignore
const outgoing = this . instance . getConnections ( {
source : name ,
} ) as Connection [ ] ;
// @ts-ignore
const incoming = this . instance . getConnections ( {
target : name ,
} ) as Connection [ ] ;
return {
incoming ,
outgoing ,
} ;
} ,
onNodeMoved ( node : INodeUi ) {
const { incoming , outgoing } = this . getIncomingOutgoingConnections ( node . name ) ;
[ ... incoming , ... outgoing ] . forEach ( ( connection : Connection ) => {
CanvasHelpers . showOrHideMidpointArrow ( connection ) ;
CanvasHelpers . showOrHideItemsLabel ( connection ) ;
} ) ;
} ,
onNodeRun ( { name , data , waiting } : { name : string , data : ITaskData [ ] | null , waiting : boolean } ) {
const sourceNodeName = name ;
const sourceIndex = this . $store . getters . getNodeIndex ( sourceNodeName ) ;
const sourceId = ` ${ NODE _NAME _PREFIX } ${ sourceIndex } ` ;
if ( data === null || data . length === 0 || waiting ) {
// @ts-ignore
const outgoing = this . instance . getConnections ( {
source : sourceId ,
} ) as Connection [ ] ;
outgoing . forEach ( ( connection : Connection ) => {
CanvasHelpers . resetConnection ( connection ) ;
} ) ;
2021-12-03 09:53:55 -08:00
const endpoints = this . getJSPlumbEndpoints ( sourceNodeName ) ;
endpoints . forEach ( ( endpoint : Endpoint ) => {
// @ts-ignore
if ( endpoint . type === 'N8nPlus' ) {
( endpoint . endpoint as N8nPlusEndpoint ) . clearSuccessOutput ( ) ;
}
} ) ;
2021-11-19 01:17:13 -08:00
return ;
}
const nodeConnections = ( this . $store . getters . outgoingConnectionsByNodeName ( sourceNodeName ) as INodeConnections ) . main ;
2021-12-03 09:53:55 -08:00
const outputMap = CanvasHelpers . getOutputSummary ( data , nodeConnections || [ ] ) ;
2021-11-19 01:17:13 -08:00
Object . keys ( outputMap ) . forEach ( ( sourceOutputIndex : string ) => {
Object . keys ( outputMap [ sourceOutputIndex ] ) . forEach ( ( targetNodeName : string ) => {
Object . keys ( outputMap [ sourceOutputIndex ] [ targetNodeName ] ) . forEach ( ( targetInputIndex : string ) => {
2021-12-03 09:53:55 -08:00
if ( targetNodeName ) {
const connection = this . getJSPlumbConnection ( sourceNodeName , parseInt ( sourceOutputIndex , 10 ) , targetNodeName , parseInt ( targetInputIndex , 10 ) ) ;
2021-11-19 01:17:13 -08:00
2021-12-03 09:53:55 -08:00
if ( connection ) {
const output = outputMap [ sourceOutputIndex ] [ targetNodeName ] [ targetInputIndex ] ;
if ( ! output || ! output . total ) {
CanvasHelpers . resetConnection ( connection ) ;
}
else {
CanvasHelpers . addConnectionOutputSuccess ( connection , output ) ;
}
}
2021-11-19 01:17:13 -08:00
}
2021-12-03 09:53:55 -08:00
const endpoint = this . getPlusEndpoint ( sourceNodeName , parseInt ( sourceOutputIndex , 10 ) ) ;
if ( endpoint && endpoint . endpoint ) {
const output = outputMap [ sourceOutputIndex ] [ NODE _OUTPUT _DEFAULT _KEY ] [ 0 ] ;
if ( output && output . total > 0 ) {
( endpoint . endpoint as N8nPlusEndpoint ) . setSuccessOutput ( CanvasHelpers . getRunItemsLabel ( output ) ) ;
}
else {
( endpoint . endpoint as N8nPlusEndpoint ) . clearSuccessOutput ( ) ;
}
2021-11-19 01:17:13 -08:00
}
} ) ;
} ) ;
} ) ;
} ,
2019-06-23 03:35:23 -07:00
removeNode ( nodeName : string ) {
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
2021-11-19 01:17:13 -08:00
const node = this . $store . getters . getNodeByName ( nodeName ) as INodeUi | null ;
if ( ! node ) {
return ;
}
2019-06-23 03:35:23 -07:00
// "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 ;
}
}
2021-12-03 09:53:55 -08:00
let waitForNewConnection = false ;
2021-11-19 01:17:13 -08:00
// connect nodes before/after deleted node
const nodeType : INodeTypeDescription | null = this . $store . getters . nodeType ( node . type , node . typeVersion ) ;
if ( nodeType && nodeType . outputs . length === 1
&& nodeType . inputs . length === 1 ) {
const { incoming , outgoing } = this . getIncomingOutgoingConnections ( node . name ) ;
if ( incoming . length === 1 && outgoing . length === 1 ) {
const conn1 = incoming [ 0 ] ;
const conn2 = outgoing [ 0 ] ;
if ( conn1 . _ _meta && conn2 . _ _meta ) {
2021-12-03 09:53:55 -08:00
waitForNewConnection = true ;
2021-11-19 01:17:13 -08:00
const sourceNodeName = conn1 . _ _meta . sourceNodeName ;
const sourceNodeOutputIndex = conn1 . _ _meta . sourceOutputIndex ;
const targetNodeName = conn2 . _ _meta . targetNodeName ;
const targetNodeOuputIndex = conn2 . _ _meta . targetOutputIndex ;
setTimeout ( ( ) => {
this . connectTwoNodes ( sourceNodeName , sourceNodeOutputIndex , targetNodeName , targetNodeOuputIndex ) ;
2021-12-03 09:53:55 -08:00
if ( waitForNewConnection ) {
this . instance . setSuspendDrawing ( false , true ) ;
waitForNewConnection = false ;
}
} , 100 ) ; // just to make it clear to users that this is a new connection
2021-11-19 01:17:13 -08:00
}
}
}
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
setTimeout ( ( ) => {
const nodeIndex = this . $store . getters . getNodeIndex ( nodeName ) ;
const nodeIdName = ` node- ${ nodeIndex } ` ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
// Suspend drawing
this . instance . setSuspendDrawing ( true ) ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
// Remove all endpoints and the connections in jsplumb
this . instance . removeAllEndpoints ( nodeIdName ) ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
// Remove the draggable
// @ts-ignore
this . instance . destroyDraggable ( nodeIdName ) ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
// Remove the connections in data
this . $store . commit ( 'removeAllNodeConnection' , node ) ;
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
this . $store . commit ( 'removeNode' , node ) ;
this . $store . commit ( 'clearNodeExecutionData' , node . name ) ;
2019-06-23 03:35:23 -07:00
2021-12-03 09:53:55 -08:00
if ( ! waitForNewConnection ) {
// Now it can draw again
this . instance . setSuspendDrawing ( false , true ) ;
}
2019-06-23 03:35:23 -07:00
2021-11-19 01:17:13 -08:00
// 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 } ) ;
}
} , 0 ) ; // allow other events to finish like drag stop
2019-06-23 03:35:23 -07:00
} ,
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
2021-11-19 01:17:13 -08:00
newName = CanvasHelpers . getUniqueNodeName ( this . $store . getters . allNodes , newName ) ;
2019-06-23 03:35:23 -07:00
// 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
2021-11-30 11:37:55 -08:00
this . deleteEveryEndpoint ( ) ;
2019-06-23 03:35:23 -07:00
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-11-30 11:37:55 -08:00
deleteEveryEndpoint ( ) {
// Check as it does not exist on first load
if ( this . instance ) {
const nodes = this . $store . getters . allNodes as INodeUi [ ] ;
// @ts-ignore
nodes . forEach ( ( node : INodeUi ) => this . instance . destroyDraggable ( ` ${ NODE _NAME _PREFIX } ${ this . $store . getters . getNodeIndex ( node . name ) } ` ) ) ;
this . instance . deleteEveryEndpoint ( ) ;
}
} ,
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-11-19 01:17:13 -08:00
nodeType = this . $store . getters . nodeType ( node . type , node . typeVersion ) as INodeTypeDescription | null ;
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 ( '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 ;
2021-11-19 01:17:13 -08:00
newName = CanvasHelpers . getUniqueNodeName ( this . $store . getters . allNodes , 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 ) => {
2021-11-19 01:17:13 -08:00
connections = this . $store . getters . outgoingConnectionsByNodeName ( node . name ) ;
2019-06-23 03:35:23 -07:00
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
2021-11-30 11:37:55 -08:00
this . deleteEveryEndpoint ( ) ;
2019-06-23 03:35:23 -07:00
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 ;
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 ;
}
2021-11-19 01:17:13 -08:00
button {
border : var ( -- border - base ) ;
}
2019-06-23 03:35:23 -07:00
}
. node - creator - button {
position : fixed ;
text - align : center ;
top : 80 px ;
right : 20 px ;
}
. 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 ;
}
. 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" >
2021-11-19 01:17:13 -08:00
. connection - run - items - label {
span {
border - radius : 7 px ;
background - color : hsla ( var ( -- color - canvas - background - h ) , var ( -- color - canvas - background - s ) , var ( -- color - canvas - background - l ) , .85 ) ;
line - height : 1.3 em ;
padding : 0 px 3 px ;
white - space : nowrap ;
font - size : var ( -- font - size - s ) ;
font - weight : var ( -- font - weight - regular ) ;
color : var ( -- color - success ) ;
}
2019-06-27 02:27:02 -07:00
2021-12-03 09:53:55 -08:00
. floating {
2021-11-19 01:17:13 -08:00
position : absolute ;
top : - 22 px ;
transform : translateX ( - 50 % ) ;
}
2019-07-25 12:57:27 -07:00
}
2021-11-19 01:17:13 -08:00
. connection - input - name - label {
2019-06-23 03:35:23 -07:00
position : relative ;
2021-11-19 01:17:13 -08:00
span {
position : absolute ;
top : - 10 px ;
left : - 60 px ;
2019-06-23 03:35:23 -07:00
}
}
. 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 {
2021-11-19 01:17:13 -08:00
background - color : hsla ( var ( -- color - canvas - background - h ) , var ( -- color - canvas - background - s ) , var ( -- color - canvas - background - l ) , .85 ) ;
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 ) ;
}
}
2021-11-19 01:17:13 -08:00
. connection - actions {
& : hover {
display : block ! important ;
}
> div {
color : var ( -- color - foreground - xdark ) ;
border : 2 px solid var ( -- color - foreground - xdark ) ;
background - color : var ( -- color - background - xlight ) ;
border - radius : var ( -- border - radius - base ) ;
height : var ( -- spacing - l ) ;
width : var ( -- spacing - l ) ;
cursor : pointer ;
display : inline - flex ;
align - items : center ;
justify - content : center ;
position : absolute ;
top : - 12 px ;
& . add {
right : 4 px ;
}
& . delete {
left : 4 px ;
}
svg {
pointer - events : none ;
font - size : var ( -- font - size - 2 xs ) ;
}
& : hover {
border - color : var ( -- color - primary ) ;
color : var ( -- color - primary ) ;
}
}
}
2019-06-23 03:35:23 -07:00
< / style >