Fix problem where the connection-point UI was not updating list correctly when button...
[osm/UI.git] / skyquake / plugins / composer / src / src / components / EditDescriptorModelProperties.js
1 /*
2 *
3 * Copyright 2016 RIFT.IO Inc
4 *
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
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
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.
16 *
17 */
18 /**
19 * Created by onvelocity on 1/18/16.
20 *
21 * This class generates the form fields used to edit the CONFD JSON model.
22 */
23
24 import _includes from 'lodash/includes'
25 import _isArray from 'lodash/isArray'
26 import _cloneDeep from 'lodash/cloneDeep'
27 import _debounce from 'lodash/debounce';
28 import _uniqueId from 'lodash/uniqueId';
29 import _set from 'lodash/set';
30 import _get from 'lodash/get';
31 import _has from 'lodash/has';
32 import utils from '../libraries/utils'
33 import React from 'react'
34 import ClassNames from 'classnames'
35 import changeCase from 'change-case'
36 import toggle from '../libraries/ToggleElementHandler'
37 import Button from './Button'
38 import Property from '../libraries/model/DescriptorModelMetaProperty'
39 import ComposerAppActions from '../actions/ComposerAppActions'
40 import CatalogItemsActions from '../actions/CatalogItemsActions'
41 import DESCRIPTOR_MODEL_FIELDS from '../libraries/model/DescriptorModelFields'
42 import DescriptorModelFactory from '../libraries/model/DescriptorModelFactory'
43 import DescriptorModelMetaFactory from '../libraries/model/DescriptorModelMetaFactory'
44 import SelectionManager from '../libraries/SelectionManager'
45 import DeletionManager from '../libraries/DeletionManager'
46 import DescriptorModelIconFactory from '../libraries/model/IconFactory'
47 import getEventPath from '../libraries/getEventPath'
48 import CatalogDataStore from '../stores/CatalogDataStore'
49
50 import imgAdd from '../../../node_modules/open-iconic/svg/plus.svg'
51 import imgRemove from '../../../node_modules/open-iconic/svg/trash.svg'
52
53 import '../styles/EditDescriptorModelProperties.scss'
54
55 const EMPTY_LEAF_PRESENT = '--empty-leaf-set--';
56
57 function resolveReactKey(value) {
58 const keyPath = ['uiState', 'fieldKey'];
59 if (!_has(value, keyPath)) {
60 _set(value, keyPath, _uniqueId());
61 }
62 return _get(value, keyPath);
63 }
64
65 function getDescriptorMetaBasicForType(type) {
66 const basicPropertiesFilter = d => _includes(DESCRIPTOR_MODEL_FIELDS[type], d.name);
67 return DescriptorModelMetaFactory.getModelMetaForType(type, basicPropertiesFilter) || {properties: []};
68 }
69
70 function getDescriptorMetaAdvancedForType(type) {
71 const advPropertiesFilter = d => !_includes(DESCRIPTOR_MODEL_FIELDS[type], d.name);
72 return DescriptorModelMetaFactory.getModelMetaForType(type, advPropertiesFilter) || {properties: []};
73 }
74
75 function getTitle(model = {}) {
76 if (typeof model['short-name'] === 'string' && model['short-name']) {
77 return model['short-name'];
78 }
79 if (typeof model.name === 'string' && model.name) {
80 return model.name;
81 }
82 if (model.uiState && typeof model.uiState.displayName === 'string' && model.uiState.displayName) {
83 return model.uiState.displayName
84 }
85 if (typeof model.id === 'string') {
86 return model.id;
87 }
88 }
89
90 export default function EditDescriptorModelProperties(props) {
91
92 const container = props.container;
93
94 if (!(DescriptorModelFactory.isContainer(container))) {
95 return
96 }
97
98 function startEditing() {
99 DeletionManager.removeEventListeners();
100 }
101
102 function endEditing() {
103 DeletionManager.addEventListeners();
104 }
105
106 function onClickSelectItem(property, path, value, event) {
107 event.preventDefault();
108 const root = this.getRoot();
109 if (SelectionManager.select(value)) {
110 CatalogItemsActions.catalogItemMetaDataChanged(root.model);
111 }
112 }
113
114 function onFocusPropertyFormInputElement(property, path, value, event) {
115
116 event.preventDefault();
117 startEditing();
118
119 function removeIsFocusedClass(event) {
120 event.target.removeEventListener('blur', removeIsFocusedClass);
121 Array.from(document.querySelectorAll('.-is-focused')).forEach(d => d.classList.remove('-is-focused'));
122 }
123
124 removeIsFocusedClass(event);
125
126 const propertyWrapper = getEventPath(event).reduce((parent, element) => {
127 if (parent) {
128 return parent;
129 }
130 if (!element.classList) {
131 return false;
132 }
133 if (element.classList.contains('property')) {
134 return element;
135 }
136 }, false);
137
138 if (propertyWrapper) {
139 propertyWrapper.classList.add('-is-focused');
140 event.target.addEventListener('blur', removeIsFocusedClass);
141 }
142
143 }
144
145 function buildAddPropertyAction(container, property, path) {
146 function onClickAddProperty(property, path, event) {
147 event.preventDefault();
148 //SelectionManager.resume();
149 const create = Property.getContainerCreateMethod(property, this);
150 if (create) {
151 const model = null;
152 create(model, path, property);
153 } else {
154 const name = path.join('.');
155 // get a unique name for the new list item based on the current list content
156 // some lists, based on the key, may not get a uniqueName generated here
157 const uniqueName = DescriptorModelMetaFactory.generateItemUniqueName(container.model[property.name], property);
158 const value = Property.createModelInstance(property, uniqueName);
159 utils.assignPathValue(this.model, name, value);
160 }
161 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
162 }
163 return (
164 <Button className="inline-hint" onClick={onClickAddProperty.bind(container, property, path)} label="Add" src={imgAdd} />
165 );
166 }
167
168 function buildRemovePropertyAction(container, property, path) {
169 function onClickRemoveProperty(property, path, event) {
170 event.preventDefault();
171 const name = path.join('.');
172 const removeMethod = Property.getContainerMethod(property, this, 'remove');
173 if (removeMethod) {
174 removeMethod(utils.resolvePath(this.model, name));
175 } else {
176 utils.removePathValue(this.model, name);
177 }
178 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
179 }
180 return (
181 <Button className="remove-property-action inline-hint" title="Remove" onClick={onClickRemoveProperty.bind(container, property, path)} label="Remove" src={imgRemove}/>
182 );
183 }
184
185 function buildField(container, property, path, value, fieldKey) {
186 let cds = CatalogDataStore;
187 let catalogs = cds.getTransientCatalogs();
188
189 const pathToProperty = path.join('.');
190 const isEditable = true;
191 const isGuid = Property.isGuid(property);
192 const isBoolean = Property.isBoolean(property);
193 const isEnumeration = Property.isEnumeration(property);
194 const isLeafRef = Property.isLeafRef(property);
195 const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
196 const placeholder = changeCase.title(property.name);
197 const className = ClassNames(property.name + '-input', {'-is-guid': isGuid});
198 const fieldValue = value ? (value.constructor.name != "Object") ? value : '' : (isNaN(value) ? undefined : value);
199
200 // process the named field value change
201 function processFieldValueChange(name, value) {
202 console.debug('processed change for -- ' + name + ' -- with value -- ' + value);
203 // this = the container being edited
204 if (DescriptorModelFactory.isContainer(this)) {
205 utils.assignPathValue(this.model, name, value);
206 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
207 }
208 }
209
210 // change handler used for onChange event
211 const changeHandler = (handleValueChange, event) => {
212 event.preventDefault();
213 console.debug(event.target.value);
214 handleValueChange(event.target.value);
215 };
216 // create an onChange event handler for a text field for the specified field path (debounced to accumulate chars)
217 const onTextChange = changeHandler.bind(null, _debounce(
218 processFieldValueChange.bind(container, pathToProperty), 2000, {maxWait: 5000})); // max wait for short-name
219 // create an onChange event handler for a select field for the specified field path
220 const onSelectChange = changeHandler.bind(null, processFieldValueChange.bind(container, pathToProperty));
221
222 if (isEnumeration) {
223 const enumeration = Property.getEnumeration(property, value);
224 const options = enumeration.map((d, i) => {
225 // note yangforge generates values for enums but the system does not use them
226 // so we categorically ignore them
227 // https://trello.com/c/uzEwVx6W/230-bug-enum-should-not-use-index-only-name
228 //return <option key={fieldKey + ':' + i} value={d.value}>{d.name}</option>;
229 return <option key={':' + i} value={d.name}>{d.name}</option>;
230 });
231 const isValueSet = enumeration.filter(d => d.isSelected).length > 0;
232 if (!isValueSet || property.cardinality === '0..1') {
233 const noValueDisplayText = changeCase.title(property.name);
234 options.unshift(<option key={'(value-not-in-enum)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
235 }
236 return (
237 <select
238 key={fieldKey}
239 id={fieldKey}
240 className={ClassNames({'-value-not-set': !isValueSet})}
241 defaultValue={value}
242 title={pathToProperty}
243 onChange={onSelectChange}
244 onFocus={onFocus}
245 onBlur={endEditing}
246 onMouseDown={startEditing}
247 onMouseOver={startEditing}
248 readOnly={!isEditable}>
249 {options}
250 </select>
251 );
252 }
253
254 if (isLeafRef) {
255 let fullPathString = container.key + ':' + path.join(':');
256 let containerRef = container;
257 while (containerRef.parent) {
258 fullPathString = containerRef.parent.key + ':' + fullPathString;
259 containerRef = containerRef.parent;
260 }
261 const leafRefPathValues = Property.getLeafRef(property, path, value, fullPathString, catalogs, container);
262
263 const options = leafRefPathValues && leafRefPathValues.map((d, i) => {
264 return <option key={':' + i} value={d.value}>{d.value}</option>;
265 });
266 const isValueSet = leafRefPathValues.filter(d => d.isSelected).length > 0;
267 if (!isValueSet || property.cardinality === '0..1') {
268 const noValueDisplayText = changeCase.title(property.name);
269 options.unshift(<option key={'(value-not-in-leafref)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
270 }
271 return (
272 <select
273 key={fieldKey}
274 id={fieldKey}
275 className={ClassNames({'-value-not-set': !isValueSet})}
276 defaultValue={value}
277 title={pathToProperty}
278 onChange={onSelectChange}
279 onFocus={onFocus}
280 onBlur={endEditing}
281 onMouseDown={startEditing}
282 onMouseOver={startEditing}
283 readOnly={!isEditable}>
284 {options}
285 </select>
286 );
287 }
288
289 if (isBoolean) {
290 const options = [
291 <option key={'true'} value="TRUE">TRUE</option>,
292 <option key={'false'} value="FALSE">FALSE</option>
293 ]
294
295 // if (!isValueSet) {
296 const noValueDisplayText = changeCase.title(property.name);
297 options.unshift(<option key={'(value-not-in-leafref)'} value="" placeholder={placeholder}></option>);
298 // }
299 let val = value;
300 if(typeof(val) == 'number') {
301 val = value ? "TRUE" : "FALSE"
302 }
303 const isValueSet = (val != '' && val)
304 return (
305 <select
306 key={fieldKey}
307 id={fieldKey}
308 className={ClassNames({'-value-not-set': !isValueSet})}
309 defaultValue={val && val.toUpperCase()}
310 title={pathToProperty}
311 onChange={onSelectChange}
312 onFocus={onFocus}
313 onBlur={endEditing}
314 onMouseDown={startEditing}
315 onMouseOver={startEditing}
316 readOnly={!isEditable}>
317 {options}
318 </select>
319 );
320 }
321
322 if (Property.isLeafEmpty(property)) {
323 // A null value indicates the leaf exists (as opposed to undefined).
324 // We stick in a string when the user actually sets it to simplify things
325 // but the correct thing happens when we serialize to user data
326 let isEmptyLeafPresent = (value === EMPTY_LEAF_PRESENT || value === null);
327 let present = isEmptyLeafPresent ? EMPTY_LEAF_PRESENT : "";
328 const options = [
329 <option key={'true'} value={EMPTY_LEAF_PRESENT}>Enabled</option>,
330 <option key={'false'} value="">Not Enabled</option>
331 ]
332
333 return (
334 <select
335 key={fieldKey}
336 id={fieldKey}
337 className={ClassNames({'-value-not-set': !isEmptyLeafPresent})}
338 defaultValue={present}
339 title={pathToProperty}
340 onChange={onSelectChange}
341 onFocus={onFocus}
342 onBlur={endEditing}
343 onMouseDown={startEditing}
344 onMouseOver={startEditing}
345 readOnly={!isEditable}>
346 {options}
347 </select>
348 );
349 }
350
351 if (property['preserve-line-breaks']) {
352 return (
353 <textarea
354 key={fieldKey}
355 cols="5"
356 id={fieldKey}
357 defaultValue={value}
358 placeholder={placeholder}
359 onChange={onTextChange}
360 onFocus={onFocus}
361 onBlur={endEditing}
362 onMouseDown={startEditing}
363 onMouseOver={startEditing}
364 onMouseOut={endEditing}
365 onMouseLeave={endEditing}
366 readOnly={!isEditable} />
367 );
368 }
369
370 return (
371 <input
372 key={fieldKey}
373 id={fieldKey}
374 type="text"
375 defaultValue={fieldValue}
376 className={className}
377 placeholder={placeholder}
378 onChange={onTextChange}
379 onFocus={onFocus}
380 onBlur={endEditing}
381 onMouseDown={startEditing}
382 onMouseOver={startEditing}
383 onMouseOut={endEditing}
384 onMouseLeave={endEditing}
385 readOnly={!isEditable}
386 />
387 );
388
389 }
390
391 function buildElement(container, property, valuePath, value) {
392 return property.properties.map((property, index) => {
393 let childValue;
394 const childPath = valuePath.slice();
395 if (typeof value === 'object') {
396 childValue = value[property.name];
397 }
398 if(property.type != 'choice'){
399 childPath.push(property.name);
400 }
401 return build(container, property, childPath, childValue);
402
403 });
404 }
405
406 function buildChoice(container, property, path, value, key) {
407
408 function processChoiceChange(name, value) {
409 if (DescriptorModelFactory.isContainer(this)) {
410
411 /*
412 Transient State is stored for convenience in the uiState field.
413 The choice yang type uses case elements to describe the "options".
414 A choice can only ever have one option selected which allows
415 the system to determine which type is selected by the name of
416 the element contained within the field.
417 */
418 /*
419 const stateExample = {
420 uiState: {
421 choice: {
422 'conf-config': {
423 selected: 'rest',
424 'case': {
425 rest: {},
426 netconf: {},
427 script: {}
428 }
429 }
430 }
431 }
432 };
433 */
434 const statePath = ['uiState.choice'].concat(name);
435 const stateObject = utils.resolvePath(this.model, statePath.join('.')) || {};
436 const selected = stateObject.selected ? stateObject.selected.split('.')[1] : undefined;
437 // write state back to the model so the new state objects are captured
438 utils.assignPathValue(this.model, statePath.join('.'), stateObject);
439
440 // write the current choice value into the state
441 let choiceObject = utils.resolvePath(this.model, [name, selected].join('.'));
442 let isTopCase = false;
443 if (!choiceObject) {
444 isTopCase = true;
445 choiceObject = utils.resolvePath(this.model, [selected].join('.'));
446 }
447 utils.assignPathValue(stateObject, [selected].join('.'), _cloneDeep(choiceObject));
448
449 if(selected) {
450 if(this.model.uiState.choice.hasOwnProperty(name)) {
451 delete this.model[selected];
452 utils.removePathValue(this.model, [name, selected].join('.'), isTopCase);
453 } else {
454 // remove the current choice value from the model
455 utils.removePathValue(this.model, [name, selected].join('.'), isTopCase);
456 }
457 }
458
459 // get any state for the new selected choice
460 const newChoiceObject = utils.resolvePath(stateObject, [value].join('.')) || {};
461
462 // assign new choice value to the model
463 if (isTopCase) {
464 utils.assignPathValue(this.model, [name, value].join('.'), newChoiceObject);
465 } else {
466 utils.assignPathValue(this.model, [value].join('.'), newChoiceObject)
467 }
468
469 // update the selected name
470 utils.assignPathValue(this.model, statePath.concat('selected').join('.'), value);
471
472 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
473 }
474 }
475
476 const pathToChoice = path.join('.');
477 const caseByNameMap = {};
478
479 const choiceChangeHandler = processChoiceChange.bind(container, pathToChoice);
480 const onChange = ((handleChoiceChange, event) => {
481 event.preventDefault();
482 handleChoiceChange(event.target.value);
483 }).bind(null, choiceChangeHandler);
484
485
486 const cases = property.properties.map(d => {
487 if (d.type === 'case') {
488 //Previous it was assumed that a case choice would have only one property. Now we pass on either the only item or the
489 caseByNameMap[d.name] = d.properties && (d.properties.length == 1 ? d.properties[0] : d.properties);
490 return {
491 optionName: d.name,
492 optionTitle: d.description,
493 //represents case name and case element name
494 optionValue: [d.name, d.properties[0].name].join('.')
495 };
496 }
497 caseByNameMap[d.name] = d;
498 return {optionName: d.name};
499 });
500
501 const options = [{optionName: '', optionValue: false}].concat(cases).map((d, i) => {
502 return (
503 <option key={i} value={d.optionValue} title={d.optionTitle}>
504 {d.optionName}
505 {i ? null : changeCase.title(property.name)}
506 </option>
507 );
508 });
509
510 let selectedOptionPath = ['uiState.choice', pathToChoice, 'selected'].join('.');
511 //Currently selected choice/case statement on UI model
512 let selectedOptionValue = utils.resolvePath(container.model, selectedOptionPath);
513 //If first time loaded, and none is selected, check if there is a value corresponding to a case statement in the container model
514 if(!selectedOptionValue) {
515 //get field properties for choice on container model
516 let fieldProperties = utils.resolvePath(container.model, pathToChoice);
517 if(fieldProperties) {
518 //Check each case statement in model and see if it is present in container model.
519 cases.map(function(c){
520 if(fieldProperties.hasOwnProperty(c.optionValue.split('.')[1])) {
521 utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), c.optionValue);
522 }
523 });
524 selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'));
525 } else {
526 property.properties.map(function(p) {
527 let pname = p.properties[0].name;
528 if(container.model.hasOwnProperty(pname)) {
529 utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), [p.name, pname].join('.'));
530 }
531 })
532 selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'));
533 }
534 }
535 //If selectedOptionValue is present, take first item in string which represents the case name.
536 const valueProperty = caseByNameMap[selectedOptionValue ? selectedOptionValue.split('.')[0] : undefined] || {properties: []};
537 const isLeaf = Property.isLeaf(valueProperty);
538 const hasProperties = _isArray(valueProperty.properties) && valueProperty.properties.length;
539 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(valueProperty);
540 //Some magic that prevents errors for arising
541 const valueResponse = valueProperty.properties && valueProperty.properties.length ? valueProperty.properties.map(valuePropertyFn) : (!isMissingDescriptorMeta) ? build(container, valueProperty, path.concat(valueProperty.name), utils.resolvePath(container.model, path.concat(valueProperty.name).join('.')) || container.model[valueProperty.name]) :
542 valueProperty.map && valueProperty.map(valuePropertyFn);
543 function valuePropertyFn(d, i) {
544 const childPath = path.concat(valueProperty.name, d.name);
545 const childValue = utils.resolvePath(container.model, childPath.join('.'));
546 return (
547 <div key={childPath.concat('info', i).join(':')}>
548 {build(container, d, childPath, childValue, props)}
549 </div>
550 );
551 }
552 // end magic
553 const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
554
555 return (
556 <div key={key} className="choice">
557 <select
558 key={Date.now()}
559 className={ClassNames({'-value-not-set': !selectedOptionValue})}
560 defaultValue={selectedOptionValue}
561 onChange={onChange}
562 onFocus={onFocus}
563 onBlur={endEditing}
564 onMouseDown={startEditing}
565 onMouseOver={startEditing}
566 onMouseOut={endEditing}
567 onMouseLeave={endEditing}
568 >
569 {options}
570 </select>
571 {valueResponse}
572 </div>
573 );
574
575 }
576
577 function buildSimpleListItem(container, property, path, value, uniqueId, index) {
578 // todo need to abstract this better
579 const title = getTitle(value);
580 var req = require.context("../", true, /\.svg/);
581 return (
582 <div key={uniqueId} >
583 <a href="#select-list-item" className={property.name + '-list-item simple-list-item '} onClick={onClickSelectItem.bind(container, property, path, value)}>
584 <img src={req('./' + DescriptorModelIconFactory.getUrlForType(property.name))} width="20px" />
585 <span>{title}</span>
586 </a>
587 {buildRemovePropertyAction(container, property, path)}
588 </div>
589 );
590 }
591
592 function buildRemoveListItem(container, property, valuePath, index) {
593 const className = ClassNames(property.name + '-remove actions');
594 return (
595 <div className={className}>
596 <h3>
597 <span className={property.type + '-name name'}>{changeCase.title(property.name)}</span>
598 <span className="info">{index + 1}</span>
599 {buildRemovePropertyAction(container, property, valuePath)}
600 </h3>
601 </div>
602 );
603 }
604
605 function buildLeafListItem(container, property, valuePath, value, uniqueId, index) {
606 // look at the type to determine how to parse the value
607 return (
608 <div key={uniqueId}>
609 {buildRemoveListItem(container, property, valuePath, index)}
610 {buildField(container, property, valuePath, value, uniqueId)}
611 </div>
612
613 );
614 }
615
616 function build(container, property, path, value, props = {}) {
617
618 const fields = [];
619 const isLeaf = Property.isLeaf(property);
620 const isArray = Property.isArray(property);
621 const isObject = Property.isObject(property);
622 const isLeafList = Property.isLeafList(property);
623 const isRequired = Property.isRequired(property);
624 const title = changeCase.titleCase(property.name);
625 const columnCount = property.properties.length || 1;
626 const isColumnar = isArray && (Math.round(props.width / columnCount) > 155);
627 const classNames = {'-is-required': isRequired, '-is-columnar': isColumnar};
628
629 // create a unique Id for use as react component keys and html element ids
630 // use uid (from ui info) instead of id property (which is not stable)
631 let uniqueId = container.uid;
632 let containerRef = container;
633 while (containerRef.parent) {
634 uniqueId = containerRef.parent.uid + ':' + uniqueId;
635 containerRef = containerRef.parent;
636 }
637 uniqueId += ':' + path.join(':')
638
639 if (!property.properties && isObject) {
640 const uiState = DescriptorModelMetaFactory.getModelMetaForType(property.name) || {};
641 property.properties = uiState.properties;
642 }
643
644 const hasProperties = _isArray(property.properties) && property.properties.length;
645 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(property);
646
647 // ensure value is not undefined for non-leaf property types
648 if (isObject) {
649 if (typeof value !== 'object') {
650 value = isArray ? [] : {};
651 }
652 }
653 const valueAsArray = _isArray(value) ? value : isLeafList && typeof value === 'undefined' ? [] : [value];
654
655 const isMetaField = property.name === 'meta';
656 const isCVNFD = property.name === 'constituent-vnfd';
657 const isSimpleListView = Property.isSimpleList(property);
658
659 valueAsArray.forEach((value, index) => {
660
661 let field;
662 const valuePath = path.slice();
663 // create a unique field Id for use as react component keys and html element ids
664 // notes:
665 // keys only need to be unique on components in the same array
666 // html element ids should be unique with the document (or form)
667 let fieldId = uniqueId;
668
669 if (isArray) {
670 valuePath.push(index);
671 fieldId = resolveReactKey(value);
672 }
673
674 if (isMetaField) {
675 if (typeof value === 'object') {
676 value = JSON.stringify(value, undefined, 12);
677 } else if (typeof value !== 'string') {
678 value = '{}';
679 }
680 }
681
682 if (isMissingDescriptorMeta) {
683 field = <span key={fieldId} className="warning">No Descriptor Meta for {property.name}</span>;
684 } else if (property.type === 'choice') {
685 field = buildChoice(container, property, valuePath, value, fieldId);
686 } else if (isSimpleListView) {
687 field = buildSimpleListItem(container, property, valuePath, value, fieldId, index);
688 } else if (isLeafList) {
689 field = buildLeafListItem(container, property, valuePath, value, fieldId, index);
690 } else if (hasProperties) {
691 field = buildElement(container, property, valuePath, value, fieldId)
692 } else {
693 field = buildField(container, property, valuePath, value, fieldId);
694 }
695
696 function onClickLeaf(property, path, value, event) {
697 if (event.isDefaultPrevented()) {
698 return;
699 }
700 event.preventDefault();
701 event.stopPropagation();
702 this.getRoot().uiState.focusedPropertyPath = path.join('.');
703 console.debug('property selected', path.join('.'));
704 ComposerAppActions.propertySelected([path.join('.')]);
705 }
706
707 const clickHandler = isLeaf ? onClickLeaf : () => {};
708 const isContainerList = isArray && !(isSimpleListView || isLeafList);
709
710 fields.push(
711 <div key={fieldId}
712 className={ClassNames('property-content', {'simple-list': isSimpleListView})}
713 onClick={clickHandler.bind(container, property, valuePath, value)}>
714 {isContainerList ? buildRemoveListItem(container, property, valuePath, index) : null}
715 {field}
716 </div>
717 );
718
719 });
720
721 classNames['-is-leaf'] = isLeaf;
722 classNames['-is-array'] = isArray;
723 classNames['cols-' + columnCount] = isColumnar;
724
725 if (property.type === 'choice') {
726 value = utils.resolvePath(container.model, ['uiState.choice'].concat(path, 'selected').join('.'));
727 if(!value) {
728 property.properties.map(function(p) {
729 let pname = p.properties[0].name;
730 if(container.model.hasOwnProperty(pname)) {
731 value = container.model[pname];
732 }
733 })
734 }
735 }
736
737 let displayValue = typeof value === 'object' ? '' : value;
738 const displayValueInfo = isArray ? valueAsArray.filter(d => typeof d !== 'undefined').length + ' items' : '';
739
740 const onFocus = isLeaf ? event => event.target.classList.add('-is-focused') : false;
741
742 return (
743 <div key={uniqueId} className={ClassNames(property.type + '-property property', classNames)} onFocus={onFocus}>
744 <h3 className="property-label">
745 <label htmlFor={uniqueId}>
746 <span className={property.type + '-name name'}>{title}</span>
747 <small>
748 <span className={property.type + '-info info'}>{displayValueInfo}</span>
749 <span className={property.type + '-value value'}>{displayValue}</span>
750 </small>
751 {isArray ? buildAddPropertyAction(container, property, path.concat(valueAsArray.length)) : null}
752 </label>
753 </h3>
754 <span className={property.type + '-description description'}>{property.description}</span>
755 <val className="property-value">
756 {isCVNFD ? <span className={property.type + '-tip tip'}>Drag a VNFD from the Catalog to add more.</span> : null}
757 {fields}
758 </val>
759 </div>
760 );
761
762 }
763
764 const containerType = container.uiState['qualified-type'] || container.uiState.type;
765 const basicProperties = getDescriptorMetaBasicForType(containerType).properties;
766
767 function buildBasicGroup() {
768 if (basicProperties.length === 0) {
769 return null;
770 }
771 return (
772 <div className="basic-properties-group">
773 <h2>Basic</h2>
774 <div>
775 {basicProperties.map(property => {
776 const path = [property.name];
777 const value = container.model[property.name];
778 return build(container, property, path, value);
779 })}
780 </div>
781 </div>
782 );
783 }
784
785 function buildAdvancedGroup() {
786 const properties = getDescriptorMetaAdvancedForType(containerType).properties;
787 if (properties.length === 0) {
788 return null;
789 }
790 const hasBasicFields = basicProperties.length > 0;
791 const closeGroup = basicProperties.length > 0;
792 return (
793 <div className="advanced-properties-group">
794 <h1 data-toggle={closeGroup ? 'true' : 'false'} className={ClassNames({'-is-toggled': closeGroup})} onClick={toggle} style={{display: hasBasicFields ? 'block' : 'none'}}>
795 <a className="toggle-show-more" href="#show-more-properties">more&hellip;</a>
796 <a className="toggle-show-less" href="#show-more-properties">less&hellip;</a>
797 </h1>
798 <div className="toggleable">
799 {properties.map(property => {
800 const path = [property.name];
801 const value = container.model[property.name];
802 return build(container, property, path, value, {toggle: true, width: props.width});
803 })}
804 </div>
805 <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>
806 </div>
807 );
808 }
809
810 function buildMoreLess(d, i) {
811 return (
812 <span key={'bread-crumb-part-' + i}>
813 <a href="#select-item" onClick={onClickSelectItem.bind(d, null, null, d)}>{d.title}</a>
814 <i> / </i>
815 </span>
816 );
817 }
818
819 const path = [];
820 if (container.parent) {
821 path.push(container.parent);
822 }
823 path.push(container);
824
825 return (
826 <div className="EditDescriptorModelProperties -is-tree-view">
827 <h1>{path.map(buildMoreLess)}</h1>
828 {buildBasicGroup()}
829 {buildAdvancedGroup()}
830 </div>
831 );
832 };
833