Merge "Support new package file management scheme - lots of changes"
[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 /**
392 * buiid and return an array of components representing an editor for each property.
393 *
394 * @param {any} container the master document being edited
395 * @param {[property]} properties
396 * @param {string} pathToProperties path within the container to the properties
397 * @param {Object} data source for each property
398 * @param {any} props object containing main data panel information, e.g. panel width {width: 375}
399 * which may be useful/necessary to a components rendering.
400 * @returns an array of react components
401 */
402 function buildComponentsForProperties(container, properties, pathToProperties, data, props) {
403 return properties.map((property) => {
404 let value;
405 let propertyPath = pathToProperties.slice();
406 if (data && typeof data === 'object') {
407 value = data[property.name];
408 }
409 if(property.type != 'choice'){
410 propertyPath.push(property.name);
411 }
412 return build(container, property, propertyPath, value, props);
413 });
414 }
415
416 function buildElement(container, property, valuePath, value) {
417 return buildComponentsForProperties(container, property.properties, valuePath, value);
418 }
419
420 function buildChoice(container, property, path, value, key) {
421
422 function processChoiceChange(name, value) {
423 if (DescriptorModelFactory.isContainer(this)) {
424
425 /*
426 Transient State is stored for convenience in the uiState field.
427 The choice yang type uses case elements to describe the "options".
428 A choice can only ever have one option selected which allows
429 the system to determine which type is selected by the name of
430 the element contained within the field.
431 */
432 /*
433 const stateExample = {
434 uiState: {
435 choice: {
436 'conf-config': {
437 selected: 'rest',
438 'case': {
439 rest: {},
440 netconf: {},
441 script: {}
442 }
443 }
444 }
445 }
446 };
447 */
448 const statePath = ['uiState.choice'].concat(name);
449 const stateObject = utils.resolvePath(this.model, statePath.join('.')) || {};
450 const selected = stateObject.selected ? stateObject.selected.split('.')[1] : undefined;
451 // write state back to the model so the new state objects are captured
452 utils.assignPathValue(this.model, statePath.join('.'), stateObject);
453
454 // write the current choice value into the state
455 let choiceObject = utils.resolvePath(this.model, [name, selected].join('.'));
456 let isTopCase = false;
457 if (!choiceObject) {
458 isTopCase = true;
459 choiceObject = utils.resolvePath(this.model, [selected].join('.'));
460 }
461 utils.assignPathValue(stateObject, [selected].join('.'), _cloneDeep(choiceObject));
462
463 if(selected) {
464 if(this.model.uiState.choice.hasOwnProperty(name)) {
465 delete this.model[selected];
466 utils.removePathValue(this.model, [name, selected].join('.'), isTopCase);
467 } else {
468 // remove the current choice value from the model
469 utils.removePathValue(this.model, [name, selected].join('.'), isTopCase);
470 }
471 }
472
473 // get any state for the new selected choice
474 const newChoiceObject = utils.resolvePath(stateObject, [value].join('.')) || {};
475
476 // assign new choice value to the model
477 if (isTopCase) {
478 utils.assignPathValue(this.model, [name, value].join('.'), newChoiceObject);
479 } else {
480 utils.assignPathValue(this.model, [value].join('.'), newChoiceObject)
481 }
482
483 // update the selected name
484 utils.assignPathValue(this.model, statePath.concat('selected').join('.'), value);
485
486 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
487 }
488 }
489
490 const pathToChoice = path.join('.');
491 const caseByNameMap = {};
492
493 const choiceChangeHandler = processChoiceChange.bind(container, pathToChoice);
494 const onChange = ((handleChoiceChange, event) => {
495 event.preventDefault();
496 handleChoiceChange(event.target.value);
497 }).bind(null, choiceChangeHandler);
498
499
500 const cases = property.properties.map(d => {
501 if (d.type === 'case') {
502 //Previous it was assumed that a case choice would have only one property. Now we pass on either the only item or the
503 caseByNameMap[d.name] = d.properties && (d.properties.length == 1 ? d.properties[0] : d.properties);
504 return {
505 optionName: d.name,
506 optionTitle: d.description,
507 //represents case name and case element name
508 optionValue: [d.name, d.properties[0].name].join('.')
509 };
510 }
511 caseByNameMap[d.name] = d;
512 return {optionName: d.name};
513 });
514
515 const options = [{optionName: '', optionValue: false}].concat(cases).map((d, i) => {
516 return (
517 <option key={i} value={d.optionValue} title={d.optionTitle}>
518 {d.optionName}
519 {i ? null : changeCase.title(property.name)}
520 </option>
521 );
522 });
523
524 let selectedOptionPath = ['uiState.choice', pathToChoice, 'selected'].join('.');
525 //Currently selected choice/case statement on UI model
526 let selectedOptionValue = utils.resolvePath(container.model, selectedOptionPath);
527 //If first time loaded, and none is selected, check if there is a value corresponding to a case statement in the container model
528 if(!selectedOptionValue) {
529 //get field properties for choice on container model
530 let fieldProperties = utils.resolvePath(container.model, pathToChoice);
531 if(fieldProperties) {
532 //Check each case statement in model and see if it is present in container model.
533 cases.map(function(c){
534 if(fieldProperties.hasOwnProperty(c.optionValue.split('.')[1])) {
535 utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), c.optionValue);
536 }
537 });
538 selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'));
539 } else {
540 property.properties.map(function(p) {
541 let pname = p.properties[0].name;
542 if(container.model.hasOwnProperty(pname)) {
543 utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), [p.name, pname].join('.'));
544 }
545 })
546 selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'));
547 }
548 }
549 //If selectedOptionValue is present, take first item in string which represents the case name.
550 const valueProperty = caseByNameMap[selectedOptionValue ? selectedOptionValue.split('.')[0] : undefined] || {properties: []};
551 const isLeaf = Property.isLeaf(valueProperty);
552 const hasProperties = _isArray(valueProperty.properties) && valueProperty.properties.length;
553 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(valueProperty);
554 //Some magic that prevents errors for arising
555 let valueResponse = null;
556 if (valueProperty.properties && valueProperty.properties.length) {
557 valueResponse = valueProperty.properties.map(valuePropertyFn);
558 } else if (!isMissingDescriptorMeta) {
559 let value = utils.resolvePath(container.model, path.concat(valueProperty.name).join('.')) || container.model[valueProperty.name];
560 valueResponse = build(container, valueProperty, path.concat(valueProperty.name), value)
561 } else {
562 valueResponse = valueProperty.map && valueProperty.map(valuePropertyFn);
563 }
564 function valuePropertyFn(d, i) {
565 const childPath = path.concat(valueProperty.name, d.name);
566 const childValue = utils.resolvePath(container.model, childPath.join('.'));
567 return (
568 <div key={childPath.concat('info', i).join(':')}>
569 {build(container, d, childPath, childValue, props)}
570 </div>
571 );
572 }
573 // end magic
574 const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
575
576 return (
577 <div key={key} className="choice">
578 <select
579 key={Date.now()}
580 className={ClassNames({'-value-not-set': !selectedOptionValue})}
581 defaultValue={selectedOptionValue}
582 onChange={onChange}
583 onFocus={onFocus}
584 onBlur={endEditing}
585 onMouseDown={startEditing}
586 onMouseOver={startEditing}
587 onMouseOut={endEditing}
588 onMouseLeave={endEditing}
589 >
590 {options}
591 </select>
592 {valueResponse}
593 </div>
594 );
595
596 }
597
598 function buildSimpleListItem(container, property, path, value, uniqueId, index) {
599 // todo need to abstract this better
600 const title = getTitle(value);
601 var req = require.context("../", true, /\.svg/);
602 return (
603 <div key={uniqueId} >
604 <a href="#select-list-item" className={property.name + '-list-item simple-list-item '} onClick={onClickSelectItem.bind(container, property, path, value)}>
605 <img src={req('./' + DescriptorModelIconFactory.getUrlForType(property.name))} width="20px" />
606 <span>{title}</span>
607 </a>
608 {buildRemovePropertyAction(container, property, path)}
609 </div>
610 );
611 }
612
613 function buildRemoveListItem(container, property, valuePath, index) {
614 const className = ClassNames(property.name + '-remove actions');
615 return (
616 <div className={className}>
617 <h3>
618 <span className={property.type + '-name name'}>{changeCase.title(property.name)}</span>
619 <span className="info">{index + 1}</span>
620 {buildRemovePropertyAction(container, property, valuePath)}
621 </h3>
622 </div>
623 );
624 }
625
626 function buildLeafListItem(container, property, valuePath, value, uniqueId, index) {
627 // look at the type to determine how to parse the value
628 return (
629 <div key={uniqueId}>
630 {buildRemoveListItem(container, property, valuePath, index)}
631 {buildField(container, property, valuePath, value, uniqueId)}
632 </div>
633
634 );
635 }
636
637 function build(container, property, path, value, props = {}) {
638
639 const fields = [];
640 const isLeaf = Property.isLeaf(property);
641 const isArray = Property.isArray(property);
642 const isObject = Property.isObject(property);
643 const isLeafList = Property.isLeafList(property);
644 const isRequired = Property.isRequired(property);
645 const title = changeCase.titleCase(property.name);
646 const columnCount = property.properties.length || 1;
647 const isColumnar = isArray && (Math.round(props.width / columnCount) > 155);
648 const classNames = {'-is-required': isRequired, '-is-columnar': isColumnar};
649
650 // create a unique Id for use as react component keys and html element ids
651 // use uid (from ui info) instead of id property (which is not stable)
652 let uniqueId = container.uid;
653 let containerRef = container;
654 while (containerRef.parent) {
655 uniqueId = containerRef.parent.uid + ':' + uniqueId;
656 containerRef = containerRef.parent;
657 }
658 uniqueId += ':' + path.join(':')
659
660 if (!property.properties && isObject) {
661 const uiState = DescriptorModelMetaFactory.getModelMetaForType(property.name) || {};
662 property.properties = uiState.properties;
663 }
664
665 const hasProperties = _isArray(property.properties) && property.properties.length;
666 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(property);
667
668 // ensure value is not undefined for non-leaf property types
669 if (isObject) {
670 if (typeof value !== 'object') {
671 value = isArray ? [] : {};
672 }
673 }
674 const valueAsArray = _isArray(value) ? value : isLeafList && typeof value === 'undefined' ? [] : [value];
675
676 const isMetaField = property.name === 'meta';
677 const isCVNFD = property.name === 'constituent-vnfd';
678 const isSimpleListView = Property.isSimpleList(property);
679
680 valueAsArray.forEach((value, index) => {
681
682 let field;
683 const valuePath = path.slice();
684 // create a unique field Id for use as react component keys and html element ids
685 // notes:
686 // keys only need to be unique on components in the same array
687 // html element ids should be unique with the document (or form)
688 let fieldId = uniqueId;
689
690 if (isArray) {
691 valuePath.push(index);
692 fieldId = isLeafList ? fieldId + index + value : resolveReactKey(value);
693 }
694
695 if (isMetaField) {
696 if (typeof value === 'object') {
697 value = JSON.stringify(value, undefined, 12);
698 } else if (typeof value !== 'string') {
699 value = '{}';
700 }
701 }
702
703 if (isMissingDescriptorMeta) {
704 field = <span key={fieldId} className="warning">No Descriptor Meta for {property.name}</span>;
705 } else if (property.type === 'choice') {
706 field = buildChoice(container, property, valuePath, value, fieldId);
707 } else if (isSimpleListView) {
708 field = buildSimpleListItem(container, property, valuePath, value, fieldId, index);
709 } else if (isLeafList) {
710 field = buildLeafListItem(container, property, valuePath, value, fieldId, index);
711 } else if (hasProperties) {
712 field = buildElement(container, property, valuePath, value, fieldId)
713 } else {
714 field = buildField(container, property, valuePath, value, fieldId);
715 }
716
717 function onClickLeaf(property, path, value, event) {
718 if (event.isDefaultPrevented()) {
719 return;
720 }
721 event.preventDefault();
722 event.stopPropagation();
723 this.getRoot().uiState.focusedPropertyPath = path.join('.');
724 console.debug('property selected', path.join('.'));
725 ComposerAppActions.propertySelected([path.join('.')]);
726 }
727
728 const clickHandler = isLeaf ? onClickLeaf : () => {};
729 const isContainerList = isArray && !(isSimpleListView || isLeafList);
730
731 fields.push(
732 <div key={fieldId}
733 className={ClassNames('property-content', {'simple-list': isSimpleListView})}
734 onClick={clickHandler.bind(container, property, valuePath, value)}>
735 {isContainerList ? buildRemoveListItem(container, property, valuePath, index) : null}
736 {field}
737 </div>
738 );
739
740 });
741
742 classNames['-is-leaf'] = isLeaf;
743 classNames['-is-array'] = isArray;
744 classNames['cols-' + columnCount] = isColumnar;
745
746 if (property.type === 'choice') {
747 value = utils.resolvePath(container.model, ['uiState.choice'].concat(path, 'selected').join('.'));
748 if(!value) {
749 property.properties.map(function(p) {
750 let pname = p.properties[0].name;
751 if(container.model.hasOwnProperty(pname)) {
752 value = container.model[pname];
753 }
754 })
755 }
756 }
757
758 let displayValue = typeof value === 'object' ? '' : value;
759 const displayValueInfo = isArray ? valueAsArray.filter(d => typeof d !== 'undefined').length + ' items' : '';
760
761 const onFocus = isLeaf ? event => event.target.classList.add('-is-focused') : false;
762
763 return (
764 <div key={uniqueId} className={ClassNames(property.type + '-property property', classNames)} onFocus={onFocus}>
765 <h3 className="property-label">
766 <label htmlFor={uniqueId}>
767 <span className={property.type + '-name name'}>{title}</span>
768 <small>
769 <span className={property.type + '-info info'}>{displayValueInfo}</span>
770 <span className={property.type + '-value value'}>{displayValue}</span>
771 </small>
772 {isArray ? buildAddPropertyAction(container, property, path.concat(valueAsArray.length)) : null}
773 </label>
774 </h3>
775 <span className={property.type + '-description description'}>{property.description}</span>
776 <val className="property-value">
777 {isCVNFD ? <span className={property.type + '-tip tip'}>Drag a VNFD from the Catalog to add more.</span> : null}
778 {fields}
779 </val>
780 </div>
781 );
782
783 }
784
785 const containerType = container.uiState['qualified-type'] || container.uiState.type;
786 const basicProperties = getDescriptorMetaBasicForType(containerType).properties;
787
788 function buildBasicGroup() {
789 if (basicProperties.length === 0) {
790 return null;
791 }
792 return (
793 <div className="basic-properties-group">
794 <h2>Basic</h2>
795 <div>
796 {buildComponentsForProperties(container, basicProperties, [], container.model)}
797 </div>
798 </div>
799 );
800 }
801
802 function buildAdvancedGroup() {
803 const properties = getDescriptorMetaAdvancedForType(containerType).properties;
804 if (properties.length === 0) {
805 return null;
806 }
807 const hasBasicFields = basicProperties.length > 0;
808 const closeGroup = basicProperties.length > 0;
809 return (
810 <div className="advanced-properties-group">
811 <h1 data-toggle={closeGroup ? 'true' : 'false'} className={ClassNames({'-is-toggled': closeGroup})} onClick={toggle} style={{display: hasBasicFields ? 'block' : 'none'}}>
812 <a className="toggle-show-more" href="#show-more-properties">more&hellip;</a>
813 <a className="toggle-show-less" href="#show-more-properties">less&hellip;</a>
814 </h1>
815 <div className="toggleable">
816 {buildComponentsForProperties(container, properties, [], container.model, {toggle: true, width: props.width})}
817 </div>
818 <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>
819 </div>
820 );
821 }
822
823 function buildMoreLess(d, i) {
824 return (
825 <span key={'bread-crumb-part-' + i}>
826 <a href="#select-item" onClick={onClickSelectItem.bind(d, null, null, d)}>{d.title}</a>
827 <i> / </i>
828 </span>
829 );
830 }
831
832 const path = [];
833 if (container.parent) {
834 path.push(container.parent);
835 }
836 path.push(container);
837
838 return (
839 <div className="EditDescriptorModelProperties -is-tree-view">
840 <h1>{path.map(buildMoreLess)}</h1>
841 {buildBasicGroup()}
842 {buildAdvancedGroup()}
843 </div>
844 );
845 };
846