2019-06-23 03:35:23 -07:00
< template >
< div class = "node-view-root" >
< div
class = "node-view-wrapper"
: class = "workflowClasses"
@ mousedown = "mouseDown"
@ mouseup = "mouseUp"
2019-07-25 22:41:09 -07:00
@ wheel = "wheelScroll"
2019-06-23 03:35:23 -07:00
>
< div class = "node-view-background" :style ="backgroundStyle" > < / div >
< div id = "node-view" class = "node-view" :style ="workflowStyle" >
< node
v - for = "nodeData in nodes"
@ duplicateNode = "duplicateNode"
@ deselectAllNodes = "deselectAllNodes"
2019-07-17 10:05:03 -07:00
@ deselectNode = "nodeDeselectedByName"
2019-06-23 03:35:23 -07:00
@ nodeSelected = "nodeSelectedByName"
@ removeNode = "removeNode"
@ runWorkflow = "runWorkflow"
: id = "'node-' + getNodeIndex(nodeData.name)"
: key = "getNodeIndex(nodeData.name)"
: name = "nodeData.name"
: instance = "instance"
> < / node >
< / div >
< / div >
< DataDisplay @valueChanged ="valueChanged" / >
< div v-if ="!createNodeActive && !isReadOnly" class="node-creator-button" title="Add Node" @click="openNodeCreator" >
< el -button icon = "el-icon-plus" circle > < / e l - b u t t o n >
< / div >
< node -creator
: active = "createNodeActive"
@ nodeTypeSelected = "nodeTypeSelected"
@ closeNodeCreator = "closeNodeCreator"
> < / n o d e - c r e a t o r >
< div class = "zoom-menu" >
< button @click ="setZoom('in')" class = "button-white" title = "Zoom In" >
< font -awesome -icon icon = "search-plus" / >
< / button >
< button @click ="setZoom('out')" class = "button-white" title = "Zoom Out" >
< font -awesome -icon icon = "search-minus" / >
< / button >
< button
v - if = "nodeViewScale !== 1"
@ click = "setZoom('reset')"
class = "button-white"
title = "Reset Zoom"
>
< font -awesome -icon icon = "undo" title = "Reset Zoom" / >
< / button >
< / div >
< div class = "workflow-execute-wrapper" v-if ="!isReadOnly" >
< el -button
type = "text"
@ click . stop = "runWorkflow()"
class = "workflow-run-button"
: class = "{'running': workflowRunning}"
: disabled = "workflowRunning"
title = "Executes the Workflow from the Start or Webhook Node."
>
< div class = "run-icon" >
< font -awesome -icon icon = "spinner" spin v -if = " workflowRunning " / >
< font -awesome -icon icon = "play-circle" v -else / >
< / div >
{ { runButtonText } }
< / e l - b u t t o n >
< el -button
v - if = "workflowRunning === true && !executionWaitingForWebhook"
circle
type = "text"
@ click . stop = "stopExecution()"
class = "stop-execution"
: title = "stopExecutionInProgress ? 'Stopping current execution':'Stop current execution'"
>
< font -awesome -icon icon = "stop" : class = "{'fa-spin': stopExecutionInProgress}" / >
< / e l - b u t t o n >
< el -button
v - if = "workflowRunning === true && executionWaitingForWebhook === true"
circle
type = "text"
@ click . stop = "stopWaitingForWebhook()"
class = "stop-execution"
title = "Stop waiting for Webhook call"
>
< font -awesome -icon icon = "stop" : class = "{'fa-spin': stopExecutionInProgress}" / >
< / e l - b u t t o n >
2019-06-24 04:52:03 -07:00
< el -button
v - if = "!isReadOnly && workflowExecution && !workflowRunning"
circle
type = "text"
@ click . stop = "clearExecutionData()"
class = "clear-execution"
title = "Deletes the current Execution Data."
>
< font -awesome -icon icon = "trash" class = "clear-execution-icon" / >
< / e l - b u t t o n >
2019-06-23 03:35:23 -07:00
< / div >
< / div >
< / template >
< script lang = "ts" >
import Vue from 'vue' ;
import { jsPlumb , OnConnectionBindInfo } from 'jsplumb' ;
import { NODE _NAME _PREFIX , PLACEHOLDER _EMPTY _WORKFLOW _ID } from '@/constants' ;
import { copyPaste } from '@/components/mixins/copyPaste' ;
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' ;
import { workflowHelpers } from '@/components/mixins/workflowHelpers' ;
import { workflowRun } from '@/components/mixins/workflowRun' ;
import DataDisplay from '@/components/DataDisplay.vue' ;
import Node from '@/components/Node.vue' ;
import NodeCreator from '@/components/NodeCreator.vue' ;
import NodeSettings from '@/components/NodeSettings.vue' ;
import RunData from '@/components/RunData.vue' ;
import mixins from 'vue-typed-mixins' ;
import { debounce } from 'lodash' ;
import axios from 'axios' ;
import {
IConnection ,
IConnections ,
IDataObject ,
INode ,
INodeConnections ,
INodeIssues ,
INodeTypeDescription ,
IRunData ,
NodeHelpers ,
} from 'n8n-workflow' ;
import {
IConnectionsUi ,
IExecutionResponse ,
IExecutionsStopData ,
IN8nUISettings ,
IStartRunData ,
IWorkflowDb ,
IWorkflowData ,
INodeUi ,
IRunDataUi ,
IUpdateInformation ,
IWorkflowDataUpdate ,
XYPositon ,
} from '../Interface' ;
export default mixins (
copyPaste ,
genericHelpers ,
mouseSelect ,
moveNodeWorkflow ,
restApi ,
showMessage ,
workflowHelpers ,
workflowRun ,
)
. extend ( {
name : 'NodeView' ,
components : {
DataDisplay ,
Node ,
NodeCreator ,
NodeSettings ,
RunData ,
} ,
errorCaptured : ( err , vm , info ) => {
console . error ( 'errorCaptured .....' ) ;
console . error ( err ) ;
} ,
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 ;
} ,
} ,
computed : {
activeNode ( ) : INodeUi | null {
return this . $store . getters . activeNode ;
} ,
executionWaitingForWebhook ( ) : boolean {
return this . $store . getters . executionWaitingForWebhook ;
} ,
lastSelectedNode ( ) : INodeUi {
return this . $store . getters . lastSelectedNode ;
} ,
connections ( ) : IConnectionsUi {
return this . $store . getters . allConnections ;
} ,
nodes ( ) : INodeUi [ ] {
return this . $store . getters . allNodes ;
} ,
runButtonText ( ) : string {
if ( this . workflowRunning === false ) {
return 'Execute Workflow' ;
}
if ( this . executionWaitingForWebhook === true ) {
return 'Waiting for Webhook-Call' ;
}
return 'Executing Workflow' ;
} ,
workflowStyle ( ) : object {
const offsetPosition = this . $store . getters . getNodeViewOffsetPosition ;
return {
left : offsetPosition [ 0 ] + 'px' ,
top : offsetPosition [ 1 ] + 'px' ,
} ;
} ,
backgroundStyle ( ) : object {
const offsetPosition = this . $store . getters . getNodeViewOffsetPosition ;
return {
'transform' : ` scale( ${ this . nodeViewScale } ) ` ,
'background-position' : ` right ${ - offsetPosition [ 0 ] } px bottom ${ - offsetPosition [ 1 ] } px ` ,
} ;
} ,
workflowClasses ( ) {
const returnClasses = [ ] ;
if ( this . ctrlKeyPressed === true ) {
if ( this . $store . getters . isNodeViewMoveInProgress === true ) {
returnClasses . push ( 'move-in-process' ) ;
} else {
returnClasses . push ( 'move-active' ) ;
}
}
if ( this . selectActive || this . ctrlKeyPressed === true ) {
// Makes sure that nothing gets selected while select or move is active
returnClasses . push ( 'do-not-select' ) ;
}
return returnClasses ;
} ,
2019-06-24 04:52:03 -07:00
workflowExecution ( ) : IExecutionResponse | null {
return this . $store . getters . getWorkflowExecution ;
} ,
2019-06-23 03:35:23 -07:00
workflowRunning ( ) : boolean {
return this . $store . getters . isActionActive ( 'workflowRunning' ) ;
} ,
} ,
data ( ) {
return {
createNodeActive : false ,
instance : jsPlumb . getInstance ( ) ,
2019-07-17 03:47:20 -07:00
lastClickPosition : [ 450 , 450 ] as XYPositon ,
2019-06-23 03:35:23 -07:00
nodeViewScale : 1 ,
ctrlKeyPressed : false ,
debouncedFunctions : [ ] as any [ ] , // tslint:disable-line:no-any
stopExecutionInProgress : false ,
} ;
} ,
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 : {
async callDebounced ( ... inputParameters : any [ ] ) : Promise < void > { // tslint:disable-line:no-any
const functionName = inputParameters . shift ( ) as string ;
const debounceTime = inputParameters . shift ( ) as number ;
// @ts-ignore
if ( this . debouncedFunctions [ functionName ] === undefined ) {
// @ts-ignore
this . debouncedFunctions [ functionName ] = debounce ( this [ functionName ] , debounceTime , { leading : true } ) ;
}
// @ts-ignore
await this . debouncedFunctions [ functionName ] . apply ( this , inputParameters ) ;
} ,
2019-06-24 04:52:03 -07:00
clearExecutionData ( ) {
this . $store . commit ( 'setWorkflowExecutionData' , null ) ;
this . updateNodesExecutionIssues ( ) ;
} ,
2019-06-23 03:35:23 -07:00
openNodeCreator ( ) {
this . createNodeActive = true ;
} ,
async openExecution ( executionId : string ) {
this . resetWorkspace ( ) ;
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! ` ) ;
}
this . $store . commit ( 'setWorkflowName' , data . workflowData . name ) ;
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 ) ) ) ;
} ,
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 ) ;
this . $store . commit ( 'setWorkflowName' , data . name ) ;
this . $store . commit ( 'setWorkflowSettings' , data . settings || { } ) ;
await this . addNodes ( data . nodes , data . connections ) ;
} ,
mouseDown ( e : MouseEvent ) {
// Save the location of the mouse click
2019-07-17 03:44:00 -07:00
const offsetPosition = this . $store . getters . getNodeViewOffsetPosition ;
this . lastClickPosition [ 0 ] = e . pageX - offsetPosition [ 0 ] ;
this . lastClickPosition [ 1 ] = e . pageY - offsetPosition [ 1 ] ;
2019-06-23 03:35:23 -07:00
this . mouseDownMouseSelect ( e ) ;
this . mouseDownMoveWorkflow ( e ) ;
// 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 ) {
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' && (
path [ index ] . className . includes ( 'el-message-box' ) || path [ index ] . className . includes ( 'ignore-key-press' )
) ) {
return ;
}
}
if ( e . key === 'd' ) {
this . callDebounced ( 'deactivateSelectedNode' , 350 ) ;
} else if ( e . key === 'Delete' ) {
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
this . callDebounced ( 'deleteSelectedNodes' , 500 ) ;
} else if ( e . key === 'Escape' ) {
this . createNodeActive = false ;
this . $store . commit ( 'setActiveNode' , null ) ;
} else if ( e . key === 'Tab' ) {
this . 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 ;
} else if ( e . key === 'F2' ) {
const lastSelectedNode = this . lastSelectedNode ;
if ( lastSelectedNode !== null ) {
this . callDebounced ( 'renameNodePrompt' , 1500 , lastSelectedNode . name ) ;
}
} else if ( e . key === '+' ) {
this . callDebounced ( 'setZoom' , 300 , 'in' ) ;
} else if ( e . key === '-' ) {
this . callDebounced ( 'setZoom' , 300 , 'out' ) ;
2019-07-16 22:26:44 -07:00
} else if ( ( e . key === '0' ) && ( this . isCtrlKeyPressed ( e ) === true ) ) {
2019-06-23 03:35:23 -07:00
this . callDebounced ( 'setZoom' , 300 , 'reset' ) ;
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 ( ) ;
this . $root . $emit ( 'openWorkflowDialog' ) ;
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 ( ) ;
this . $router . push ( { name : 'NodeViewNew' } ) ;
this . $showMessage ( {
title : 'Created' ,
message : 'A new workflow got created!' ,
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 ( ) ;
this . callDebounced ( 'saveCurrentWorkflow' , 1000 ) ;
} else if ( e . key === 'Enter' ) {
// Activate the last selected node
const lastSelectedNode = this . $store . getters . lastSelectedNode ;
if ( lastSelectedNode !== null ) {
this . $store . commit ( 'setActiveNode' , lastSelectedNode . name ) ;
}
2019-07-25 09:50:45 -07:00
} else if ( e . key === 'ArrowRight' && e . shiftKey === true ) {
2019-07-17 09:44:05 -07:00
// Select all downstream nodes
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
this . callDebounced ( 'selectDownstreamNodes' , 1000 ) ;
2019-07-25 09:50:45 -07:00
} else if ( e . key === 'ArrowRight' ) {
2019-06-23 03:35:23 -07:00
// Set child node active
const lastSelectedNode = this . $store . getters . lastSelectedNode ;
if ( lastSelectedNode === null ) {
return ;
}
const connections = this . $store . getters . connectionsByNodeName ( lastSelectedNode . name ) ;
if ( connections . main === undefined || connections . main . length === 0 ) {
return ;
}
this . callDebounced ( 'nodeSelectedByName' , 100 , connections . main [ 0 ] [ 0 ] . node , false , true ) ;
2019-07-25 09:50:45 -07:00
} else if ( e . key === 'ArrowLeft' && e . shiftKey === true ) {
2019-07-17 09:44:05 -07:00
// Select all downstream nodes
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
this . callDebounced ( 'selectUpstreamNodes' , 1000 ) ;
2019-07-25 09:50:45 -07:00
} else if ( e . key === 'ArrowLeft' ) {
2019-06-23 03:35:23 -07:00
// Set parent node active
const lastSelectedNode = this . $store . getters . lastSelectedNode ;
if ( lastSelectedNode === null ) {
return ;
}
const workflow = this . getWorkflow ( ) ;
if ( ! workflow . connectionsByDestinationNode . hasOwnProperty ( lastSelectedNode . name ) ) {
return ;
}
const connections = workflow . connectionsByDestinationNode [ lastSelectedNode . name ] ;
if ( connections . main === undefined || connections . main . length === 0 ) {
return ;
}
this . callDebounced ( 'nodeSelectedByName' , 100 , connections . main [ 0 ] [ 0 ] . node , false , true ) ;
2019-07-25 09:50:45 -07:00
} else if ( [ 'ArrowUp' , 'ArrowDown' ] . includes ( e . key ) ) {
2019-06-23 03:35:23 -07:00
// Set sibling node as active
// Check first if it has a parent node
const lastSelectedNode = this . $store . getters . lastSelectedNode ;
if ( lastSelectedNode === null ) {
return ;
}
const workflow = this . getWorkflow ( ) ;
if ( ! workflow . connectionsByDestinationNode . hasOwnProperty ( lastSelectedNode . name ) ) {
return ;
}
const connections = workflow . connectionsByDestinationNode [ lastSelectedNode . name ] ;
if ( connections . main === undefined || connections . main . length === 0 ) {
return ;
}
const parentNode = connections . main [ 0 ] [ 0 ] . node ;
const connectionsParent = this . $store . getters . connectionsByNodeName ( parentNode ) ;
if ( connectionsParent . main === undefined || connectionsParent . main . length === 0 ) {
return ;
}
// Get all the sibling nodes and their x positions to know which one to set active
let siblingNode : INodeUi ;
2019-07-25 09:50:45 -07:00
let lastCheckedNodePosition = e . key === 'ArrowUp' ? - 99999999 : 99999999 ;
2019-06-23 03:35:23 -07:00
let nextSelectNode : string | null = null ;
for ( const ouputConnections of connectionsParent . main ) {
for ( const ouputConnection of ouputConnections ) {
if ( ouputConnection . node === lastSelectedNode . name ) {
// Ignore current node
continue ;
}
siblingNode = this . $store . getters . nodeByName ( ouputConnection . node ) ;
2019-07-25 09:50:45 -07:00
if ( e . key === 'ArrowUp' ) {
2019-06-23 03:35:23 -07:00
// Get the next node on the left
2019-07-25 09:50:45 -07:00
if ( siblingNode . position [ 1 ] <= lastSelectedNode . position [ 1 ] && siblingNode . position [ 1 ] > lastCheckedNodePosition ) {
2019-06-23 03:35:23 -07:00
nextSelectNode = siblingNode . name ;
2019-07-25 09:50:45 -07:00
lastCheckedNodePosition = siblingNode . position [ 1 ] ;
2019-06-23 03:35:23 -07:00
}
} else {
// Get the next node on the right
2019-07-25 09:50:45 -07:00
if ( siblingNode . position [ 1 ] >= lastSelectedNode . position [ 1 ] && siblingNode . position [ 1 ] < lastCheckedNodePosition ) {
2019-06-23 03:35:23 -07:00
nextSelectNode = siblingNode . name ;
2019-07-25 09:50:45 -07:00
lastCheckedNodePosition = siblingNode . position [ 1 ] ;
2019-06-23 03:35:23 -07:00
}
}
}
}
if ( nextSelectNode !== null ) {
this . callDebounced ( 'nodeSelectedByName' , 100 , nextSelectNode , false , true ) ;
}
}
} ,
deactivateSelectedNode ( ) {
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
let updateInformation ;
this . $store . getters . getSelectedNodes . forEach ( ( node : INodeUi ) => {
// Toggle disabled flag
updateInformation = {
name : node . name ,
properties : {
disabled : ! node . disabled ,
} ,
} ;
this . $store . commit ( 'updateNodeProperties' , updateInformation ) ;
} ) ;
} ,
deleteSelectedNodes ( ) {
// Copy "selectedNodes" as the nodes get deleted out of selection
// when they get deleted and if we would use original it would mess
// with the index and would so not delete all nodes
const nodesToDelete : string [ ] = this . $store . getters . getSelectedNodes . map ( ( node : INodeUi ) => {
return node . name ;
} ) ;
nodesToDelete . forEach ( ( nodeName : string ) => {
this . removeNode ( nodeName ) ;
} ) ;
} ,
selectAllNodes ( ) {
this . nodes . forEach ( ( node ) => {
this . nodeSelectedByName ( node . name ) ;
} ) ;
} ,
2019-07-17 09:44:05 -07:00
selectUpstreamNodes ( ) {
const lastSelectedNode = this . $store . getters . lastSelectedNode as INodeUi | null ;
if ( lastSelectedNode === null ) {
return ;
}
this . deselectAllNodes ( ) ;
// Get all upstream nodes and select them
const workflow = this . getWorkflow ( ) ;
for ( const nodeName of workflow . getParentNodes ( lastSelectedNode . name ) ) {
this . nodeSelectedByName ( nodeName ) ;
}
// At the end select the previously selected node again
this . nodeSelectedByName ( lastSelectedNode . name ) ;
} ,
selectDownstreamNodes ( ) {
const lastSelectedNode = this . $store . getters . lastSelectedNode as INodeUi | null ;
if ( lastSelectedNode === null ) {
return ;
}
this . deselectAllNodes ( ) ;
// Get all downstream nodes and select them
const workflow = this . getWorkflow ( ) ;
for ( const nodeName of workflow . getChildNodes ( lastSelectedNode . name ) ) {
this . nodeSelectedByName ( nodeName ) ;
}
// At the end select the previously selected node again
this . nodeSelectedByName ( lastSelectedNode . name ) ;
} ,
2019-06-23 03:35:23 -07:00
cutSelectedNodes ( ) {
this . copySelectedNodes ( ) ;
this . deleteSelectedNodes ( ) ;
} ,
copySelectedNodes ( ) {
this . getSelectedNodesToSave ( ) . then ( ( data ) => {
const nodeData = JSON . stringify ( data , null , 2 ) ;
this . copyToClipboard ( nodeData ) ;
} ) ;
} ,
setZoom ( zoom : string ) {
if ( zoom === 'in' ) {
this . nodeViewScale *= 1.25 ;
} else if ( zoom === 'out' ) {
this . nodeViewScale /= 1.25 ;
} else {
this . nodeViewScale = 1 ;
}
const zoomLevel = this . nodeViewScale ;
const element = this . instance . getContainer ( ) as HTMLElement ;
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 ) ;
} ,
async stopExecution ( ) {
const executionId = this . $store . getters . activeExecutionId ;
if ( executionId === null ) {
return ;
}
try {
this . stopExecutionInProgress = true ;
const stopData : IExecutionsStopData = await this . restApi ( ) . stopCurrentExecution ( executionId ) ;
this . $showMessage ( {
title : 'Execution stopped' ,
message : ` The execution with the id " ${ executionId } " got stopped! ` ,
type : 'success' ,
} ) ;
} catch ( error ) {
this . $showError ( error , 'Problem stopping execution' , 'There was a problem stopping the execuction:' ) ;
}
this . stopExecutionInProgress = false ;
} ,
async stopWaitingForWebhook ( ) {
let result ;
try {
result = await this . restApi ( ) . removeTestWebhook ( this . $store . getters . workflowId ) ;
} catch ( error ) {
this . $showError ( error , 'Problem deleting the test-webhook' , 'There was a problem deleting webhook:' ) ;
return ;
}
this . $showMessage ( {
title : 'Webhook got deleted' ,
message : ` The webhook got deleted! ` ,
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 ;
}
}
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 ( ) ;
return workflowData ;
} ,
// Imports the given workflow data into the current workflow
async importWorkflowData ( workflowData : IWorkflowDataUpdate ) : Promise < void > {
// If it is JSON check if it looks on the first look like data we can use
if (
! workflowData . hasOwnProperty ( 'nodes' ) ||
! workflowData . hasOwnProperty ( 'connections' )
) {
return ;
}
try {
// By default we automatically deselect all the currently
// selected nodes and select the new ones
this . deselectAllNodes ( ) ;
2019-07-17 07:05:01 -07:00
// Fix the node position as it could be totally offscreen
// and the pasted nodes would so not be directly visible to
// the user
this . updateNodePositions ( workflowData , this . getNewNodePosition ( ) ) ;
const data = await this . addNodesToWorkflow ( workflowData ) ;
2019-06-23 03:35:23 -07:00
setTimeout ( ( ) => {
data . nodes ! . forEach ( ( node : INodeUi ) => {
this . nodeSelectedByName ( node . name ) ;
} ) ;
} ) ;
} catch ( error ) {
this . $showError ( error , 'Problem importing workflow' , 'There was a problem importing workflow data:' ) ;
}
} ,
closeNodeCreator ( ) {
this . createNodeActive = false ;
} ,
nodeTypeSelected ( nodeTypeName : string ) {
this . addNodeButton ( nodeTypeName ) ;
this . createNodeActive = false ;
} ,
2019-07-17 10:05:03 -07:00
nodeDeselectedByName ( nodeName : string ) {
const node = this . $store . getters . nodeByName ( nodeName ) ;
if ( node ) {
this . nodeDeselected ( node ) ;
}
} ,
2019-06-23 03:35:23 -07:00
nodeSelectedByName ( nodeName : string , setActive = false , deselectAllOthers ? : boolean ) {
if ( deselectAllOthers === true ) {
this . deselectAllNodes ( ) ;
}
const node = this . $store . getters . nodeByName ( nodeName ) ;
if ( node ) {
this . nodeSelected ( node ) ;
}
this . $store . commit ( 'setLastSelectedNode' , node . name ) ;
if ( setActive === true ) {
this . $store . commit ( 'setActiveNode' , node . name ) ;
}
} ,
canUsePosition ( position1 : XYPositon , position2 : XYPositon ) {
if ( Math . abs ( position1 [ 0 ] - position2 [ 0 ] ) <= 100 ) {
if ( Math . abs ( position1 [ 1 ] - position2 [ 1 ] ) <= 50 ) {
return false ;
}
}
return true ;
} ,
getNewNodePosition ( newPosition ? : XYPositon , movePosition ? : XYPositon ) : XYPositon {
// TODO: Lates has to consider also the view position (that it creates the node where it is visible)
// Use the last click position as position for new node
if ( newPosition === undefined ) {
newPosition = this . lastClickPosition ;
}
// @ts-ignore
newPosition = newPosition . slice ( ) ;
if ( ! movePosition ) {
movePosition = [ 35 , 35 ] ;
}
let conflictFound = false ;
let i , node ;
do {
conflictFound = false ;
for ( i = 0 ; i < this . nodes . length ; i ++ ) {
node = this . nodes [ i ] ;
if ( ! this . canUsePosition ( node . position , newPosition ! ) ) {
conflictFound = true ;
break ;
}
}
if ( conflictFound === true ) {
newPosition ! [ 0 ] += movePosition [ 0 ] ;
newPosition ! [ 1 ] += movePosition [ 1 ] ;
}
} while ( conflictFound === true ) ;
return newPosition ! ;
} ,
getUniqueNodeName ( originalName : string , additinalUsedNames ? : string [ ] ) {
2019-07-17 10:58:54 -07:00
// Check if node-name is unique else find one that is
2019-06-23 03:35:23 -07:00
additinalUsedNames = additinalUsedNames || [ ] ;
2019-07-17 10:58:54 -07:00
// Get all the names of the current nodes
2019-06-23 03:35:23 -07:00
const nodeNames = this . $store . getters . allNodes . map ( ( node : INodeUi ) => {
return node . name ;
} ) ;
2019-07-17 10:58:54 -07:00
const nameMatch = originalName . match ( /(.*[a-zA-Z])(\d*)/ ) ;
let ignore , baseName , nameIndex , uniqueName ;
2019-06-23 03:35:23 -07:00
let index = 1 ;
2019-07-17 10:58:54 -07:00
if ( nameMatch === null ) {
// Name is only a number
index = parseInt ( originalName , 10 ) ;
baseName = '' ;
uniqueName = baseName + index ;
} else {
// Name is string or string/number combination
[ ignore , baseName , nameIndex ] = nameMatch ;
if ( nameIndex !== '' ) {
index = parseInt ( nameIndex , 10 ) ;
}
uniqueName = baseName ;
}
2019-06-23 03:35:23 -07:00
while (
2019-07-17 10:58:54 -07:00
nodeNames . includes ( uniqueName ) ||
additinalUsedNames . includes ( uniqueName )
2019-06-23 03:35:23 -07:00
) {
2019-07-17 10:58:54 -07:00
uniqueName = baseName + ( index ++ ) ;
2019-06-23 03:35:23 -07:00
}
return uniqueName ;
} ,
showMaxNodeTypeError ( nodeTypeData : INodeTypeDescription ) {
const maxNodes = nodeTypeData . maxNodes ;
this . $showMessage ( {
title : 'Could not create node!' ,
message : ` Node can not be created because in a workflow max. ${ maxNodes } ${ maxNodes === 1 ? 'node' : 'nodes' } of type " ${ nodeTypeData . displayName } " ${ maxNodes === 1 ? 'is' : 'are' } allowed! ` ,
type : 'error' ,
duration : 0 ,
} ) ;
} ,
async addNodeButton ( nodeTypeName : string ) {
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
const nodeTypeData : INodeTypeDescription | null = this . $store . getters . nodeType ( nodeTypeName ) ;
if ( nodeTypeData === null ) {
this . $showMessage ( {
title : 'Could not create node!' ,
message : ` Node of type " ${ nodeTypeName } " could not be created as it is not known. ` ,
type : 'error' ,
} ) ;
return ;
}
if ( nodeTypeData . maxNodes !== undefined && this . getNodeTypeCount ( nodeTypeName ) >= nodeTypeData . maxNodes ) {
this . showMaxNodeTypeError ( nodeTypeData ) ;
return ;
}
const newNodeData : INodeUi = {
name : nodeTypeData . defaults . name as string ,
type : nodeTypeData . name ,
typeVersion : nodeTypeData . version ,
position : [ 0 , 0 ] ,
parameters : { } ,
} ;
// Check if there is a last selected node
const lastSelectedNode = this . $store . getters . lastSelectedNode ;
if ( lastSelectedNode ) {
// If a node is active then add the new node directly after the current one
// newNodeData.position = [activeNode.position[0], activeNode.position[1] + 60];
newNodeData . position = this . getNewNodePosition (
2019-07-25 09:50:45 -07:00
[ lastSelectedNode . position [ 0 ] + 180 , lastSelectedNode . position [ 1 ] ] ,
[ 110 , 0 ]
2019-06-23 03:35:23 -07:00
) ;
} else {
// If no node is active find a free spot
newNodeData . position = this . getNewNodePosition ( ) ;
}
// Check if node-name is unique else find one that is
newNodeData . name = this . getUniqueNodeName ( newNodeData . name ) ;
await this . addNodes ( [ newNodeData ] ) ;
// Automatically deselect all nodes and select the current one and also active
// current node
this . deselectAllNodes ( ) ;
setTimeout ( ( ) => {
this . nodeSelectedByName ( newNodeData . name , true ) ;
} ) ;
if ( lastSelectedNode ) {
// If a node is last selected then connect between the active and its child ones
await Vue . nextTick ( ) ;
// Add connections of active node to newly created one
let connections = this . $store . getters . connectionsByNodeName (
lastSelectedNode . name
) ;
connections = JSON . parse ( JSON . stringify ( connections ) ) ;
for ( const type of Object . keys ( connections ) ) {
for ( let inputIndex = 0 ; inputIndex < connections [ type ] . length ; inputIndex ++ ) {
connections [ type ] [ inputIndex ] . forEach ( ( connectionInfo : IConnection ) => {
// Remove currenct connection
const connectionDataDisonnect = [
{
node : lastSelectedNode . name ,
type ,
index : inputIndex ,
} ,
connectionInfo ,
] as [ IConnection , IConnection ] ;
this . _ _removeConnection ( connectionDataDisonnect , true ) ;
const connectionDataConnect = [
{
node : newNodeData . name ,
type ,
index : inputIndex ,
} ,
connectionInfo ,
] as [ IConnection , IConnection ] ;
this . _ _addConnection ( connectionDataConnect , true ) ;
} ) ;
}
}
// TODO: Check if new node has input
// TODO: disconnect
// Connect active node to the newly created one
const connectionData = [
{
node : lastSelectedNode . name ,
type : 'main' ,
index : 0 ,
} ,
{
node : newNodeData . name ,
type : 'main' ,
index : 0 ,
} ,
] as [ IConnection , IConnection ] ;
this . _ _addConnection ( connectionData , true ) ;
}
} ,
initNodeView ( ) {
this . instance . importDefaults ( {
// notice the 'curviness' argument to this Bezier curve.
// the curves on this page are far smoother
// than the curves on the first demo, which use the default curviness value.
// Connector: ["Bezier", { curviness: 80 }],
Connector : [ 'Bezier' , { curviness : 40 } ] ,
// @ts-ignore
Endpoint : [ 'Dot' , { radius : 5 } ] ,
DragOptions : { cursor : 'pointer' , zIndex : 5000 } ,
PaintStyle : { strokeWidth : 2 , stroke : '#334455' } ,
EndpointStyle : { radius : 9 , fill : '#acd' , stroke : 'red' } ,
// EndpointStyle: {},
HoverPaintStyle : { stroke : '#ff6d5a' , lineWidth : 4 } ,
EndpointHoverStyle : { fill : '#ff6d5a' , stroke : '#acd' } ,
ConnectionOverlays : [
[
'Arrow' ,
{
location : 1 ,
foldback : 0.7 ,
width : 12 ,
} ,
] ,
[
'Label' ,
{
id : 'drop-add-node' ,
label : 'Drop connection<br />to create node' ,
cssClass : 'drop-add-node-label' ,
location : 0.5 ,
} ,
] ,
] ,
Container : '#node-view' ,
} ) ;
this . instance . bind ( 'connectionAborted' , ( info ) => {
// Get the node and set it as active that new nodes
// which get created get automatically connected
// to it.
const sourceNodeName = this . $store . getters . getNodeNameByIndex ( info . sourceId . slice ( NODE _NAME _PREFIX . length ) ) ;
this . $store . commit ( 'setLastSelectedNode' , sourceNodeName ) ;
// Display the node-creator
this . createNodeActive = true ;
} ) ;
this . instance . bind ( 'connection' , ( info : OnConnectionBindInfo ) => {
2019-06-27 02:27:02 -07:00
// @ts-ignore
const sourceInfo = info . sourceEndpoint . getParameters ( ) ;
// @ts-ignore
const targetInfo = info . targetEndpoint . getParameters ( ) ;
const sourceNodeName = this . $store . getters . getNodeNameByIndex ( sourceInfo . nodeIndex ) ;
const targetNodeName = this . $store . getters . getNodeNameByIndex ( targetInfo . nodeIndex ) ;
const sourceNode = this . $store . getters . nodeByName ( sourceNodeName ) ;
2019-06-23 03:35:23 -07:00
// TODO: That should happen after each move (only the setConnector part)
2019-07-25 09:50:45 -07:00
if ( info . sourceEndpoint . anchor . lastReturnValue [ 0 ] >= info . targetEndpoint . anchor . lastReturnValue [ 0 ] ) {
// When the source is before the target it will make sure that
2019-06-23 03:35:23 -07:00
// the connection is clearer visible
// Use the Flowchart connector if the source is underneath the target
// so that the connection is properly visible
info . connection . setConnector ( [ 'Flowchart' , { cornerRadius : 15 } ] ) ;
// TODO: Location should be dependent on distance. The closer together
// the further away from the center
info . connection . addOverlay ( [
'Arrow' ,
{
location : 0.55 ,
foldback : 0.7 ,
width : 12 ,
} ,
] ) ;
// Change also the color to give an additional visual hint
info . connection . setPaintStyle ( { strokeWidth : 2 , stroke : '#334455' } ) ;
2019-07-25 09:50:45 -07:00
} else if ( Math . abs ( info . sourceEndpoint . anchor . lastReturnValue [ 1 ] - info . targetEndpoint . anchor . lastReturnValue [ 1 ] ) < 30 ) {
2019-06-23 03:35:23 -07:00
info . connection . setConnector ( [ 'Straight' ] ) ;
}
// Display the connection-delete button only on hover
let timer : NodeJS . Timeout | undefined ;
info . connection . bind ( 'mouseover' , ( connection : IConnection ) => {
if ( timer !== undefined ) {
clearTimeout ( timer ) ;
}
const overlay = info . connection . getOverlay ( 'remove-connection' ) ;
overlay . setVisible ( true ) ;
} ) ;
info . connection . bind ( 'mouseout' , ( connection : IConnection ) => {
timer = setTimeout ( ( ) => {
const overlay = info . connection . getOverlay ( 'remove-connection' ) ;
overlay . setVisible ( false ) ;
timer = undefined ;
} , 500 ) ;
} ) ;
// @ts-ignore
info . connection . removeOverlay ( 'drop-add-node' ) ;
// @ts-ignore
info . connection . addOverlay ( [
'Label' ,
{
id : 'remove-connection' ,
label : '<span class="delete-connection clickable" title="Delete Connection">x</span>' ,
cssClass : 'remove-connection-label' ,
visible : false ,
events : {
mousedown : ( ) => {
this . _ _removeConnectionByConnectionInfo ( info , true ) ;
} ,
} ,
} ,
] ) ;
2019-06-27 02:27:02 -07:00
// Display output names if they exist on connection
const sourceNodeTypeData : INodeTypeDescription = this . $store . getters . nodeType ( sourceNode . type ) ;
if ( sourceNodeTypeData . outputNames !== undefined ) {
for ( const output of sourceNodeTypeData . outputNames ) {
const outputName = sourceNodeTypeData . outputNames [ sourceInfo . index ] ;
if ( info . connection . getOverlay ( 'output-name-label' ) ) {
// Make sure that it does not get added multiple times
continue ;
}
// @ts-ignore
info . connection . addOverlay ( [
'Label' ,
{
id : 'output-name-label' ,
label : outputName ,
cssClass : 'connection-output-name-label' ,
location : 0.3 ,
} ,
] ) ;
}
}
// When connection gets made the output name get displayed as overlay
// on the connection. So the one on the endpoint can be hidden.
2019-06-23 03:35:23 -07:00
// @ts-ignore
2019-06-27 02:27:02 -07:00
const outputNameOverlay = info . connection . endpoints [ 0 ] . getOverlay ( 'output-name-label' ) ;
2019-06-27 04:00:52 -07:00
if ( ! [ null , undefined ] . includes ( outputNameOverlay ) ) {
2019-06-27 02:27:02 -07:00
outputNameOverlay . setVisible ( false ) ;
}
2019-06-23 03:35:23 -07:00
this . $store . commit ( 'addConnection' , {
connection : [
{
2019-06-27 02:27:02 -07:00
node : sourceNodeName ,
2019-06-23 03:35:23 -07:00
type : sourceInfo . type ,
index : sourceInfo . index ,
} ,
{
2019-06-27 02:27:02 -07:00
node : targetNodeName ,
2019-06-23 03:35:23 -07:00
type : targetInfo . type ,
index : targetInfo . index ,
} ,
] ,
} ) ;
} ) ;
this . instance . bind ( 'connectionMoved' , ( info ) => {
// When a connection gets moved from one node to another it for some reason
// calls the "connection" event but not the "connectionDetached" one. So we listen
// additionally to the "connectionMoved" event and then only delete the existing connection.
// @ts-ignore
const sourceInfo = info . originalSourceEndpoint . getParameters ( ) ;
// @ts-ignore
const targetInfo = info . originalTargetEndpoint . getParameters ( ) ;
const connectionInfo = [
{
node : this . $store . getters . getNodeNameByIndex ( sourceInfo . nodeIndex ) ,
type : sourceInfo . type ,
index : sourceInfo . index ,
} ,
{
node : this . $store . getters . getNodeNameByIndex ( targetInfo . nodeIndex ) ,
type : targetInfo . type ,
index : targetInfo . index ,
} ,
] as [ IConnection , IConnection ] ;
this . _ _removeConnection ( connectionInfo , false ) ;
// Make sure to remove the overlay else after the second move
// it visibly stays behind free floating without a connection.
2019-06-27 02:27:02 -07:00
info . connection . removeOverlays ( ) ;
2019-06-23 03:35:23 -07:00
} ) ;
2019-06-27 02:27:02 -07:00
2019-06-23 03:35:23 -07:00
this . instance . bind ( 'connectionDetached' , ( info ) => {
2019-06-27 02:27:02 -07:00
const sourceEndpoint = info . connection . endpoints [ 0 ] ;
// If the source endpoint is not connected to anything else anymore
// display the output-name overlays on the endpoint if any exist
if ( sourceEndpoint !== undefined && sourceEndpoint . connections ! . length === 1 ) {
const outputNameOverlay = sourceEndpoint . getOverlay ( 'output-name-label' ) ;
2019-06-27 04:00:52 -07:00
if ( ! [ null , undefined ] . includes ( outputNameOverlay ) ) {
2019-06-27 02:27:02 -07:00
outputNameOverlay . setVisible ( true ) ;
}
}
info . connection . removeOverlays ( ) ;
2019-06-23 03:35:23 -07:00
this . _ _removeConnectionByConnectionInfo ( info , false ) ;
} ) ;
} ,
async newWorkflow ( ) : Promise < void > {
await this . resetWorkspace ( ) ;
// Create start node
const defaultNodes = [
{
name : 'Start' ,
type : 'n8n-nodes-base.start' ,
typeVersion : 1 ,
position : [
100 ,
2019-07-25 10:23:27 -07:00
300 ,
2019-06-23 03:35:23 -07:00
] as XYPositon ,
parameters : { } ,
} ,
] ;
await this . addNodes ( defaultNodes ) ;
} ,
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
return Promise . resolve ( ) ;
}
if ( this . $route . name === 'ExecutionById' ) {
// Load an execution
const executionId = this . $route . params . id ;
await this . openExecution ( executionId ) ;
} else {
// Load a workflow
let workflowId = null as string | null ;
if ( this . $route . params . name ) {
workflowId = this . $route . params . name ;
}
if ( workflowId !== null ) {
// Open existing workflow
await this . openWorkflow ( workflowId ) ;
} else {
// Create new workflow
await this . newWorkflow ( ) ;
}
}
document . addEventListener ( 'keydown' , this . keyDown ) ;
document . addEventListener ( 'keyup' , this . keyUp ) ;
} ,
_ _addConnection ( connection : [ IConnection , IConnection ] , addVisualConnection = false ) {
if ( addVisualConnection === true ) {
const uuid : [ string , string ] = [
2019-07-25 09:50:45 -07:00
` ${ this . getNodeIndex ( connection [ 0 ] . node ) } -output ${ connection [ 0 ] . index } ` ,
` ${ this . getNodeIndex ( connection [ 1 ] . node ) } -input ${ connection [ 1 ] . index } ` ,
2019-06-23 03:35:23 -07:00
] ;
// Create connections in DOM
// @ts-ignore
this . instance . connect ( {
uuids : uuid ,
} ) ;
} else {
// When nodes get connected it gets saved automatically to the storage
// so if we do not connect we have to save the connection manually
this . $store . commit ( 'addConnection' , { connection } ) ;
}
} ,
_ _removeConnection ( connection : [ IConnection , IConnection ] , removeVisualConnection = false ) {
if ( removeVisualConnection === true ) {
// @ts-ignore
const connections = this . instance . getConnections ( {
source : NODE _NAME _PREFIX + this . getNodeIndex ( connection [ 0 ] . node ) ,
target : NODE _NAME _PREFIX + this . getNodeIndex ( connection [ 1 ] . node ) ,
} ) ;
// @ts-ignore
connections . forEach ( ( connectionInstance ) => {
this . instance . deleteConnection ( connectionInstance ) ;
} ) ;
}
this . $store . commit ( 'removeConnection' , { connection } ) ;
} ,
_ _removeConnectionByConnectionInfo ( info : OnConnectionBindInfo , removeVisualConnection = false ) {
// @ts-ignore
const sourceInfo = info . sourceEndpoint . getParameters ( ) ;
// @ts-ignore
const targetInfo = info . targetEndpoint . getParameters ( ) ;
const connectionInfo = [
{
node : this . $store . getters . getNodeNameByIndex ( sourceInfo . nodeIndex ) ,
type : sourceInfo . type ,
index : sourceInfo . index ,
} ,
{
node : this . $store . getters . getNodeNameByIndex ( targetInfo . nodeIndex ) ,
type : targetInfo . type ,
index : targetInfo . index ,
} ,
] as [ IConnection , IConnection ] ;
this . _ _removeConnection ( connectionInfo , removeVisualConnection ) ;
} ,
async duplicateNode ( nodeName : string ) {
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
const node = this . $store . getters . nodeByName ( nodeName ) ;
const nodeTypeData : INodeTypeDescription = this . $store . getters . nodeType ( node . type ) ;
if ( nodeTypeData . maxNodes !== undefined && this . getNodeTypeCount ( node . type ) >= nodeTypeData . maxNodes ) {
this . showMaxNodeTypeError ( nodeTypeData ) ;
return ;
}
// Deep copy the data so that data on lower levels of the node-properties do
// not share objects
const newNodeData = JSON . parse ( JSON . stringify ( this . getNodeDataToSave ( node ) ) ) ;
// Check if node-name is unique else find one that is
newNodeData . name = this . getUniqueNodeName ( newNodeData . name ) ;
newNodeData . position = this . getNewNodePosition (
2019-07-25 22:17:40 -07:00
[ node . position [ 0 ] , node . position [ 1 ] + 150 ] ,
[ 0 , 150 ]
2019-06-23 03:35:23 -07:00
) ;
await this . addNodes ( [ newNodeData ] ) ;
// Automatically deselect all nodes and select the current one and also active
// current node
this . deselectAllNodes ( ) ;
setTimeout ( ( ) => {
this . nodeSelectedByName ( newNodeData . name , true ) ;
} ) ;
} ,
removeNode ( nodeName : string ) {
if ( this . editAllowedCheck ( ) === false ) {
return ;
}
const node = this . $store . getters . nodeByName ( nodeName ) ;
// "requiredNodeTypes" are also defined in cli/commands/run.ts
const requiredNodeTypes = [ 'n8n-nodes-base.start' ] ;
if ( requiredNodeTypes . includes ( node . type ) ) {
// The node is of the required type so check first
// if any node of that type would be left when the
// current one would get deleted.
let deleteAllowed = false ;
for ( const checkNode of this . nodes ) {
if ( checkNode . name === node . name ) {
continue ;
}
if ( requiredNodeTypes . includes ( checkNode . type ) ) {
deleteAllowed = true ;
break ;
}
}
if ( deleteAllowed === false ) {
return ;
}
}
const nodeIndex = this . $store . getters . getNodeIndex ( nodeName ) ;
const nodeIdName = ` node- ${ nodeIndex } ` ;
// Suspend drawing
this . instance . setSuspendDrawing ( true ) ;
// Remove all endpoints and the connections in jsplumb
this . instance . removeAllEndpoints ( nodeIdName ) ;
// Remove the draggable
// @ts-ignore
this . instance . destroyDraggable ( nodeIdName ) ;
// Remove the connections in data
this . $store . commit ( 'removeAllNodeConnection' , node ) ;
this . $store . commit ( 'removeNode' , node ) ;
// Now it can draw again
this . instance . setSuspendDrawing ( false , true ) ;
// Remove node from selected index if found in it
this . $store . commit ( 'removeNodeFromSelection' , node ) ;
// Remove from node index
if ( nodeIndex !== - 1 ) {
this . $store . commit ( 'setNodeIndex' , { index : nodeIndex , name : null } ) ;
}
} ,
valueChanged ( parameterData : IUpdateInformation ) {
if ( parameterData . name === 'name' && parameterData . oldValue ) {
// The name changed so we have to take care that
// the connections get changed.
this . renameNode ( parameterData . oldValue as string , parameterData . value as string ) ;
}
} ,
async renameNodePrompt ( currentName : string ) {
try {
2019-07-24 06:21:44 -07:00
const promptResponsePromise = this . $prompt ( 'New Name:' , ` Rename Node: " ${ currentName } " ` , {
customClass : 'rename-prompt' ,
2019-06-23 03:35:23 -07:00
confirmButtonText : 'Rename' ,
cancelButtonText : 'Cancel' ,
inputErrorMessage : 'Invalid Name' ,
inputValue : currentName ,
} ) ;
2019-07-24 06:21:44 -07:00
// Wait till it had time to display
await Vue . nextTick ( ) ;
// Get the input and select the text in it
const nameInput = document . querySelector ( '.rename-prompt .el-input__inner' ) as HTMLInputElement | undefined ;
if ( nameInput ) {
nameInput . focus ( ) ;
nameInput . select ( ) ;
}
const promptResponse = await promptResponsePromise ;
2019-06-23 03:35:23 -07:00
this . renameNode ( currentName , promptResponse . value ) ;
} catch ( e ) { }
} ,
async renameNode ( currentName : string , newName : string ) {
if ( currentName === newName ) {
return ;
}
// Check if node-name is unique else find one that is
newName = this . getUniqueNodeName ( newName ) ;
// Rename the node and update the connections
const workflow = this . getWorkflow ( true ) ;
workflow . renameNode ( currentName , newName ) ;
// Update also last selected node and exeuction data
this . $store . commit ( 'renameNodeSelectedAndExecution' , { old : currentName , new : newName } ) ;
// Reset all nodes and connections to load the new ones
if ( this . instance ) {
// On first load it does not exist
this . instance . deleteEveryEndpoint ( ) ;
}
this . $store . commit ( 'removeAllConnections' ) ;
this . $store . commit ( 'removeAllNodes' ) ;
// 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
} ,
async addNodes ( nodes : INodeUi [ ] , connections ? : IConnections ) {
if ( ! nodes || ! nodes . length ) {
return ;
}
// Add the node to the node-list
let nodeType : INodeTypeDescription | null ;
let foundNodeIssues : INodeIssues | null ;
nodes . forEach ( ( node ) => {
nodeType = this . $store . getters . nodeType ( node . type ) ;
// Make sure that some properties always exist
if ( ! node . hasOwnProperty ( 'disabled' ) ) {
node . disabled = false ;
}
if ( ! node . hasOwnProperty ( 'color' ) ) {
// If no color is defined set the default color of the node type
if ( nodeType && nodeType . defaults . color ) {
node . color = nodeType . defaults . color as string ;
}
}
if ( ! node . hasOwnProperty ( 'parameters' ) ) {
node . parameters = { } ;
}
// Load the defaul parameter values because only values which differ
// from the defaults get saved
if ( nodeType !== null ) {
let nodeParameters = null ;
try {
2019-07-14 05:10:16 -07:00
nodeParameters = NodeHelpers . getNodeParameters ( nodeType . properties , node . parameters , true , false ) ;
2019-06-23 03:35:23 -07:00
} catch ( e ) {
console . error ( ` There was a problem loading the node-parameters of node: " ${ node . name } " ` ) ;
console . error ( e ) ;
}
node . parameters = nodeParameters !== null ? nodeParameters : { } ;
}
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 ( (
targetData
) => {
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 ( ) ;
let oldName ;
const createNodes : INode [ ] = [ ] ;
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 ;
node . name = this . getUniqueNodeName ( node . name , newNodeNames ) ;
newNodeNames . push ( node . name ) ;
nodeNameTable [ oldName ] = node . name ;
createNodes . push ( node ) ;
} ) ;
data . nodes = createNodes ;
const nameData = { old : '' , new : '' } ;
for ( oldName of Object . keys ( nodeNameTable ) ) {
nameData . old = oldName ;
nameData . new = nodeNameTable [ oldName ] ;
// More or less identical to "renameNode" in "Workflow.ts"
if (
nameData . old !== nameData . new &&
data . connections &&
data . connections . hasOwnProperty ( nameData . old )
) {
data . connections [ nameData . new ] = data . connections [ nameData . old ] ;
delete data . connections [ nameData . old ] ;
}
// Rename all destination connections
let sourceNode , type , sourceIndex , connectionIndex , connectionData ;
if ( data . connections ) {
for ( sourceNode of Object . keys ( data . connections ) ) {
for ( type of Object . keys ( data . connections [ sourceNode ] ) ) {
for ( sourceIndex = 0 ; sourceIndex < data . connections [ sourceNode ] [ type ] . length ; sourceIndex ++ ) {
for ( connectionIndex = 0 ; connectionIndex < data . connections [ sourceNode ] [ type ] [ sourceIndex ] . length ; connectionIndex ++ ) {
connectionData = data . connections [ sourceNode ] [ type ] [ sourceIndex ] [ connectionIndex ] ;
if ( connectionData . node === nameData . old ) {
connectionData . node = nameData . new ;
}
}
}
}
}
}
}
await this . addNodes ( data . nodes , data . connections ) ;
return data ;
} ,
getSelectedNodesToSave ( ) : Promise < IWorkflowData > {
const data : IWorkflowData = {
nodes : [ ] ,
connections : { } ,
} ;
// Get data of all the selected noes
let nodeData ;
const exportNodeNames : string [ ] = [ ] ;
for ( const node of this . $store . getters . getSelectedNodes ) {
try {
nodeData = this . getNodeDataToSave ( node ) ;
exportNodeNames . push ( node . name ) ;
} catch ( e ) {
return Promise . reject ( e ) ;
}
data . nodes . push ( nodeData ) ;
}
// Get only connections of exported nodes and ignore all other ones
let connectionToKeep ,
connections : INodeConnections ,
type : string ,
connectionIndex : number ,
sourceIndex : number ,
connectionData : IConnection ,
typeConnections : INodeConnections ;
data . nodes . forEach ( ( node ) => {
connections = this . $store . getters . connectionsByNodeName ( node . name ) ;
if ( Object . keys ( connections ) . length === 0 ) {
return ;
}
// Keep only the connection to node which get also exported
// @ts-ignore
typeConnections = { } ;
for ( type of Object . keys ( connections ) ) {
for ( sourceIndex = 0 ; sourceIndex < connections [ type ] . length ; sourceIndex ++ ) {
connectionToKeep = [ ] ;
for ( connectionIndex = 0 ; connectionIndex < connections [ type ] [ sourceIndex ] . length ; connectionIndex ++ ) {
connectionData = connections [ type ] [ sourceIndex ] [ connectionIndex ] ;
if ( exportNodeNames . indexOf ( connectionData . node ) !== - 1 ) {
connectionToKeep . push ( connectionData ) ;
}
}
if ( connectionToKeep . length ) {
if ( ! typeConnections . hasOwnProperty ( type ) ) {
typeConnections [ type ] = [ ] ;
}
typeConnections [ type ] [ sourceIndex ] = connectionToKeep ;
}
}
}
if ( Object . keys ( typeConnections ) . length ) {
data . connections [ node . name ] = typeConnections ;
}
} ) ;
return Promise . resolve ( data ) ;
} ,
resetWorkspace ( ) {
// Reset nodes
if ( this . instance ) {
// On first load it does not exist
this . instance . deleteEveryEndpoint ( ) ;
}
if ( this . executionWaitingForWebhook === true ) {
// Make sure that if there is a waiting test-webhook that
// it gets removed
this . restApi ( ) . removeTestWebhook ( this . $store . getters . workflowId )
. catch ( ( ) => {
// Ignore all errors
} ) ;
}
this . $store . commit ( 'removeAllConnections' ) ;
this . $store . commit ( 'removeAllNodes' ) ;
// 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 ) ;
this . $store . commit ( 'setWorkflowName' , '' ) ;
this . $store . commit ( 'setWorkflowSettings' , { } ) ;
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' ) ;
this . $store . commit ( 'setNodeViewOffsetPosition' , [ 0 , 0 ] ) ;
return Promise . resolve ( ) ;
} ,
async loadActiveWorkflows ( ) : Promise < void > {
const activeWorkflows = await this . restApi ( ) . getActiveWorkflows ( ) ;
this . $store . commit ( 'setActiveWorkflows' , activeWorkflows ) ;
} ,
async loadSettings ( ) : Promise < void > {
const settings = await this . restApi ( ) . getSettings ( ) as IN8nUISettings ;
2019-07-10 11:53:13 -07:00
2019-06-23 03:35:23 -07:00
this . $store . commit ( 'setUrlBaseWebhook' , settings . urlBaseWebhook ) ;
this . $store . commit ( 'setEndpointWebhook' , settings . endpointWebhook ) ;
this . $store . commit ( 'setEndpointWebhookTest' , settings . endpointWebhookTest ) ;
2019-07-10 11:53:13 -07:00
this . $store . commit ( 'setSaveDataErrorExecution' , settings . saveDataErrorExecution ) ;
this . $store . commit ( 'setSaveDataSuccessExecution' , settings . saveDataSuccessExecution ) ;
2019-07-10 09:06:26 -07:00
this . $store . commit ( 'setSaveManualExecutions' , settings . saveManualExecutions ) ;
2019-06-23 03:35:23 -07:00
this . $store . commit ( 'setTimezone' , settings . timezone ) ;
} ,
async loadNodeTypes ( ) : Promise < void > {
const nodeTypes = await this . restApi ( ) . getNodeTypes ( ) ;
this . $store . commit ( 'setNodeTypes' , nodeTypes ) ;
} ,
async loadCredentialTypes ( ) : Promise < void > {
const credentialTypes = await this . restApi ( ) . getCredentialTypes ( ) ;
this . $store . commit ( 'setCredentialTypes' , credentialTypes ) ;
} ,
async loadCredentials ( ) : Promise < void > {
const credentials = await this . restApi ( ) . getAllCredentials ( ) ;
this . $store . commit ( 'setCredentials' , credentials ) ;
} ,
} ,
async mounted ( ) {
this . $root . $on ( 'importWorkflowData' , async ( data : IDataObject ) => {
await this . importWorkflowData ( data . data as IWorkflowDataUpdate ) ;
} ) ;
this . $root . $on ( 'importWorkflowUrl' , async ( data : IDataObject ) => {
const workflowData = await this . getWorkflowDataFromUrl ( data . url as string ) ;
if ( workflowData !== undefined ) {
await this . importWorkflowData ( workflowData ) ;
}
} ) ;
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 ( ) ;
} ) ;
} ,
destroyed ( ) {
this . resetWorkspace ( ) ;
} ,
} ) ;
< / script >
< style scoped lang = "scss" >
. zoom - menu {
position : fixed ;
left : 70 px ;
width : 200 px ;
bottom : 45 px ;
line - height : 25 px ;
z - index : 18 ;
color : # 444 ;
padding - right : 5 px ;
}
. node - creator - button {
position : fixed ;
text - align : center ;
top : 80 px ;
right : 20 px ;
z - index : 10 ;
}
. node - creator - button button {
position : relative ;
background : $ -- color - primary ;
font - size : 1.4 em ;
color : # fff ;
}
. node - creator - button : hover button {
transform : scale ( 1.05 ) ;
}
. 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 % ;
}
. node - view - background {
position : absolute ;
width : 10000 px ;
height : 10000 px ;
top : - 5000 px ;
left : - 5000 px ;
background - size : 50 px 50 px ;
background - image : linear - gradient ( to right , # eeeefe 1 px , transparent 1 px ) , linear - gradient ( to bottom , # eeeefe 1 px , transparent 1 px ) ;
}
. move - active {
cursor : grab ;
cursor : - moz - grab ;
cursor : - webkit - grab ;
touch - action : none ;
}
. move - in - process {
cursor : grabbing ;
cursor : - moz - grabbing ;
cursor : - webkit - grabbing ;
touch - action : none ;
}
. workflow - execute - wrapper {
position : fixed ;
line - height : 65 px ;
left : calc ( 50 % - 150 px ) ;
bottom : 30 px ;
width : 300 px ;
text - align : center ;
. run - icon {
display : inline - block ;
transform : scale ( 1.4 ) ;
margin - right : 0.5 em ;
}
. workflow - run - button {
padding : 12 px ;
}
. stop - execution ,
. workflow - run - button . running {
color : $ -- color - primary ;
background - color : $ -- color - primary - light ;
}
}
/* Makes sure that when selected with mouse it does not select text */
. do - not - select * ,
. jtk - drag - select * {
- webkit - touch - callout : none ;
- webkit - user - select : none ;
- khtml - user - select : none ;
- moz - user - select : none ;
- ms - user - select : none ;
user - select : none ;
}
< / style >
< style lang = "scss" >
2019-06-27 02:27:02 -07:00
. connection - output - name - label {
background - color : $ -- custom - node - view - background ;
line - height : 1.3 em ;
font - size : 0.8 em ;
}
2019-07-25 12:57:27 -07:00
. delete - connection {
font - weight : 500 ;
}
2019-06-23 03:35:23 -07:00
. remove - connection - label {
font - size : 12 px ;
color : # fff ;
line - height : 13 px ;
border - radius : 15 px ;
height : 15 px ;
background - color : # 334455 ;
position : relative ;
height : 15 px ;
width : 15 px ;
text - align : center ;
& : hover {
background - color : $ -- color - primary ;
font - size : 20 px ;
line - height : 17 px ;
height : 20 px ;
width : 20 px ;
}
}
. drop - add - node - label {
color : # 555 ;
font - weight : 600 ;
font - size : 0.8 em ;
text - align : center ;
background - color : # ffffff55 ;
}
. node - endpoint - label {
font - size : 10 px ;
2019-06-27 02:27:02 -07:00
background - color : $ -- custom - node - view - background ;
2019-06-23 03:35:23 -07:00
}
. button - white {
border : none ;
padding : 0.3 em ;
margin : 0 0.1 em ;
border - radius : 3 px ;
font - size : 1.2 em ;
background : # fff ;
width : 40 px ;
height : 40 px ;
color : # 666 ;
cursor : pointer ;
& : hover {
transform : scale ( 1.1 ) ;
}
}
< / style >