3 * Copyright 2016 RIFT.IO Inc
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
19 * Created by onvelocity on 1/18/16.
21 * This class generates the form fields used to edit the CONFD JSON model.
25 import _
from 'lodash'
26 import utils
from '../libraries/utils'
27 import React
from 'react'
28 import ClassNames
from 'classnames'
29 import changeCase
from 'change-case'
30 import toggle
from '../libraries/ToggleElementHandler'
31 import Button
from './Button'
32 import Property
from '../libraries/model/DescriptorModelMetaProperty'
33 import ComposerAppActions
from '../actions/ComposerAppActions'
34 import CatalogItemsActions
from '../actions/CatalogItemsActions'
35 import DESCRIPTOR_MODEL_FIELDS
from '../libraries/model/DescriptorModelFields'
36 import DescriptorModelFactory
from '../libraries/model/DescriptorModelFactory'
37 import DescriptorModelMetaFactory
from '../libraries/model/DescriptorModelMetaFactory'
38 import SelectionManager
from '../libraries/SelectionManager'
39 import DeletionManager
from '../libraries/DeletionManager'
40 import DescriptorModelIconFactory
from '../libraries/model/IconFactory'
41 import getEventPath
from '../libraries/getEventPath'
43 import imgAdd
from '../../../node_modules/open-iconic/svg/plus.svg'
44 import imgRemove
from '../../../node_modules/open-iconic/svg/trash.svg'
46 import '../styles/EditDescriptorModelProperties.scss'
48 function getDescriptorMetaBasicForType(type
) {
49 const basicPropertiesFilter
= d
=> _
.contains(DESCRIPTOR_MODEL_FIELDS
[type
], d
.name
);
50 return DescriptorModelMetaFactory
.getModelMetaForType(type
, basicPropertiesFilter
) || {properties
: []};
53 function getDescriptorMetaAdvancedForType(type
) {
54 const advPropertiesFilter
= d
=> !_
.contains(DESCRIPTOR_MODEL_FIELDS
[type
], d
.name
);
55 return DescriptorModelMetaFactory
.getModelMetaForType(type
, advPropertiesFilter
) || {properties
: []};
58 function getTitle(model
= {}) {
59 if (typeof model
['short-name'] === 'string' && model
['short-name']) {
60 return model
['short-name'];
62 if (typeof model
.name
=== 'string' && model
.name
) {
65 if (model
.uiState
&& typeof model
.uiState
.displayName
=== 'string' && model
.uiState
.displayName
) {
66 return model
.uiState
.displayName
68 if (typeof model
.id
=== 'string') {
73 export default function EditDescriptorModelProperties(props
) {
75 const container
= props
.container
;
77 if (!(DescriptorModelFactory
.isContainer(container
))) {
81 function startEditing() {
82 DeletionManager
.removeEventListeners();
85 function endEditing() {
86 DeletionManager
.addEventListeners();
89 function onClickSelectItem(property
, path
, value
, event
) {
90 event
.preventDefault();
91 const root
= this.getRoot();
92 if (SelectionManager
.select(value
)) {
93 CatalogItemsActions
.catalogItemMetaDataChanged(root
.model
);
97 function onFocusPropertyFormInputElement(property
, path
, value
, event
) {
99 event
.preventDefault();
102 function removeIsFocusedClass(event
) {
103 event
.target
.removeEventListener('blur', removeIsFocusedClass
);
104 Array
.from(document
.querySelectorAll('.-is-focused')).forEach(d
=> d
.classList
.remove('-is-focused'));
107 removeIsFocusedClass(event
);
109 const propertyWrapper
= getEventPath(event
).reduce((parent
, element
) => {
113 if (!element
.classList
) {
116 if (element
.classList
.contains('property')) {
121 if (propertyWrapper
) {
122 propertyWrapper
.classList
.add('-is-focused');
123 event
.target
.addEventListener('blur', removeIsFocusedClass
);
128 function buildAddPropertyAction(container
, property
, path
) {
129 function onClickAddProperty(property
, path
, event
) {
130 event
.preventDefault();
131 //SelectionManager.resume();
132 const create
= Property
.getContainerCreateMethod(property
, this);
135 create(model
, path
, property
);
137 const name
= path
.join('.');
138 const value
= Property
.createModelInstance(property
);
139 utils
.assignPathValue(this.model
, name
, value
);
141 CatalogItemsActions
.catalogItemDescriptorChanged(this.getRoot());
144 <Button className
="inline-hint" onClick
={onClickAddProperty
.bind(container
, property
, path
)} label
="Add" src
={imgAdd
} />
148 function buildRemovePropertyAction(container
, property
, path
) {
149 function onClickRemoveProperty(property
, path
, event
) {
150 event
.preventDefault();
151 const name
= path
.join('.');
152 const removeMethod
= Property
.getContainerMethod(property
, this, 'remove');
154 removeMethod(utils
.resolvePath(this.model
, name
));
156 utils
.removePathValue(this.model
, name
);
158 CatalogItemsActions
.catalogItemDescriptorChanged(this.getRoot());
161 <Button className
="remove-property-action inline-hint" title
="Remove" onClick
={onClickRemoveProperty
.bind(container
, property
, path
)} label
="Remove" src
={imgRemove
}/>
165 function onFormFieldValueChanged(event
) {
166 if (DescriptorModelFactory
.isContainer(this)) {
167 event
.preventDefault();
168 const name
= event
.target
.name
;
169 const value
= event
.target
.value
;
170 utils
.assignPathValue(this.model
, name
, value
);
171 CatalogItemsActions
.catalogItemDescriptorChanged(this.getRoot());
175 function buildField(container
, property
, path
, value
, fieldKey
) {
177 const name
= path
.join('.');
178 const isEditable
= true;
179 const isGuid
= Property
.isGuid(property
);
180 const onChange
= onFormFieldValueChanged
.bind(container
);
181 const isEnumeration
= Property
.isEnumeration(property
);
182 const onFocus
= onFocusPropertyFormInputElement
.bind(container
, property
, path
, value
);
183 const placeholder
= changeCase
.title(property
.name
);
184 const className
= ClassNames(property
.name
+ '-input', {'-is-guid': isGuid
});
185 const fieldValue
= value
? (value
.constructor.name
!= "Object") ? value
: '' : undefined;
187 const enumeration
= Property
.getEnumeration(property
, value
);
188 const options
= enumeration
.map((d
, i
) => {
189 // note yangforge generates values for enums but the system does not use them
190 // so we categorically ignore them
191 // https://trello.com/c/uzEwVx6W/230-bug-enum-should-not-use-index-only-name
192 //return <option key={fieldKey + ':' + i} value={d.value}>{d.name}</option>;
193 return <option key
={fieldKey
.toString() + ':' + i
} value
={d
.name
}>{d
.name
}</option
>;
195 const isValueSet
= enumeration
.filter(d
=> d
.isSelected
).length
> 0;
196 if (!isValueSet
|| property
.cardinality
=== '0..1') {
197 const noValueDisplayText
= changeCase
.title(property
.name
);
198 options
.unshift(<option key
={'(value-not-in-enum)' + fieldKey
.toString()} value
="" placeholder
={placeholder
}>{noValueDisplayText
}</option
>);
200 return <select key
={fieldKey
.toString()} id
={fieldKey
.toString()} className
={ClassNames({'-value-not-set': !isValueSet
})} name
={name
} value
={value
} title
={name
} onChange
={onChange
} onFocus
={onFocus
} onBlur
={endEditing
} onMouseDown
={startEditing
} onMouseOver
={startEditing
} readOnly
={!isEditable
}>{options
}</select
>;
203 if (property
['preserve-line-breaks']) {
204 return <textarea key
={fieldKey
.toString()} cols
="5" id
={fieldKey
.toString()} name
={name
} value
={value
} placeholder
={placeholder
} onChange
={onChange
} onFocus
={onFocus
} onBlur
={endEditing
} onMouseDown
={startEditing
} onMouseOver
={startEditing
} onMouseOut
={endEditing
} onMouseLeave
={endEditing
} readOnly
={!isEditable
} />;
207 return <input key
={fieldKey
.toString()}
208 id
={fieldKey
.toString()}
212 className
={className
}
213 placeholder
={placeholder
}
217 onMouseDown
={startEditing
}
218 onMouseOver
={startEditing
}
219 onMouseOut
={endEditing
}
220 onMouseLeave
={endEditing
}
221 readOnly
={!isEditable
}
226 function buildElement(container
, property
, valuePath
, value
) {
227 return property
.properties
.map((property
, index
) => {
229 const childPath
= valuePath
.slice();
230 if (typeof value
=== 'object') {
231 childValue
= value
[property
.name
];
233 if(property
.type
!= 'choice'){
234 childPath
.push(property
.name
);
236 return build(container
, property
, childPath
, childValue
);
241 function buildChoice(container
, property
, path
, value
, key
) {
243 function onFormFieldValueChanged(event
) {
244 if (DescriptorModelFactory
.isContainer(this)) {
246 event
.preventDefault();
248 const name
= event
.target
.name
;
249 const value
= event
.target
.value
;
252 Transient State is stored for convenience in the uiState field.
253 The choice yang type uses case elements to describe the "options".
254 A choice can only ever have one option selected which allows
255 the system to determine which type is selected by the name of
256 the element contained within the field.
259 //const stateExample = {
274 const statePath
= ['uiState.choice'].concat(name
);
275 const stateObject
= utils
.resolvePath(this.model
, statePath
.join('.')) || {};
276 const selected
= stateObject
.selected
? stateObject
.selected
.split('.')[1] : undefined;
277 // write state back to the model so the new state objects are captured
278 utils
.assignPathValue(this.model
, statePath
.join('.'), stateObject
);
280 // write the current choice value into the state
281 const choiceObject
= utils
.resolvePath(this.model
, [name
, selected
].join('.'));
283 utils
.assignPathValue(stateObject
, ['case', selected
].join('.'), _
.cloneDeep(choiceObject
));
286 // remove the current choice value from the model
287 utils
.removePathValue(this.model
, [name
, selected
].join('.'));
289 // get any state for the new selected choice
290 const newChoiceObject
= utils
.resolvePath(stateObject
, ['case', value
].join('.')) || {};
292 // assign new choice value to the model
293 utils
.assignPathValue(this.model
, [name
, value
].join('.'), newChoiceObject
);
295 // update the selected name
296 utils
.assignPathValue(this.model
, statePath
.concat('selected').join('.'), value
);
298 CatalogItemsActions
.catalogItemDescriptorChanged(this.getRoot());
302 const caseByNameMap
= {};
304 const onChange
= onFormFieldValueChanged
.bind(container
);
306 const cases
= property
.properties
.map(d
=> {
307 if (d
.type
=== 'case') {
308 caseByNameMap
[d
.name
] = d
.properties
[0];
311 optionTitle
: d
.description
,
312 //represents case name and case element name
313 optionValue
: [d
.name
, d
.properties
[0].name
].join('.')
316 caseByNameMap
[d
.name
] = d
;
317 return {optionName
: d
.name
};
320 const options
= [{optionName
: ''}].concat(cases
).map((d
, i
) => {
322 <option key
={i
} value
={d
.optionValue
} title
={d
.optionTitle
}>
324 {i
? null : changeCase
.title(property
.name
)}
329 const selectName
= path
.join('.');
330 let selectedOptionPath
= ['uiState.choice', selectName
, 'selected'].join('.');
331 //Currently selected choice/case statement on UI model
332 let selectedOptionValue
= utils
.resolvePath(container
.model
, selectedOptionPath
);
333 //If first time loaded, and none is selected, check if there is a value corresponding to a case statement in the container model
334 if(!selectedOptionValue
) {
335 //get field properties for choice on container model
336 let fieldProperties
= utils
.resolvePath(container
.model
, selectName
);
337 if(fieldProperties
) {
338 //Check each case statement in model and see if it is present in container model.
339 cases
.map(function(c
){
340 if(fieldProperties
.hasOwnProperty(c
.optionName
)) {
341 utils
.assignPathValue(container
.model
, ['uiState.choice', selectName
, 'selected'].join('.'), c
.optionValue
);
344 selectedOptionValue
= utils
.resolvePath(container
.model
, ['uiState.choice', selectName
, 'selected'].join('.'));
347 //If selectedOptionValue is present, take first item in string which represents the case name.
348 const valueProperty
= caseByNameMap
[selectedOptionValue
? selectedOptionValue
.split('.')[0] : undefined] || {properties
: []};
349 const isLeaf
= Property
.isLeaf(valueProperty
);
350 const hasProperties
= _
.isArray(valueProperty
.properties
) && valueProperty
.properties
.length
;
351 const isMissingDescriptorMeta
= !hasProperties
&& !Property
.isLeaf(valueProperty
);
352 //Some magic that prevents errors for arising
353 const valueResponse
= valueProperty
.properties
.length
? valueProperty
.properties
.map((d
, i
) => {
354 const childPath
= path
.concat(valueProperty
.name
, d
.name
);
355 const childValue
= utils
.resolvePath(container
.model
, childPath
.join('.'));
357 <div key
={childPath
.concat('info', i
).join(':')}>
358 {build(container
, d
, childPath
, childValue
, props
)}
361 }) : (!isMissingDescriptorMeta
) ? build(container
, valueProperty
, path
.concat(valueProperty
.name
), utils
.resolvePath(container
.model
, path
.concat(valueProperty
.name
).join('.'))) : null
363 const onFocus
= onFocusPropertyFormInputElement
.bind(container
, property
, path
, value
);
366 <div key
={key
} className
="choice">
367 <select key
={Date
.now()} className
={ClassNames({'-value-not-set': !selectedOptionValue
})} name
={selectName
} value
={selectedOptionValue
} onChange
={onChange
} onFocus
={onFocus
} onBlur
={endEditing
} onMouseDown
={startEditing
} onMouseOver
={startEditing
} onMouseOut
={endEditing
} onMouseLeave
={endEditing
}>
376 function buildSimpleListItem(container
, property
, path
, value
, key
, index
) {
377 // todo need to abstract this better
378 const title
= getTitle(value
);
379 var req
= require
.context("../", true, /\.svg
/);
382 <a href
="#select-list-item" key
={Date
.now()} className
={property
.name
+ '-list-item simple-list-item '} onClick
={onClickSelectItem
.bind(container
, property
, path
, value
)}>
383 <img src
={req('./' + DescriptorModelIconFactory
.getUrlForType(property
.name
))} width
="20px" />
386 {buildRemovePropertyAction(container
, property
, path
)}
391 function buildRemoveListItem(container
, property
, valuePath
, fieldKey
, index
) {
392 const className
= ClassNames(property
.name
+ '-remove actions');
394 <div key
={fieldKey
.concat(index
).join(':')} className
={className
}>
396 <span className
={property
.type
+ '-name name'}>{changeCase
.title(property
.name
)}</span
>
397 <span className
="info">{index
+ 1}</span
>
398 {buildRemovePropertyAction(container
, property
, valuePath
)}
404 function buildLeafListItem(container
, property
, valuePath
, value
, key
, index
) {
405 // look at the type to determine how to parse the value
408 {buildRemoveListItem(container
, property
, valuePath
, key
, index
)}
409 {buildField(container
, property
, valuePath
, value
, key
)}
415 function build(container
, property
, path
, value
, props
= {}) {
418 const isLeaf
= Property
.isLeaf(property
);
419 const isArray
= Property
.isArray(property
);
420 const isObject
= Property
.isObject(property
);
421 const isLeafList
= Property
.isLeafList(property
);
422 const fieldKey
= [container
.id
].concat(path
);
423 const isRequired
= Property
.isRequired(property
);
424 const title
= changeCase
.titleCase(property
.name
);
425 const columnCount
= property
.properties
.length
|| 1;
426 const isColumnar
= isArray
&& (Math
.round(props
.width
/ columnCount
) > 155);
427 const classNames
= {'-is-required': isRequired
, '-is-columnar': isColumnar
};
429 if (!property
.properties
&& isObject
) {
430 const uiState
= DescriptorModelMetaFactory
.getModelMetaForType(property
.name
) || {};
431 property
.properties
= uiState
.properties
;
434 const hasProperties
= _
.isArray(property
.properties
) && property
.properties
.length
;
435 const isMissingDescriptorMeta
= !hasProperties
&& !Property
.isLeaf(property
);
437 // ensure value is not undefined for non-leaf property types
439 if (typeof value
!== 'object') {
440 value
= isArray
? [] : {};
443 const valueAsArray
= _
.isArray(value
) ? value
: isLeafList
&& typeof value
=== 'undefined' ? [] : [value
];
445 const isMetaField
= property
.name
=== 'meta';
446 const isCVNFD
= property
.name
=== 'constituent-vnfd';
447 const isSimpleListView
= Property
.isSimpleList(property
);
449 valueAsArray
.forEach((value
, index
) => {
452 const key
= fieldKey
.slice();
453 const valuePath
= path
.slice();
456 valuePath
.push(index
);
461 if (typeof value
=== 'object') {
462 value
= JSON
.stringify(value
, undefined, 12);
463 } else if (typeof value
!== 'string') {
468 if (isMissingDescriptorMeta
) {
469 field
= <span key
={key
.concat('warning').join(':')} className
="warning">No Descriptor Meta
for {property
.name
}</span
>;
470 } else if (property
.type
=== 'choice') {
471 field
= buildChoice(container
, property
, valuePath
, value
, key
.join(':'));
472 } else if (isSimpleListView
) {
473 field
= buildSimpleListItem(container
, property
, valuePath
, value
, key
, index
);
474 } else if (isLeafList
) {
475 field
= buildLeafListItem(container
, property
, valuePath
, value
, key
, index
);
476 } else if (hasProperties
) {
477 field
= buildElement(container
, property
, valuePath
, value
, key
.join(':'))
479 field
= buildField(container
, property
, valuePath
, value
, key
.join(':'));
482 function onClickLeaf(property
, path
, value
, event
) {
483 if (event
.isDefaultPrevented()) {
486 event
.preventDefault();
487 event
.stopPropagation();
488 this.getRoot().uiState
.focusedPropertyPath
= path
.join('.');
489 console
.log('property selected', path
.join('.'));
490 ComposerAppActions
.propertySelected([path
.join('.')]);
493 const clickHandler
= isLeaf
? onClickLeaf
: () => {};
494 const isContainerList
= isArray
&& !(isSimpleListView
|| isLeafList
);
497 <div key
={fieldKey
.concat(['property-content', index
]).join(':')}
498 className
={ClassNames('property-content', {'simple-list': isSimpleListView
})}
499 onClick
={clickHandler
.bind(container
, property
, valuePath
, value
)}>
500 {isContainerList
? buildRemoveListItem(container
, property
, valuePath
, fieldKey
, index
) : null}
507 classNames
['-is-leaf'] = isLeaf
;
508 classNames
['-is-array'] = isArray
;
509 classNames
['cols-' + columnCount
] = isColumnar
;
511 if (property
.type
=== 'choice') {
512 value
= utils
.resolvePath(container
.model
, ['uiState.choice'].concat(path
, 'selected').join('.'));
515 let displayValue
= typeof value
=== 'object' ? '' : value
;
516 const displayValueInfo
= isArray
? valueAsArray
.filter(d
=> typeof d
!== 'undefined').length
+ ' items' : '';
518 const onFocus
= isLeaf
? event
=> event
.target
.classList
.add('-is-focused') : false;
521 <div key
={fieldKey
.join(':')} className
={ClassNames(property
.type
+ '-property property', classNames
)} onFocus
={onFocus
}>
522 <h3 className
="property-label">
523 <label htmlFor
={fieldKey
.join(':')}>
524 <span className
={property
.type
+ '-name name'}>{title
}</span
>
526 <span className
={property
.type
+ '-info info'}>{displayValueInfo
}</span
>
527 <span className
={property
.type
+ '-value value'}>{displayValue
}</span
>
529 {isArray
? buildAddPropertyAction(container
, property
, path
.concat(valueAsArray
.length
)) : null}
532 <span className
={property
.type
+ '-description description'}>{property
.description
}</span
>
533 <val className
="property-value">
534 {isCVNFD
? <span className
={property
.type
+ '-tip tip'}>Drag a VNFD
from the Catalog to add more
.</span
> : null}
542 const containerType
= container
.uiState
['qualified-type'] || container
.uiState
.type
;
543 const basicProperties
= getDescriptorMetaBasicForType(containerType
).properties
;
545 function buildBasicGroup() {
546 if (basicProperties
.length
=== 0) {
550 <div className
="basic-properties-group">
553 {basicProperties
.map(property
=> {
554 const path
= [property
.name
];
555 const value
= container
.model
[property
.name
];
556 return build(container
, property
, path
, value
);
563 function buildAdvancedGroup() {
564 const properties
= getDescriptorMetaAdvancedForType(containerType
).properties
;
565 if (properties
.length
=== 0) {
568 const hasBasicFields
= basicProperties
.length
> 0;
569 const closeGroup
= basicProperties
.length
> 0;
571 <div className
="advanced-properties-group">
572 <h1 data
-toggle
={closeGroup
? 'true' : 'false'} className
={ClassNames({'-is-toggled': closeGroup
})} onClick
={toggle
} style
={{display
: hasBasicFields
? 'block' : 'none'}}>
573 <a className
="toggle-show-more" href
="#show-more-properties">more
&hellip
;</a
>
574 <a className
="toggle-show-less" href
="#show-more-properties">less
&hellip
;</a
>
576 <div className
="toggleable">
577 {properties
.map(property
=> {
578 const path
= [property
.name
];
579 const value
= container
.model
[property
.name
];
580 return build(container
, property
, path
, value
, {toggle
: true, width
: props
.width
});
583 <div className
="toggle-bottom-spacer" style
={{visibility
: 'hidden', 'height': '50%', position
: 'absolute'}}>We need
this so when the user closes the panel it won
't shift away and scare the bj out of them!</div>
588 function buildMoreLess(d, i) {
590 <span key={'bread
-crumb
-part
-' + i}>
591 <a href="#select-item" onClick={onClickSelectItem.bind(d, null, null, d)}>{d.title}</a>
598 if (container.parent) {
599 path.push(container.parent);
601 path.push(container);
604 <div className="EditDescriptorModelProperties -is-tree-view">
605 <h1>{path.map(buildMoreLess)}</h1>
607 {buildAdvancedGroup()}