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