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-12-07 08:28:10 -08:00
< div v-if ="!createNodeActive && !isReadOnly" class="node-creator-button" :title="$i.baseText('nodeView.addNode')" @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-12-07 08:28:10 -08:00
< button @click ="zoomToFit" class = "button-white" :title ="$i.baseText('nodeView.zoomToFit')" >
2021-06-22 10:33:07 -07:00
< font -awesome -icon icon = "expand" / >
< / button >
2021-12-07 08:28:10 -08:00
< button @click ="zoomIn()" class = "button-white" :title ="$i.baseText('nodeView.zoomIn')" >
2019-06-23 03:35:23 -07:00
< font -awesome -icon icon = "search-plus" / >
< / button >
2021-12-07 08:28:10 -08:00
< button @click ="zoomOut()" class = "button-white" :title ="$i.baseText('nodeView.zoomOut')" >
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"
2021-12-07 08:28:10 -08:00
: title = "$i.baseText('nodeView.resetZoom')"
2019-06-23 03:35:23 -07:00
>
2021-12-07 08:28:10 -08:00
< font -awesome -icon icon = "undo" :title ="$i.baseText('nodeView.resetZoom')" / >
2019-06-23 03:35:23 -07:00
< / 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"
2021-12-07 08:28:10 -08:00
: title = "$i.baseText('nodeView.executesTheWorkflowFromTheStartOrWebhookNode')"
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"
2021-11-10 10:41:40 -08:00
: title = " stopExecutionInProgress
2021-12-07 08:28:10 -08:00
? $i . baseText ( 'nodeView.stoppingCurrentExecution' )
: $i . baseText ( 'nodeView.stopCurrentExecution' )
2021-11-10 10:41:40 -08:00
"
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"
2021-12-07 08:28:10 -08:00
: title = "$i.baseText('nodeView.stopWaitingForWebhookCall')"
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"
2021-12-07 08:28:10 -08:00
: title = "$i.baseText('nodeView.deletesTheCurrentExecutionData')"
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 06:35:38 -08:00
2021-11-18 02:32:13 -08:00
import {
loadLanguage ,
addNodeTranslation ,
addHeaders ,
2021-12-07 08:46:17 -08:00
} from '@/plugins/i18n' ;
2019-06-23 03:35:23 -07:00
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
} ,
2021-11-09 00:59:48 -08:00
2021-11-18 02:32:13 -08:00
async defaultLocale ( newLocale , oldLocale ) {
2021-11-09 00:59:48 -08:00
loadLanguage ( newLocale ) ;
} ,
2019-06-23 03:35:23 -07:00
} ,
2020-10-25 04:47:49 -07:00
async beforeRouteLeave ( to , from , next ) {
const result = this . $store . getters . getStateIsDirty ;
if ( result ) {
2021-11-10 10:41:40 -08:00
const importConfirm = await this . confirmMessage (
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.confirmMessage.beforeRouteLeave.message' ) ,
this . $i . baseText ( 'nodeView.confirmMessage.beforeRouteLeave.headline' ) ,
2021-11-10 10:41:40 -08:00
'warning' ,
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.confirmMessage.beforeRouteLeave.confirmButtonText' ) ,
this . $i . baseText ( 'nodeView.confirmMessage.beforeRouteLeave.cancelButtonText' ) ,
2021-11-10 10:41:40 -08:00
) ;
2020-10-25 04:47:49 -07:00
if ( importConfirm === false ) {
next ( false ) ;
} else {
// Prevent other popups from displaying
this . $store . commit ( 'setStateDirty' , false ) ;
next ( ) ;
}
} else {
next ( ) ;
}
} ,
2019-06-23 03:35:23 -07:00
computed : {
2021-05-29 11:31:21 -07:00
... mapGetters ( 'ui' , [
'sidebarMenuCollapsed' ,
] ) ,
2021-11-09 00:59:48 -08:00
defaultLocale ( ) : string {
return this . $store . getters . defaultLocale ;
} ,
2021-12-02 08:51:50 -08:00
englishLocale ( ) : boolean {
return this . defaultLocale === 'en' ;
} ,
2021-12-01 01:00:03 -08:00
... mapGetters ( [ 'nativelyNumberSuffixedDefaults' ] ) ,
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 ) {
2021-12-07 08:28:10 -08:00
return this . $i . baseText ( 'nodeView.runButtonText.executeWorkflow' ) ;
2019-06-23 03:35:23 -07:00
}
if ( this . executionWaitingForWebhook === true ) {
2021-12-07 08:28:10 -08:00
return this . $i . baseText ( 'nodeView.runButtonText.waitingForTriggerEvent' ) ;
2019-06-23 03:35:23 -07:00
}
2021-12-07 08:28:10 -08:00
return this . $i . baseText ( 'nodeView.runButtonText.executingWorkflow' ) ;
2019-06-23 03:35:23 -07:00
} ,
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-12-02 08:51:50 -08:00
translateName ( type : string , originalName : string ) {
2021-12-07 08:28:10 -08:00
return this . $i . headerText ( {
key : ` headers. ${ this . $i . shortNodeType ( type ) } .displayName ` ,
2021-12-02 08:51:50 -08:00
fallback : originalName ,
} ) ;
} ,
getUniqueNodeName ( {
originalName ,
additionalUsedNames = [ ] ,
type = '' ,
} : {
originalName : string ,
additionalUsedNames ? : string [ ] ,
type ? : string ,
} ) {
const allNodeNamesOnCanvas = this . $store . getters . allNodes . map ( ( n : INodeUi ) => n . name ) ;
originalName = this . englishLocale ? originalName : this . translateName ( type , originalName ) ;
if (
! allNodeNamesOnCanvas . includes ( originalName ) &&
! additionalUsedNames . includes ( originalName )
) {
return originalName ; // already unique
}
let natives : string [ ] = this . nativelyNumberSuffixedDefaults ;
natives = this . englishLocale ? natives : natives . map ( name => {
const type = name . toLowerCase ( ) . replace ( '_' , '' ) ;
return this . translateName ( type , name ) ;
} ) ;
const found = natives . find ( ( n ) => originalName . startsWith ( n ) ) ;
let ignore , baseName , nameIndex , uniqueName ;
let index = 1 ;
if ( found ) {
// name natively ends with number
nameIndex = originalName . split ( found ) . pop ( ) ;
if ( nameIndex ) {
index = parseInt ( nameIndex , 10 ) ;
}
baseName = uniqueName = originalName ;
} else {
const nameMatch = originalName . match ( /(.*\D+)(\d*)/ ) ;
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 = originalName ;
}
}
while (
allNodeNamesOnCanvas . includes ( uniqueName ) ||
additionalUsedNames . includes ( uniqueName )
) {
uniqueName = baseName + ( index ++ ) ;
}
return uniqueName ;
} ,
:zap: Add Value Survey (#2499)
* N8N-2619 Value Survey Front-end
* N8N-2619 Added Contact Prompt Modal and logic
* N8N-2619 Added Link to Toast Message on Successful submitting ValueSurvey
* N8N-2619 Updated TypeForm URL in ValueSurvey Success Toast
* N8N-2619 Fixed Typo placeholder for ValueSurvey and ContactPrompt Modal
* N8N-2619 Fixed Toast not close automatically in ValueSurvey, Make part of the title bold, Changed Font-sizes on Value Survey
* N8N-2619 Fixed Close Button on ValueSurvey, Vertical Allignment for Questions in ValueSurvey Drawer
* N8N-2619 Make Value Survey with static height
* N8N-2619 Fixed Telemetry Events on closing ValueSurvey
* N8N-2619 Updated N8NPrompt Interface, Added Dynamic Title and Description on ContactPrompt Modal
* N8N-2619 Reversed Answers in ValueSurveyModal
* N8N-2619 Added Telemetry Event on user close ValueSurvey on second Question
* N8N-2619 Re-work, Optimized, Simplify the code after technical review
* N8N-2619 Fixed If else statement in openUserPromptsIfPossible
* N8N-2619 Change Text under Email Box - ValueSurvey, ContactPrompt, Added new Telemetary Event on ValueSurvey Open, Fixed Toast to close aftet 15s
* N8N-2619 Change ContactPrompt Modal to use Atoms like N8N-Heading and N8N-Text
* N8N-2619 Change Design & Logic on ValueSurvey - When to open
* N8N-2619 Updated Value Survey with new Telemetry Events (Refactor), Simplified functions, Added Atoms in ValueSurvey + ContactPrompt
* N8N-2619 Refactor in Interfaces, Updated/Refactor Getters and Vuex store props
* N8N-2619 Defined IN8nValueSurveyData interface
* N8N-2619 Disabled Keyboard shortcuts to be activated on typing in ValueSurvey Input field, Fire an event on Saving WF from Menu and with shorcut, Make Drawer keep-alive
* N8N-2619 Added Atoms in Value Survey Modal (buttons), Rework css
* N8N-2619 Added Responses on ValueSurvey Submit
* N8N-2619 Added Response for SubmittingContactInfo
* N8N-2619 Added loading state for buttons / ValueSurvey
* N8N-2619 Changed ValueSurvey and ContactPrompt to support enter key on submit, Simplifed closeDialog Function, Changed css for button in ValueSurvey, Prevent showing the Modals if Saving WF fails, Add Debouncing on showing prompt
* N8N-2619 Added IsTelemetryEnabled Getter in Vuex.store
* N8N-2619 Created/Added N8N-SquareButton to Design-system
* N8N-2619 Change Promise in MainSideBar with Async/Await function, Nitpick simpliefied
* N8N-2619 Update the text under the input fields
* N8N-2619 Update the text in ContactPrompt Modal
* N8N-2619 Allign Send button on ValueSurvey Modal
* N8N-2619 Fixed Input in ValueSurvey Modal
* N8N-2619 Check if the workflow is saving
* N8N-2619 Check if WF is saving to allowed performs fetchPromptsData
* N8N-2619 Hotfix
* N8N-2619 Fixed ValueSurvey, Updated onSaveButtonClick function, Created onSaveKeyboardShortcut function in NodeView
* N8N-2619 Rework css module classes for ValueSurvey, Simplified
* N8N-2619 Simplified N8N-SquareButton Component, removed dead code
* N8N-2619 Added Breakpoints for Mobile/Tablet View
* N8N-2619 Formatting fix
* N8N-2619 Update css for mobile/tablet, change promises to asyn/await functions in ContactPrompt and ValueSurvey, Added isActive prop to ValueSurvey
* N8N-2619 Update TEMPLATE_BASE_URL to production
2021-12-11 08:38:16 -08:00
async onSaveKeyboardShortcut ( ) {
const saved = await this . saveCurrentWorkflow ( ) ;
if ( saved ) this . $store . dispatch ( 'settings/fetchPromptsData' ) ;
} ,
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 ) {
2021-11-10 10:41:40 -08:00
this . $showError (
error ,
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.showError.openExecution.title' ) ,
this . $i . baseText ( 'nodeView.showError.openExecution.message' ) + ':' ,
2021-11-10 10:41:40 -08:00
) ;
2019-06-23 03:35:23 -07:00
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 ( {
2021-12-07 08:28:10 -08:00
title : this . $i . baseText ( 'nodeView.thisExecutionHasntFinishedYet' ) ,
message : ` <a onclick="window.location.reload(false);"> ${ this . $i . baseText ( 'nodeView.refresh' ) } </a> ${ this . $i . baseText ( 'nodeView.toSeeTheLatestStatus' ) } .<br/> <a href="https://docs.n8n.io/nodes/n8n-nodes-base.wait/" target="_blank"> ${ this . $i . baseText ( 'nodeView.moreInfo' ) } </a> ` ,
2021-09-22 00:23:37 -07:00
type : 'warning' ,
duration : 0 ,
} ) ;
}
2019-06-23 03:35:23 -07:00
} ,
2021-06-22 10:33:07 -07:00
async openWorkflowTemplate ( templateId : string ) {
2021-12-07 08:28:10 -08:00
this . setLoadingText ( this . $i . baseText ( 'nodeView.loadingTemplate' ) ) ;
2021-06-22 10:33:07 -07:00
this . resetWorkspace ( ) ;
let data : IWorkflowTemplate | undefined ;
try {
this . $externalHooks ( ) . run ( 'template.requested' , { templateId } ) ;
data = await this . $store . dispatch ( 'workflows/getWorkflowTemplate' , templateId ) ;
if ( ! data ) {
2021-11-10 10:41:40 -08:00
throw new Error (
2021-12-07 08:28:10 -08:00
this . $i . baseText (
2021-11-10 10:41:40 -08:00
'nodeView.workflowTemplateWithIdCouldNotBeFound' ,
{ interpolate : { templateId } } ,
) ,
) ;
2021-06-22 10:33:07 -07:00
}
data . workflow . nodes . forEach ( ( node ) => {
if ( ! this . $store . getters . nodeType ( node . type ) ) {
2021-12-07 08:28:10 -08:00
throw new Error ( ` The ${ this . $i . shortNodeType ( node . type ) } node is not supported ` ) ;
2021-06-22 10:33:07 -07:00
}
} ) ;
} catch ( error ) {
2021-12-07 08:28:10 -08:00
this . $showError ( error , this . $i . baseText ( 'nodeView.couldntImportWorkflow' ) ) ;
2021-06-22 10:33:07 -07:00
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 ) {
2021-11-10 10:41:40 -08:00
this . $showError (
error ,
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.showError.openWorkflow.title' ) ,
this . $i . baseText ( 'nodeView.showError.openWorkflow.message' ) + ':' ,
2021-11-10 10:41:40 -08:00
) ;
2019-06-23 03:35:23 -07:00
return ;
}
if ( data === undefined ) {
2021-11-10 10:41:40 -08:00
throw new Error (
2021-12-07 08:28:10 -08:00
this . $i . baseText (
2021-11-10 10:41:40 -08:00
'nodeView.workflowWithIdCouldNotBeFound' ,
{ interpolate : { workflowId } } ,
) ,
) ;
2019-06-23 03:35:23 -07:00
}
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-12-07 08:28:10 -08:00
title : this . $i . baseText ( 'nodeView.showMessage.keyDown.title' ) ,
message : this . $i . baseText ( 'nodeView.showMessage.keyDown.message' ) ,
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
:zap: Add Value Survey (#2499)
* N8N-2619 Value Survey Front-end
* N8N-2619 Added Contact Prompt Modal and logic
* N8N-2619 Added Link to Toast Message on Successful submitting ValueSurvey
* N8N-2619 Updated TypeForm URL in ValueSurvey Success Toast
* N8N-2619 Fixed Typo placeholder for ValueSurvey and ContactPrompt Modal
* N8N-2619 Fixed Toast not close automatically in ValueSurvey, Make part of the title bold, Changed Font-sizes on Value Survey
* N8N-2619 Fixed Close Button on ValueSurvey, Vertical Allignment for Questions in ValueSurvey Drawer
* N8N-2619 Make Value Survey with static height
* N8N-2619 Fixed Telemetry Events on closing ValueSurvey
* N8N-2619 Updated N8NPrompt Interface, Added Dynamic Title and Description on ContactPrompt Modal
* N8N-2619 Reversed Answers in ValueSurveyModal
* N8N-2619 Added Telemetry Event on user close ValueSurvey on second Question
* N8N-2619 Re-work, Optimized, Simplify the code after technical review
* N8N-2619 Fixed If else statement in openUserPromptsIfPossible
* N8N-2619 Change Text under Email Box - ValueSurvey, ContactPrompt, Added new Telemetary Event on ValueSurvey Open, Fixed Toast to close aftet 15s
* N8N-2619 Change ContactPrompt Modal to use Atoms like N8N-Heading and N8N-Text
* N8N-2619 Change Design & Logic on ValueSurvey - When to open
* N8N-2619 Updated Value Survey with new Telemetry Events (Refactor), Simplified functions, Added Atoms in ValueSurvey + ContactPrompt
* N8N-2619 Refactor in Interfaces, Updated/Refactor Getters and Vuex store props
* N8N-2619 Defined IN8nValueSurveyData interface
* N8N-2619 Disabled Keyboard shortcuts to be activated on typing in ValueSurvey Input field, Fire an event on Saving WF from Menu and with shorcut, Make Drawer keep-alive
* N8N-2619 Added Atoms in Value Survey Modal (buttons), Rework css
* N8N-2619 Added Responses on ValueSurvey Submit
* N8N-2619 Added Response for SubmittingContactInfo
* N8N-2619 Added loading state for buttons / ValueSurvey
* N8N-2619 Changed ValueSurvey and ContactPrompt to support enter key on submit, Simplifed closeDialog Function, Changed css for button in ValueSurvey, Prevent showing the Modals if Saving WF fails, Add Debouncing on showing prompt
* N8N-2619 Added IsTelemetryEnabled Getter in Vuex.store
* N8N-2619 Created/Added N8N-SquareButton to Design-system
* N8N-2619 Change Promise in MainSideBar with Async/Await function, Nitpick simpliefied
* N8N-2619 Update the text under the input fields
* N8N-2619 Update the text in ContactPrompt Modal
* N8N-2619 Allign Send button on ValueSurvey Modal
* N8N-2619 Fixed Input in ValueSurvey Modal
* N8N-2619 Check if the workflow is saving
* N8N-2619 Check if WF is saving to allowed performs fetchPromptsData
* N8N-2619 Hotfix
* N8N-2619 Fixed ValueSurvey, Updated onSaveButtonClick function, Created onSaveKeyboardShortcut function in NodeView
* N8N-2619 Rework css module classes for ValueSurvey, Simplified
* N8N-2619 Simplified N8N-SquareButton Component, removed dead code
* N8N-2619 Added Breakpoints for Mobile/Tablet View
* N8N-2619 Formatting fix
* N8N-2619 Update css for mobile/tablet, change promises to asyn/await functions in ContactPrompt and ValueSurvey, Added isActive prop to ValueSurvey
* N8N-2619 Update TEMPLATE_BASE_URL to production
2021-12-11 08:38:16 -08:00
this . callDebounced ( 'onSaveKeyboardShortcut' , 1000 ) ;
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 ( ) {
2021-12-10 05:24:09 -08:00
this . copySelectedNodes ( true ) ;
2019-06-23 03:35:23 -07:00
this . deleteSelectedNodes ( ) ;
} ,
2021-12-10 05:24:09 -08:00
copySelectedNodes ( isCut : boolean ) {
2019-06-23 03:35:23 -07:00
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 ) {
2021-12-10 05:24:09 -08:00
if ( ! isCut ) {
this . $showMessage ( {
title : 'Copied!' ,
message : '' ,
type : 'success' ,
} ) ;
}
2021-10-18 20:57:49 -07:00
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 ( {
2021-12-07 08:28:10 -08:00
title : this . $i . baseText ( 'nodeView.showMessage.stopExecutionTry.title' ) ,
message : this . $i . baseText (
2021-11-10 10:41:40 -08:00
'nodeView.showMessage.stopExecutionTry.message' ,
{ interpolate : { executionId } } ,
) ,
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 ( {
2021-12-07 08:28:10 -08:00
title : this . $i . baseText ( 'nodeView.showMessage.stopExecutionCatch.title' ) ,
message : this . $i . baseText ( 'nodeView.showMessage.stopExecutionCatch.message' ) ,
2021-02-09 14:32:40 -08:00
type : 'success' ,
} ) ;
} else {
2021-11-10 10:41:40 -08:00
this . $showError (
error ,
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.showError.stopExecution.title' ) ,
this . $i . baseText ( 'nodeView.showError.stopExecution.message' ) + ':' ,
2021-11-10 10:41:40 -08:00
) ;
2021-02-09 14:32:40 -08:00
}
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 ) {
2021-11-10 10:41:40 -08:00
this . $showError (
error ,
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.showError.stopWaitingForWebhook.title' ) ,
this . $i . baseText ( 'nodeView.showError.stopWaitingForWebhook.message' ) + ':' ,
2021-11-10 10:41:40 -08:00
) ;
2019-06-23 03:35:23 -07:00
return ;
}
this . $showMessage ( {
2021-12-07 08:28:10 -08:00
title : this . $i . baseText ( 'nodeView.showMessage.stopWaitingForWebhook.title' ) ,
message : this . $i . baseText ( 'nodeView.showMessage.stopWaitingForWebhook.message' ) ,
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 ;
}
2021-11-10 10:41:40 -08:00
const importConfirm = await this . confirmMessage (
2021-12-07 08:28:10 -08:00
this . $i . baseText (
2021-11-10 10:41:40 -08:00
'nodeView.confirmMessage.receivedCopyPasteData.message' ,
{ interpolate : { plainTextData } } ,
) ,
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.confirmMessage.receivedCopyPasteData.headline' ) ,
2021-11-10 10:41:40 -08:00
'warning' ,
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.confirmMessage.receivedCopyPasteData.confirmButtonText' ) ,
this . $i . baseText ( 'nodeView.confirmMessage.receivedCopyPasteData.cancelButtonText' ) ,
2021-11-10 10:41:40 -08:00
) ;
2019-06-23 03:35:23 -07:00
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 ( ) ;
2021-11-10 10:41:40 -08:00
this . $showError (
error ,
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.showError.getWorkflowDataFromUrl.title' ) ,
this . $i . baseText ( 'nodeView.showError.getWorkflowDataFromUrl.message' ) + ':' ,
2021-11-10 10:41:40 -08:00
) ;
2019-06-23 03:35:23 -07:00
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 ) {
2021-11-10 10:41:40 -08:00
this . $showError (
error ,
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.showError.importWorkflowData.title' ) ,
this . $i . baseText ( 'nodeView.showError.importWorkflowData.message' ) + ':' ,
2021-11-10 10:41:40 -08:00
) ;
2019-06-23 03:35:23 -07:00
}
} ,
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 ( {
2021-12-07 08:28:10 -08:00
title : this . $i . baseText ( 'nodeView.showMessage.showMaxNodeTypeError.title' ) ,
message : this . $i . baseText (
2021-11-10 10:41:40 -08:00
maxNodes === 1
? 'nodeView.showMessage.showMaxNodeTypeError.message.singular'
: 'nodeView.showMessage.showMaxNodeTypeError.message.plural' ,
{
interpolate : {
maxNodes : maxNodes ! . toString ( ) ,
nodeTypeDataDisplayName : nodeTypeData . displayName ,
} ,
} ,
) ,
2019-06-23 03:35:23 -07:00
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 ( {
2021-12-07 08:28:10 -08:00
title : this . $i . baseText ( 'nodeView.showMessage.addNodeButton.title' ) ,
message : this . $i . baseText (
2021-11-10 10:41:40 -08:00
'nodeView.showMessage.addNodeButton.message' ,
{ interpolate : { nodeTypeName } } ,
) ,
2019-06-23 03:35:23 -07:00
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
}
2021-12-02 08:51:50 -08:00
2019-06-23 03:35:23 -07:00
// Check if node-name is unique else find one that is
2021-12-02 08:51:50 -08:00
newNodeData . name = this . getUniqueNodeName ( {
2021-11-29 03:20:10 -08:00
originalName : newNodeData . name ,
2021-12-02 08:51:50 -08:00
type : newNodeData . type ,
2021-11-29 03:20:10 -08:00
} ) ;
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 ) ;
2021-12-06 00:41:15 -08:00
2021-11-19 01:17:13 -08:00
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 ) {
2021-11-10 10:41:40 -08:00
const importConfirm = await this . confirmMessage (
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.confirmMessage.initView.message' ) ,
this . $i . baseText ( 'nodeView.confirmMessage.initView.headline' ) ,
2021-11-10 10:41:40 -08:00
'warning' ,
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.confirmMessage.initView.confirmButtonText' ) ,
this . $i . baseText ( 'nodeView.confirmMessage.initView.cancelButtonText' ) ,
2021-11-10 10:41:40 -08:00
) ;
2020-10-25 04:47:49 -07:00
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 ) {
2021-12-07 08:28:10 -08:00
const confirmationMessage = this . $i . baseText ( 'nodeView.itLooksLikeYouHaveBeenEditingSomething' ) ;
2020-07-20 07:57:58 -07:00
( e || window . event ) . returnValue = confirmationMessage ; //Gecko + IE
return confirmationMessage ; //Gecko + Webkit, Safari, Chrome etc.
2020-07-20 08:52:24 -07:00
} else {
2021-12-07 06:58:26 -08:00
this . startLoading (
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.redirecting' ) ,
2021-12-07 06:58:26 -08:00
) ;
2021-05-04 08:55:39 -07:00
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-12-02 08:51:50 -08:00
newNodeData . name = this . getUniqueNodeName ( {
2021-11-29 03:20:10 -08:00
originalName : newNodeData . name ,
2021-12-02 08:51:50 -08:00
type : newNodeData . type ,
2021-11-29 03:20:10 -08:00
} ) ;
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 {
2021-11-10 10:41:40 -08:00
const promptResponsePromise = this . $prompt (
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.prompt.newName' ) + ':' ,
this . $i . baseText ( 'nodeView.prompt.renameNode' ) + ` : ${ currentName } ` ,
2021-11-10 10:41:40 -08:00
{
customClass : 'rename-prompt' ,
2021-12-07 08:28:10 -08:00
confirmButtonText : this . $i . baseText ( 'nodeView.prompt.rename' ) ,
cancelButtonText : this . $i . baseText ( 'nodeView.prompt.cancel' ) ,
inputErrorMessage : this . $i . baseText ( 'nodeView.prompt.invalidName' ) ,
2021-11-10 10:41:40 -08:00
inputValue : currentName ,
} ,
) ;
2019-06-23 03:35:23 -07:00
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-12-02 08:51:50 -08:00
newName = this . getUniqueNodeName ( {
2021-11-29 03:20:10 -08:00
originalName : 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 ) {
2021-12-07 08:28:10 -08:00
console . error ( this . $i . baseText ( 'nodeView.thereWasAProblemLoadingTheNodeParametersOfNode' ) + ` : " ${ node . name } " ` ) ; // eslint-disable-line no-console
2019-09-20 05:07:02 -07:00
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
2021-11-10 10:41:40 -08:00
throw new Error (
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.noNodesGivenToAdd' ) ,
2021-11-10 10:41:40 -08:00
) ;
2019-06-23 03:35:23 -07:00
}
// 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-12-02 08:51:50 -08:00
newName = this . getUniqueNodeName ( {
2021-11-29 03:20:10 -08:00
originalName : node . name ,
additionalUsedNames : newNodeNames ,
2021-12-02 08:51:50 -08:00
type : node . type ,
2021-11-29 03:20:10 -08:00
} ) ;
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 ( ) ;
2021-11-15 02:19:43 -08:00
const nodesInfo = await this . restApi ( ) . getNodesInformation ( nodesToBeFetched ) ;
nodesInfo . forEach ( nodeInfo => {
if ( nodeInfo . translation ) {
addNodeTranslation ( nodeInfo . translation , this . $store . getters . defaultLocale ) ;
}
} ) ;
this . $store . commit ( 'updateNodeTypes' , nodesInfo ) ;
2020-10-22 08:24:35 -07:00
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 ) ;
2021-12-02 05:09:41 -08:00
if ( this . defaultLocale !== 'en' ) {
2021-12-06 02:03:39 -08:00
try {
const headers = await this . restApi ( ) . getNodeTranslationHeaders ( ) ;
addHeaders ( headers , this . defaultLocale ) ;
} catch ( _ ) {
// no headers available
}
2021-12-02 05:09:41 -08:00
}
2019-06-23 03:35:23 -07:00
} catch ( error ) {
2021-11-10 10:41:40 -08:00
this . $showError (
error ,
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.showError.mounted1.title' ) ,
this . $i . baseText ( 'nodeView.showError.mounted1.message' ) + ':' ,
2021-11-10 10:41:40 -08:00
) ;
2019-06-23 03:35:23 -07:00
return ;
}
this . instance . ready ( async ( ) => {
try {
this . initNodeView ( ) ;
await this . initView ( ) ;
} catch ( error ) {
2021-11-10 10:41:40 -08:00
this . $showError (
error ,
2021-12-07 08:28:10 -08:00
this . $i . baseText ( 'nodeView.showError.mounted2.title' ) ,
this . $i . baseText ( 'nodeView.showError.mounted2.message' ) + ':' ,
2021-11-10 10:41:40 -08:00
) ;
2019-06-23 03:35:23 -07:00
}
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' ) ;
2021-12-10 06:29:05 -08:00
this . $telemetry . page ( 'Editor' , this . $route . name ) ;
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 >