2019-06-23 03:35:23 -07:00
< template >
< div @ keydown.stop :class ="parameterInputClasses" >
< expression -edit :dialogVisible ="expressionEditDialogVisible" :value ="value" :parameter ="parameter" :path ="path" @closeDialog ="closeExpressionEditDialog" @valueChanged ="expressionUpdated" > < / expression -edit >
2019-07-11 03:58:07 -07:00
< div class = "parameter-input ignore-key-press" :style ="parameterInputWrapperStyle" >
2019-09-04 09:22:06 -07:00
< div v-if ="['json', 'string'].includes(parameter.type)" >
< code -edit :dialogVisible ="codeEditDialogVisible" :value ="value" :parameter ="parameter" @closeDialog ="closeCodeEditDialog" @valueChanged ="expressionUpdated" > < / code -edit >
< text -edit :dialogVisible ="textEditDialogVisible" :value ="value" :parameter ="parameter" @closeDialog ="closeTextEditDialog" @valueChanged ="expressionUpdated" > < / text -edit >
2019-06-23 03:35:23 -07:00
2019-09-04 09:22:06 -07:00
< div v-if ="isEditor === true" class="clickable" @click="displayEditDialog()" >
< prism -editor v-if ="!codeEditDialogVisible" :lineNumbers="true" :readonly="true" :code="displayValue" language="js" > < / prism -editor >
< / div >
< el -input v -else ref = "inputField" size = "small" :type ="getStringInputType" :rows ="getArgument('rows')" :value ="displayValue" :disabled ="isReadOnly" @change ="valueChanged" @ keydown.stop @focus ="setFocus" :title ="displayTitle" :placeholder ="isValueExpression?'':parameter.placeholder" >
< font -awesome -icon v -if = " ! isValueExpression & & ! isReadOnly " slot = "suffix" icon = "external-link-alt" class = "edit-window-button clickable" title = "Open Edit Window" @click ="displayEditDialog()" / >
< / e l - i n p u t >
< / div >
2019-06-23 03:35:23 -07:00
< div v -else -if = " parameter.type = = = ' dateTime ' " >
< el -date -picker
v - model = "tempValue"
ref = "inputField"
type = "datetime"
size = "small"
: value = "displayValue"
: title = "displayTitle"
: disabled = "isReadOnly"
: placeholder = "parameter.placeholder?parameter.placeholder:'Select date and time'"
: picker - options = "dateTimePickerOptions"
@ change = "valueChanged"
@ focus = "setFocus"
@ keydown . stop
>
< / e l - d a t e - p i c k e r >
< / div >
< div v -else -if = " parameter.type = = = ' number ' " >
<!-- < el -slider :value ="value" @input ="valueChanged" > < / el -slider > -- >
< el -input -number ref = "inputField" size = "small" :value ="displayValue" :max ="getArgument('maxValue')" :min ="getArgument('minValue')" :precision ="getArgument('numberPrecision')" :step ="getArgument('numberStepSize')" :disabled ="isReadOnly" @change ="valueChanged" @focus ="setFocus" @ keydown.stop :title ="displayTitle" :placeholder ="parameter.placeholder" > < / e l - i n p u t - n u m b e r >
< / div >
< el -select
v - else - if = "parameter.type === 'options'"
ref = "inputField"
size = "small"
filterable
: value = "displayValue"
: loading = "remoteParameterOptionsLoading"
: disabled = "isReadOnly || remoteParameterOptionsLoading"
: title = "displayTitle"
@ change = "valueChanged"
@ keydown . stop
@ focus = "setFocus"
>
< el -option
v - for = "option in parameterOptions"
: value = "option.value"
: key = "option.value"
: label = "option.name"
>
< div class = "option-headline" > { { option . name } } < / div >
< div v-if ="option.description" class="option-description" v-html="option.description" > < / div >
< / e l - o p t i o n >
< / e l - s e l e c t >
< el -select multiple v -else -if = " parameter.type = = = ' multiOptions ' " ref = "inputField" size = "small" :value ="displayValue" filterable :disabled ="isReadOnly" @change ="valueChanged" @ keydown.stop @focus ="setFocus" :title ="displayTitle" >
< el -option v -for = " option in parameter.options " :value ="option.value" :key ="option.value" :label ="option.name" >
< div class = "option-headline" > { { option . name } } < / div >
< div v-if ="option.description" class="option-description" v-html="option.description" > < / div >
< / e l - o p t i o n >
< / e l - s e l e c t >
< div v -else -if = " parameter.type = = = ' color ' " ref = "inputField" class = "color-input" >
< el -color -picker :value ="displayValue" :disabled ="isReadOnly" @change ="valueChanged" size = "small" class = "color-picker" @focus ="setFocus" :title ="displayTitle" > < / e l - c o l o r - p i c k e r >
< el -input size = "small" type = "text" :value ="displayValue" :disabled ="isReadOnly" @change ="valueChanged" @ keydown.stop @focus ="setFocus" :title ="displayTitle" > < / e l - i n p u t >
< / div >
< div v -else -if = " parameter.type = = = ' boolean ' " >
< el -switch ref = "inputField" :value ="displayValue" @change ="valueChanged" active -color = " # 13ce66 " : disabled = "isValueExpression || isReadOnly" > < / e l - s w i t c h >
< div class = "expression-info clickable" @ click = "expressionEditDialogVisible = true" > Edit Expression < / div >
< / div >
< / div >
< div class = "parameter-options" v-if ="displayOptionsComputed" >
< el -dropdown trigger = "click" @command ="optionSelected" size = "mini" >
< span class = "el-dropdown-link" >
< font -awesome -icon icon = "cogs" class = "reset-icon clickable" title = "Parameter Options" / >
< / span >
< el -dropdown -menu slot = "dropdown" >
2019-07-18 10:26:16 -07:00
< el -dropdown -item command = "addExpression" v-if ="parameter.noDataExpression !== true && !isValueExpression" > Add Expression < / el -dropdown -item >
< el -dropdown -item command = "removeExpression" v-if ="parameter.noDataExpression !== true && isValueExpression" > Remove Expression < / el -dropdown -item >
2019-06-23 03:35:23 -07:00
< el -dropdown -item command = "resetValue" :disabled ="isDefault" divided > Reset Value < / e l - d r o p d o w n - i t e m >
< / e l - d r o p d o w n - m e n u >
< / e l - d r o p d o w n >
< / div >
< div class = "parameter-issues" v-if ="getIssues.length" >
< el -tooltip placement = "top" effect = "light" >
< div slot = "content" v-html ="'Issues:<br /> - ' + getIssues.join('<br /> - ')" > < / div >
< font -awesome -icon icon = "exclamation-triangle" / >
< / e l - t o o l t i p >
< / div >
< / div >
< / template >
< script lang = "ts" >
import Vue from 'vue' ;
import {
INodeUi ,
IVariableItemSelected ,
IVariableSelectorOption ,
} from '@/Interface' ;
import {
NodeHelpers ,
NodeParameterValue ,
INodePropertyOptions ,
Workflow ,
} from 'n8n-workflow' ;
2019-09-04 09:22:06 -07:00
import CodeEdit from '@/components/CodeEdit.vue' ;
2019-06-23 03:35:23 -07:00
import ExpressionEdit from '@/components/ExpressionEdit.vue' ;
2019-09-04 09:22:06 -07:00
// @ts-ignore
import PrismEditor from 'vue-prism-editor' ;
2019-06-23 03:35:23 -07:00
import TextEdit from '@/components/TextEdit.vue' ;
import { genericHelpers } from '@/components/mixins/genericHelpers' ;
import { nodeHelpers } from '@/components/mixins/nodeHelpers' ;
import { showMessage } from '@/components/mixins/showMessage' ;
import { workflowHelpers } from '@/components/mixins/workflowHelpers' ;
import mixins from 'vue-typed-mixins' ;
export default mixins (
genericHelpers ,
nodeHelpers ,
showMessage ,
workflowHelpers ,
)
. extend ( {
name : 'ParameterInput' ,
components : {
2019-09-04 09:22:06 -07:00
CodeEdit ,
2019-06-23 03:35:23 -07:00
ExpressionEdit ,
2019-09-04 09:22:06 -07:00
PrismEditor ,
2019-06-23 03:35:23 -07:00
TextEdit ,
} ,
props : [
'displayOptions' , // boolean
'parameter' , // NodeProperties
'path' , // string
'value' ,
'isCredential' , // boolean
] ,
data ( ) {
return {
2019-09-04 09:22:06 -07:00
codeEditDialogVisible : false ,
2019-06-23 03:35:23 -07:00
nodeName : '' ,
expressionAddOperation : 'set' as 'add' | 'set' ,
expressionEditDialogVisible : false ,
remoteParameterOptions : [ ] as INodePropertyOptions [ ] ,
remoteParameterOptionsLoading : false ,
remoteParameterOptionsLoadingIssues : null as string | null ,
textEditDialogVisible : false ,
tempValue : '' , // el-date-picker does not seem to work without v-model so add one
dateTimePickerOptions : {
shortcuts : [
{
text : 'Today' ,
// tslint:disable-next-line:no-any
onClick ( picker : any ) {
picker . $emit ( 'pick' , new Date ( ) ) ;
} ,
} ,
{
text : 'Yesterday' ,
// tslint:disable-next-line:no-any
onClick ( picker : any ) {
const date = new Date ( ) ;
date . setTime ( date . getTime ( ) - 3600 * 1000 * 24 ) ;
picker . $emit ( 'pick' , date ) ;
} ,
} ,
{
text : 'A week ago' ,
// tslint:disable-next-line:no-any
onClick ( picker : any ) {
const date = new Date ( ) ;
date . setTime ( date . getTime ( ) - 3600 * 1000 * 24 * 7 ) ;
picker . $emit ( 'pick' , date ) ;
} ,
} ,
] ,
} ,
} ;
} ,
watch : {
value ( ) {
this . tempValue = this . displayValue as string ;
} ,
} ,
computed : {
node ( ) : INodeUi | null {
if ( this . isCredential === true ) {
return null ;
}
return this . $store . getters . activeNode ;
} ,
displayTitle ( ) : string {
let title = ` Parameter: " ${ this . shortPath } " ` ;
if ( this . getIssues . length ) {
title += ` has issues ` ;
if ( this . isValueExpression === true ) {
title += ` and expression ` ;
}
title += ` ! ` ;
} else {
if ( this . isValueExpression === true ) {
title += ` has expression ` ;
}
}
return title ;
} ,
displayValue ( ) : string | number | boolean | null {
if ( this . remoteParameterOptionsLoadingIssues !== null ) {
return 'Error loading...' ;
} else if ( this . remoteParameterOptionsLoading === true ) {
// If it is loading options from server display
// to user that the data is loading. If not it would
// display the user the key instead of the value it
// represents
return 'Loading options...' ;
}
let returnValue ;
if ( this . isValueExpression === false ) {
returnValue = this . value ;
} else {
returnValue = this . expressionValueComputed ;
}
if ( returnValue !== undefined && this . parameter . type === 'string' ) {
const rows = this . getArgument ( 'rows' ) ;
if ( rows === undefined || rows === 1 ) {
returnValue = returnValue . toString ( ) . replace ( /\n/ , '|' ) ;
}
}
return returnValue ;
} ,
displayOptionsComputed ( ) : boolean {
if ( this . isReadOnly === true ) {
return false ;
}
if ( this . parameter . type === 'collection' ) {
return false ;
}
if ( this . displayOptions === true ) {
return true ;
}
return false ;
} ,
expressionValueComputed ( ) : NodeParameterValue | null {
if ( this . isCredential === true || this . node === null ) {
return null ;
}
let computedValue : NodeParameterValue ;
try {
computedValue = this . resolveExpression ( this . value ) as NodeParameterValue ;
} catch ( error ) {
computedValue = ` [ERROR: ${ error . message } ] ` ;
}
// Try to convert it into the corret type
if ( this . parameter . type === 'number' ) {
computedValue = parseInt ( computedValue as string , 10 ) ;
if ( isNaN ( computedValue ) ) {
return null ;
}
}
return computedValue ;
} ,
getStringInputType ( ) {
if ( this . getArgument ( 'password' ) === true ) {
return 'password' ;
}
const rows = this . getArgument ( 'rows' ) ;
if ( rows !== undefined && rows > 1 ) {
return 'textarea' ;
}
return 'text' ;
} ,
getIssues ( ) : string [ ] {
if ( this . isCredential === true || this . node === null ) {
return [ ] ;
}
const newPath = this . shortPath . split ( '.' ) ;
newPath . pop ( ) ;
const issues = NodeHelpers . getParameterIssues ( this . parameter , this . node . parameters , newPath . join ( '.' ) ) ;
if ( this . parameter . type === 'options' && this . remoteParameterOptionsLoading === false && this . remoteParameterOptionsLoadingIssues === null ) {
// Check if the value resolves to a valid option
// Currently it only displays an error in the node itself in
// case the value is not valid. The workflow can still be executed
// and the error is not displayed on the node in the workflow
const validOptions = this . parameterOptions ! . map ( ( options : INodePropertyOptions ) => options . value ) ;
if ( this . displayValue === null || ! validOptions . includes ( this . displayValue as string ) ) {
if ( issues . parameters === undefined ) {
issues . parameters = { } ;
}
issues . parameters [ this . parameter . name ] = [ ` The value " ${ this . displayValue } " is not supported! ` ] ;
}
} else if ( this . remoteParameterOptionsLoadingIssues !== null ) {
if ( issues . parameters === undefined ) {
issues . parameters = { } ;
}
issues . parameters [ this . parameter . name ] = [ ` There was a problem loading the parameter options from server: " ${ this . remoteParameterOptionsLoadingIssues } " ` ] ;
}
if ( issues !== undefined &&
issues . parameters !== undefined &&
issues . parameters [ this . parameter . name ] !== undefined ) {
return issues . parameters [ this . parameter . name ] ;
}
return [ ] ;
} ,
isDefault ( ) : boolean {
return this . parameter . default === this . value ;
} ,
2019-09-04 09:22:06 -07:00
isEditor ( ) : boolean {
return this . getArgument ( 'editor' ) === 'code' ;
} ,
2019-06-23 03:35:23 -07:00
isValueExpression ( ) {
2019-07-18 10:26:16 -07:00
if ( this . parameter . noDataExpression === true ) {
return false ;
}
2019-06-23 03:35:23 -07:00
if ( typeof this . value === 'string' && this . value . charAt ( 0 ) === '=' ) {
return true ;
}
return false ;
} ,
parameterOptions ( ) : INodePropertyOptions [ ] {
if ( this . remoteMethod === undefined ) {
// Options are already given
return this . parameter . options ;
}
// Options get loaded from server
return this . remoteParameterOptions ;
} ,
parameterInputClasses ( ) {
const classes = [ ] ;
if ( this . isValueExpression ) {
classes . push ( 'expression' ) ;
}
if ( this . getIssues . length ) {
classes . push ( 'has-issues' ) ;
}
return classes ;
} ,
parameterInputWrapperStyle ( ) {
let deductWidth = 0 ;
const styles = {
width : '100%' ,
} ;
if ( this . displayOptionsComputed === true ) {
deductWidth += 25 ;
}
if ( this . getIssues . length ) {
deductWidth += 20 ;
}
if ( deductWidth !== 0 ) {
styles . width = ` calc(100% - ${ deductWidth } px) ` ;
}
return styles ;
} ,
remoteMethod ( ) : string | undefined {
return this . getArgument ( 'loadOptionsMethod' ) as string | undefined ;
} ,
shortPath ( ) : string {
const shortPath = this . path . split ( '.' ) ;
shortPath . shift ( ) ;
return shortPath . join ( '.' ) ;
} ,
workflow ( ) : Workflow {
return this . getWorkflow ( ) ;
} ,
} ,
methods : {
async loadRemoteParameterOptions ( ) {
if ( this . node === null || this . remoteMethod === undefined ) {
return ;
}
this . remoteParameterOptionsLoadingIssues = null ;
this . remoteParameterOptionsLoading = true ;
this . remoteParameterOptions . length = 0 ;
try {
const options = await this . restApi ( ) . getNodeParameterOptions ( this . node . type , this . remoteMethod , this . node . credentials ) ;
this . remoteParameterOptions . push . apply ( this . remoteParameterOptions , options ) ;
} catch ( error ) {
this . remoteParameterOptionsLoadingIssues = error . message ;
}
this . remoteParameterOptionsLoading = false ;
} ,
2019-09-04 09:22:06 -07:00
closeCodeEditDialog ( ) {
this . codeEditDialogVisible = false ;
} ,
2019-06-23 03:35:23 -07:00
closeExpressionEditDialog ( ) {
this . expressionEditDialogVisible = false ;
} ,
closeTextEditDialog ( ) {
this . textEditDialogVisible = false ;
} ,
2019-09-04 09:22:06 -07:00
displayEditDialog ( ) {
if ( this . isEditor ) {
this . codeEditDialogVisible = true ;
} else {
this . textEditDialogVisible = true ;
}
} ,
2019-06-23 03:35:23 -07:00
getArgument ( argumentName : string ) : string | number | boolean | undefined {
if ( this . parameter . typeOptions === undefined ) {
return undefined ;
}
if ( this . parameter . typeOptions [ argumentName ] === undefined ) {
return undefined ;
}
return this . parameter . typeOptions [ argumentName ] ;
} ,
expressionUpdated ( value : string ) {
this . valueChanged ( value ) ;
} ,
setFocus ( ) {
if ( this . isReadOnly === true ) {
return ;
}
if ( this . isValueExpression ) {
this . expressionEditDialogVisible = true ;
return ;
}
if ( this . parameter . type === 'string' && this . getArgument ( 'alwaysOpenEditWindow' ) ) {
2019-09-04 09:22:06 -07:00
this . displayEditDialog ( ) ;
2019-06-23 03:35:23 -07:00
return ;
}
if ( this . node !== null ) {
// When an event like mouse-click removes the active node while
// editing is active it does not know where to save the value to.
// For that reason do we save the node-name here. We could probably
// also just do that once on load but if Vue decides for some reason to
// reuse the input it could have the wrong value so lets set it everytime
// just to be sure
this . nodeName = this . node . name ;
}
// Set focus on field
setTimeout ( ( ) => {
2019-10-15 14:19:20 -07:00
// @ts-ignore
2019-10-15 07:44:09 -07:00
( this . $refs . inputField . $el . querySelector ( 'input' ) as HTMLInputElement ) . focus ( ) ;
2019-06-23 03:35:23 -07:00
} ) ;
} ,
valueChanged ( value : string | number | boolean | Date | null ) {
if ( value instanceof Date ) {
value = value . toISOString ( ) ;
}
const parameterData = {
node : this . node !== null ? this . node . name : this . nodeName ,
name : this . path ,
value ,
} ;
this . $emit ( 'valueChanged' , parameterData ) ;
} ,
optionSelected ( command : string ) {
if ( command === 'resetValue' ) {
this . valueChanged ( this . parameter . default ) ;
} else if ( command === 'addExpression' ) {
this . valueChanged ( ` = ${ this . value } ` ) ;
this . expressionEditDialogVisible = true ;
} else if ( command === 'removeExpression' ) {
this . valueChanged ( this . expressionValueComputed || null ) ;
}
} ,
} ,
mounted ( ) {
this . tempValue = this . displayValue as string ;
if ( this . node !== null ) {
this . nodeName = this . node . name ;
}
if ( this . remoteMethod !== undefined && this . node !== null ) {
// Make sure to load the parameter options
// directly and whenever the credentials change
this . $watch ( ( ) => this . node ! . credentials , ( ) => {
this . loadRemoteParameterOptions ( ) ;
} , { deep : true , immediate : true } ) ;
}
} ,
} ) ;
< / script >
< style scoped lang = "scss" >
. parameter - input {
display : inline - block ;
}
. parameter - options {
width : 25 px ;
text - align : right ;
float : right ;
}
. parameter - issues {
width : 20 px ;
text - align : right ;
float : right ;
color : # ff8080 ;
font - size : 1.2 em ;
}
. color - input {
background - color : $ -- custom - input - background ;
border - radius : 16 px ;
line - height : 2.2 em ;
. el - input {
width : 90 px ;
}
. color - picker {
float : left ;
z - index : 10 ;
}
}
< / style >
< style lang = "scss" >
. ql - editor {
padding : 6 px ;
line - height : 26 px ;
}
. expression - info {
display : none ;
}
. expression {
. expression - info {
display : inline - block ;
background - color : # 441133 ;
color : # fff ;
font - size : 0.7 em ;
line - height : 2.5 em ;
padding : 0 0.5 em ;
margin - left : 1 em ;
border - radius : 3 px ;
}
. el - switch _ _core {
border : 1 px dashed $ -- custom - expression - text ;
}
. el - input > . el - input _ _inner ,
. el - select > . el - input _ _inner ,
. el - textarea textarea ,
. el - textarea textarea : active ,
. el - textarea textarea : focus ,
. el - textarea textarea : hover ,
. el - input - number ,
. color - input {
border : 1 px dashed $ -- custom - expression - text ;
color : $ -- custom - expression - text ;
background - color : $ -- custom - expression - background ;
}
. el - input - number input ,
. color - input . el - input _ _inner {
background - color : $ -- custom - expression - background ;
}
// Overwrite again for number and color inputs to not create
// a second border inside of the already existing one
. color - input . el - input > . el - input _ _inner ,
. el - input - number . el - input > . el - input _ _inner {
border : none ;
background - color : none ;
}
}
. has - issues {
. el - textarea textarea ,
. el - textarea textarea : active ,
. el - textarea textarea : focus ,
. el - textarea textarea : hover ,
. el - input - number input ,
. el - input - number input : active ,
. el - input - number input : focus ,
. el - input - number input : hover ,
. el - input - number [ role = "button" ] ,
. el - input - number [ role = "button" ] : active ,
. el - input - number [ role = "button" ] : focus ,
. el - input - number [ role = "button" ] : hover ,
. el - input input ,
. el - input input : active ,
. el - input input : focus ,
. el - input input : hover {
border - width : 1 px ;
border - color : # ff8080 ;
border - style : solid ;
}
}
. el - dropdown {
color : # 999 ;
}
. option - headline {
font - weight : 600 ;
}
li : not ( . selected ) . option - description {
color : $ -- custom - font - very - light ;
}
. option - description {
font - weight : 400 ;
white - space : normal ;
max - width : 350 px ;
margin - top : - 4 px ;
}
. edit - window - button {
display : none ;
}
. parameter - input : hover . edit - window - button {
display : inline ;
}
< / style >