RIFT-15154: Config parameter map
[osm/UI.git] / skyquake / plugins / composer / src / src / components / EditDescriptorModelPropertiesBkUp.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 'use strict';
24
25 import _ from 'lodash'
26 import utils from '../libraries/utils'
27 import React from 'react'
28 import ClassNames from 'classnames'
29 import changeCase from 'change-case'
30 import toggle from '../libraries/ToggleElementHandler'
31 import Button from './Button'
32 import Property from '../libraries/model/DescriptorModelMetaProperty'
33 import ComposerAppActions from '../actions/ComposerAppActions'
34 import CatalogItemsActions from '../actions/CatalogItemsActions'
35 import DESCRIPTOR_MODEL_FIELDS from '../libraries/model/DescriptorModelFields'
36 import DescriptorModelFactory from '../libraries/model/DescriptorModelFactory'
37 import DescriptorModelMetaFactory from '../libraries/model/DescriptorModelMetaFactory'
38 import SelectionManager from '../libraries/SelectionManager'
39 import DeletionManager from '../libraries/DeletionManager'
40 import DescriptorModelIconFactory from '../libraries/model/IconFactory'
41 import getEventPath from '../libraries/getEventPath'
42 import CatalogDataStore from '../stores/CatalogDataStore'
43
44 import imgAdd from '../../../node_modules/open-iconic/svg/plus.svg'
45 import imgRemove from '../../../node_modules/open-iconic/svg/trash.svg'
46
47 import '../styles/EditDescriptorModelProperties.scss'
48
49
50
51 function getDescriptorMetaBasicForType(type) {
52 const basicPropertiesFilter = d => _.contains(DESCRIPTOR_MODEL_FIELDS[type], d.name);
53 return DescriptorModelMetaFactory.getModelMetaForType(type, basicPropertiesFilter) || {properties: []};
54 }
55
56 function getDescriptorMetaAdvancedForType(type) {
57 const advPropertiesFilter = d => !_.contains(DESCRIPTOR_MODEL_FIELDS[type], d.name);
58 return DescriptorModelMetaFactory.getModelMetaForType(type, advPropertiesFilter) || {properties: []};
59 }
60
61 function getTitle(model = {}) {
62 if (typeof model['short-name'] === 'string' && model['short-name']) {
63 return model['short-name'];
64 }
65 if (typeof model.name === 'string' && model.name) {
66 return model.name;
67 }
68 if (model.uiState && typeof model.uiState.displayName === 'string' && model.uiState.displayName) {
69 return model.uiState.displayName
70 }
71 if (typeof model.id === 'string') {
72 return model.id;
73 }
74 }
75
76 export default function EditDescriptorModelProperties(props) {
77
78 const container = props.container;
79
80 if (!(DescriptorModelFactory.isContainer(container))) {
81 return
82 }
83
84 function startEditing() {
85 DeletionManager.removeEventListeners();
86 }
87
88 function endEditing() {
89 DeletionManager.addEventListeners();
90 }
91
92 function onClickSelectItem(property, path, value, event) {
93 event.preventDefault();
94 const root = this.getRoot();
95 if (SelectionManager.select(value)) {
96 CatalogItemsActions.catalogItemMetaDataChanged(root.model);
97 }
98 }
99
100 function onFocusPropertyFormInputElement(property, path, value, event) {
101
102 event.preventDefault();
103 startEditing();
104
105 function removeIsFocusedClass(event) {
106 event.target.removeEventListener('blur', removeIsFocusedClass);
107 Array.from(document.querySelectorAll('.-is-focused')).forEach(d => d.classList.remove('-is-focused'));
108 }
109
110 removeIsFocusedClass(event);
111
112 const propertyWrapper = getEventPath(event).reduce((parent, element) => {
113 if (parent) {
114 return parent;
115 }
116 if (!element.classList) {
117 return false;
118 }
119 if (element.classList.contains('property')) {
120 return element;
121 }
122 }, false);
123
124 if (propertyWrapper) {
125 propertyWrapper.classList.add('-is-focused');
126 event.target.addEventListener('blur', removeIsFocusedClass);
127 }
128
129 }
130
131 function buildAddPropertyAction(container, property, path) {
132 function onClickAddProperty(property, path, event) {
133 event.preventDefault();
134 //SelectionManager.resume();
135 const create = Property.getContainerCreateMethod(property, this);
136 if (create) {
137 const model = null;
138 create(model, path, property);
139 } else {
140 const name = path.join('.');
141 const value = Property.createModelInstance(property);
142 utils.assignPathValue(this.model, name, value);
143 }
144 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
145 }
146 return (
147 <Button className="inline-hint" onClick={onClickAddProperty.bind(container, property, path)} label="Add" src={imgAdd} />
148 );
149 }
150
151 function buildRemovePropertyAction(container, property, path) {
152 function onClickRemoveProperty(property, path, event) {
153 event.preventDefault();
154 const name = path.join('.');
155 const removeMethod = Property.getContainerMethod(property, this, 'remove');
156 if (removeMethod) {
157 removeMethod(utils.resolvePath(this.model, name));
158 } else {
159 utils.removePathValue(this.model, name);
160 }
161 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
162 }
163 return (
164 <Button className="remove-property-action inline-hint" title="Remove" onClick={onClickRemoveProperty.bind(container, property, path)} label="Remove" src={imgRemove}/>
165 );
166 }
167
168 function onFormFieldValueChanged(event) {
169 if (DescriptorModelFactory.isContainer(this)) {
170 event.preventDefault();
171 const name = event.target.name;
172 const value = event.target.value;
173 utils.assignPathValue(this.model, name, value);
174 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
175 }
176 }
177
178 function buildField(container, property, path, value, fieldKey) {
179 let cds = CatalogDataStore;
180 let catalogs = cds.getTransientCatalogs();
181
182 const name = path.join('.');
183 const isEditable = true;
184 const isGuid = Property.isGuid(property);
185 const onChange = onFormFieldValueChanged.bind(container);
186 const isEnumeration = Property.isEnumeration(property);
187 const isLeafRef = Property.isLeafRef(property);
188 const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
189 const placeholder = changeCase.title(property.name);
190 const className = ClassNames(property.name + '-input', {'-is-guid': isGuid});
191 const fieldValue = value ? (value.constructor.name != "Object") ? value : '' : undefined;
192 if (isEnumeration) {
193 const enumeration = Property.getEnumeration(property, value);
194 const options = enumeration.map((d, i) => {
195 // note yangforge generates values for enums but the system does not use them
196 // so we categorically ignore them
197 // https://trello.com/c/uzEwVx6W/230-bug-enum-should-not-use-index-only-name
198 //return <option key={fieldKey + ':' + i} value={d.value}>{d.name}</option>;
199 return <option key={fieldKey.toString() + ':' + i} value={d.name}>{d.name}</option>;
200 });
201 const isValueSet = enumeration.filter(d => d.isSelected).length > 0;
202 if (!isValueSet || property.cardinality === '0..1') {
203 const noValueDisplayText = changeCase.title(property.name);
204 options.unshift(<option key={'(value-not-in-enum)' + fieldKey.toString()} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
205 }
206 return <select key={fieldKey.toString()} id={fieldKey.toString()} className={ClassNames({'-value-not-set': !isValueSet})} name={name} value={value} title={name} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} readOnly={!isEditable}>{options}</select>;
207 }
208
209 if (isLeafRef) {
210 let fullFieldKey = fieldKey;
211 let containerRef = container;
212 while (containerRef.parent) {
213 fullFieldKey = containerRef.parent.key + ':' + fullFieldKey;
214 containerRef = containerRef.parent;
215 }
216 const leafRefPathValues = Property.getLeafRef(property, path, value, fullFieldKey, catalogs, container);
217
218 const options = leafRefPathValues && leafRefPathValues.map((d, i) => {
219 return <option key={fieldKey.toString() + ':' + i} value={d.value}>{d.value}</option>;
220 });
221 const isValueSet = leafRefPathValues.filter(d => d.isSelected).length > 0;
222 if (!isValueSet || property.cardinality === '0..1') {
223 const noValueDisplayText = changeCase.title(property.name);
224 options.unshift(<option key={'(value-not-in-leafref)' + fieldKey.toString()} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
225 }
226 return <select key={fieldKey.toString()} id={fieldKey.toString()} className={ClassNames({'-value-not-set': !isValueSet})} name={name} value={value} title={name} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} readOnly={!isEditable}>{options}</select>;
227 }
228
229 if (property['preserve-line-breaks']) {
230 return <textarea key={fieldKey.toString()} cols="5" id={fieldKey.toString()} name={name} value={value} placeholder={placeholder} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} onMouseOut={endEditing} onMouseLeave={endEditing} readOnly={!isEditable} />;
231 }
232
233 return <input key={fieldKey.toString()}
234 id={fieldKey.toString()}
235 type="text"
236 name={name}
237 value={fieldValue}
238 className={className}
239 placeholder={placeholder}
240 onChange={onChange}
241 onFocus={onFocus}
242 onBlur={endEditing}
243 onMouseDown={startEditing}
244 onMouseOver={startEditing}
245 onMouseOut={endEditing}
246 onMouseLeave={endEditing}
247 readOnly={!isEditable}
248 />;
249
250 }
251
252 function buildElement(container, property, valuePath, value) {
253 return property.properties.map((property, index) => {
254 let childValue;
255 const childPath = valuePath.slice();
256 if (typeof value === 'object') {
257 childValue = value[property.name];
258 }
259 if(property.type != 'choice'){
260 childPath.push(property.name);
261 }
262 return build(container, property, childPath, childValue);
263
264 });
265 }
266
267 function buildChoice(container, property, path, value, key) {
268
269 function onFormFieldValueChanged(event) {
270 if (DescriptorModelFactory.isContainer(this)) {
271
272 event.preventDefault();
273
274 let name = event.target.name;
275 const value = event.target.value;
276
277
278 /*
279 Transient State is stored for convenience in the uiState field.
280 The choice yang type uses case elements to describe the "options".
281 A choice can only ever have one option selected which allows
282 the system to determine which type is selected by the name of
283 the element contained within the field.
284 */
285 /*
286 const stateExample = {
287 uiState: {
288 choice: {
289 'conf-config': {
290 selected: 'rest',
291 'case': {
292 rest: {},
293 netconf: {},
294 script: {}
295 }
296 }
297 }
298 }
299 };
300 */
301 const statePath = ['uiState.choice'].concat(name);
302 const stateObject = utils.resolvePath(this.model, statePath.join('.')) || {};
303 const selected = stateObject.selected ? stateObject.selected.split('.')[1] : undefined;
304 // write state back to the model so the new state objects are captured
305 utils.assignPathValue(this.model, statePath.join('.'), stateObject);
306
307 // write the current choice value into the state
308 let choiceObject = utils.resolvePath(this.model, [name, selected].join('.'));
309 let isTopCase = false;
310 if (!choiceObject) {
311 isTopCase = true;
312 choiceObject = utils.resolvePath(this.model, [selected].join('.'));
313 }
314 utils.assignPathValue(stateObject, [selected].join('.'), _.cloneDeep(choiceObject));
315
316 if(selected) {
317 if(this.model.uiState.choice.hasOwnProperty(name)) {
318 delete this.model[selected];
319 utils.removePathValue(this.model, [name, selected].join('.'), isTopCase);
320 } else {
321 // remove the current choice value from the model
322 utils.removePathValue(this.model, [name, selected].join('.'), isTopCase);
323 }
324 }
325
326 // get any state for the new selected choice
327 const newChoiceObject = utils.resolvePath(stateObject, [value].join('.')) || {};
328
329 // assign new choice value to the model
330 if (isTopCase) {
331 utils.assignPathValue(this.model, [name, value].join('.'), newChoiceObject);
332 } else {
333 utils.assignPathValue(this.model, [value].join('.'), newChoiceObject)
334 }
335
336
337 // update the selected name
338 utils.assignPathValue(this.model, statePath.concat('selected').join('.'), value);
339
340 CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
341 }
342 }
343
344 const caseByNameMap = {};
345
346 const onChange = onFormFieldValueChanged.bind(container);
347
348 const cases = property.properties.map(d => {
349 if (d.type === 'case') {
350 caseByNameMap[d.name] = d.properties[0];
351 return {
352 optionName: d.name,
353 optionTitle: d.description,
354 //represents case name and case element name
355 optionValue: [d.name, d.properties[0].name].join('.')
356 };
357 }
358 caseByNameMap[d.name] = d;
359 return {optionName: d.name};
360 });
361
362 const options = [{optionName: '', optionValue: false}].concat(cases).map((d, i) => {
363 return (
364 <option key={i} value={d.optionValue} title={d.optionTitle}>
365 {d.optionName}
366 {i ? null : changeCase.title(property.name)}
367 </option>
368 );
369 });
370
371 const selectName = path.join('.');
372 let selectedOptionPath = ['uiState.choice', selectName, 'selected'].join('.');
373 //Currently selected choice/case statement on UI model
374 let selectedOptionValue = utils.resolvePath(container.model, selectedOptionPath);
375 //If first time loaded, and none is selected, check if there is a value corresponding to a case statement in the container model
376 if(!selectedOptionValue) {
377 //get field properties for choice on container model
378 let fieldProperties = utils.resolvePath(container.model, selectName);
379 if(fieldProperties) {
380 //Check each case statement in model and see if it is present in container model.
381 cases.map(function(c){
382 if(fieldProperties.hasOwnProperty(c.optionValue.split('.')[1])) {
383 utils.assignPathValue(container.model, ['uiState.choice', selectName, 'selected'].join('.'), c.optionValue);
384 }
385 });
386 selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', selectName, 'selected'].join('.'));
387 } else {
388 property.properties.map(function(p) {
389 let pname = p.properties[0].name;
390 if(container.model.hasOwnProperty(pname)) {
391 utils.assignPathValue(container.model, ['uiState.choice', selectName, 'selected'].join('.'), [p.name, pname].join('.'));
392 }
393 })
394 selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', selectName, 'selected'].join('.'));
395 }
396 }
397 //If selectedOptionValue is present, take first item in string which represents the case name.
398 const valueProperty = caseByNameMap[selectedOptionValue ? selectedOptionValue.split('.')[0] : undefined] || {properties: []};
399 const isLeaf = Property.isLeaf(valueProperty);
400 const hasProperties = _.isArray(valueProperty.properties) && valueProperty.properties.length;
401 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(valueProperty);
402 //Some magic that prevents errors for arising
403 const valueResponse = valueProperty.properties.length ? valueProperty.properties.map((d, i) => {
404 const childPath = path.concat(valueProperty.name, d.name);
405 const childValue = utils.resolvePath(container.model, childPath.join('.'));
406 return (
407 <div key={childPath.concat('info', i).join(':')}>
408 {build(container, d, childPath, childValue, props)}
409 </div>
410 );
411 }) : (!isMissingDescriptorMeta) ? build(container, valueProperty, path.concat(valueProperty.name), utils.resolvePath(container.model, path.concat(valueProperty.name).join('.')) || container.model[valueProperty.name]) : null
412 // end magic
413 const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
414
415 return (
416 <div key={key} className="choice">
417 <select key={Date.now()} className={ClassNames({'-value-not-set': !selectedOptionValue})} name={selectName} value={selectedOptionValue} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} onMouseOut={endEditing} onMouseLeave={endEditing}>
418 {options}
419 </select>
420 {valueResponse}
421 </div>
422 );
423
424 }
425
426 function buildSimpleListItem(container, property, path, value, key, index) {
427 // todo need to abstract this better
428 const title = getTitle(value);
429 var req = require.context("../", true, /\.svg/);
430 return (
431 <div>
432 <a href="#select-list-item" key={Date.now()} className={property.name + '-list-item simple-list-item '} onClick={onClickSelectItem.bind(container, property, path, value)}>
433 <img src={req('./' + DescriptorModelIconFactory.getUrlForType(property.name))} width="20px" />
434 <span>{title}</span>
435 </a>
436 {buildRemovePropertyAction(container, property, path)}
437 </div>
438 );
439 }
440
441 function buildRemoveListItem(container, property, valuePath, fieldKey, index) {
442 const className = ClassNames(property.name + '-remove actions');
443 return (
444 <div key={fieldKey.concat(index).join(':')} className={className}>
445 <h3>
446 <span className={property.type + '-name name'}>{changeCase.title(property.name)}</span>
447 <span className="info">{index + 1}</span>
448 {buildRemovePropertyAction(container, property, valuePath)}
449 </h3>
450 </div>
451 );
452 }
453
454 function buildLeafListItem(container, property, valuePath, value, key, index) {
455 // look at the type to determine how to parse the value
456 return (
457 <div>
458 {buildRemoveListItem(container, property, valuePath, key, index)}
459 {buildField(container, property, valuePath, value, key)}
460 </div>
461
462 );
463 }
464
465 function build(container, property, path, value, props = {}) {
466
467 const fields = [];
468 const isLeaf = Property.isLeaf(property);
469 const isArray = Property.isArray(property);
470 const isObject = Property.isObject(property);
471 const isLeafList = Property.isLeafList(property);
472 const fieldKey = [container.id].concat(path);
473 const isRequired = Property.isRequired(property);
474 const title = changeCase.titleCase(property.name);
475 const columnCount = property.properties.length || 1;
476 const isColumnar = isArray && (Math.round(props.width / columnCount) > 155);
477 const classNames = {'-is-required': isRequired, '-is-columnar': isColumnar};
478
479 if (!property.properties && isObject) {
480 const uiState = DescriptorModelMetaFactory.getModelMetaForType(property.name) || {};
481 property.properties = uiState.properties;
482 }
483
484 const hasProperties = _.isArray(property.properties) && property.properties.length;
485 const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(property);
486
487 // ensure value is not undefined for non-leaf property types
488 if (isObject) {
489 if (typeof value !== 'object') {
490 value = isArray ? [] : {};
491 }
492 }
493 const valueAsArray = _.isArray(value) ? value : isLeafList && typeof value === 'undefined' ? [] : [value];
494
495 const isMetaField = property.name === 'meta';
496 const isCVNFD = property.name === 'constituent-vnfd';
497 const isSimpleListView = Property.isSimpleList(property);
498
499 valueAsArray.forEach((value, index) => {
500
501 let field;
502 const key = fieldKey.slice();
503 const valuePath = path.slice();
504
505 if (isArray) {
506 valuePath.push(index);
507 key.push(index);
508 }
509
510 if (isMetaField) {
511 if (typeof value === 'object') {
512 value = JSON.stringify(value, undefined, 12);
513 } else if (typeof value !== 'string') {
514 value = '{}';
515 }
516 }
517
518 if (isMissingDescriptorMeta) {
519 field = <span key={key.concat('warning').join(':')} className="warning">No Descriptor Meta for {property.name}</span>;
520 } else if (property.type === 'choice') {
521 field = buildChoice(container, property, valuePath, value, key.join(':'));
522 } else if (isSimpleListView) {
523 field = buildSimpleListItem(container, property, valuePath, value, key, index);
524 } else if (isLeafList) {
525 field = buildLeafListItem(container, property, valuePath, value, key, index);
526 } else if (hasProperties) {
527 field = buildElement(container, property, valuePath, value, key.join(':'))
528 } else {
529 field = buildField(container, property, valuePath, value, key.join(':'));
530 }
531
532 function onClickLeaf(property, path, value, event) {
533 if (event.isDefaultPrevented()) {
534 return;
535 }
536 event.preventDefault();
537 event.stopPropagation();
538 this.getRoot().uiState.focusedPropertyPath = path.join('.');
539 console.log('property selected', path.join('.'));
540 ComposerAppActions.propertySelected([path.join('.')]);
541 }
542
543 const clickHandler = isLeaf ? onClickLeaf : () => {};
544 const isContainerList = isArray && !(isSimpleListView || isLeafList);
545
546 fields.push(
547 <div key={fieldKey.concat(['property-content', index]).join(':')}
548 className={ClassNames('property-content', {'simple-list': isSimpleListView})}
549 onClick={clickHandler.bind(container, property, valuePath, value)}>
550 {isContainerList ? buildRemoveListItem(container, property, valuePath, fieldKey, index) : null}
551 {field}
552 </div>
553 );
554
555 });
556
557 classNames['-is-leaf'] = isLeaf;
558 classNames['-is-array'] = isArray;
559 classNames['cols-' + columnCount] = isColumnar;
560
561 if (property.type === 'choice') {
562 value = utils.resolvePath(container.model, ['uiState.choice'].concat(path, 'selected').join('.'));
563 if(!value) {
564 property.properties.map(function(p) {
565 let pname = p.properties[0].name;
566 if(container.model.hasOwnProperty(pname)) {
567 value = container.model[pname];
568 }
569 })
570 }
571 }
572
573 let displayValue = typeof value === 'object' ? '' : value;
574 const displayValueInfo = isArray ? valueAsArray.filter(d => typeof d !== 'undefined').length + ' items' : '';
575
576 const onFocus = isLeaf ? event => event.target.classList.add('-is-focused') : false;
577
578 return (
579 <div key={fieldKey.join(':')} className={ClassNames(property.type + '-property property', classNames)} onFocus={onFocus}>
580 <h3 className="property-label">
581 <label htmlFor={fieldKey.join(':')}>
582 <span className={property.type + '-name name'}>{title}</span>
583 <small>
584 <span className={property.type + '-info info'}>{displayValueInfo}</span>
585 <span className={property.type + '-value value'}>{displayValue}</span>
586 </small>
587 {isArray ? buildAddPropertyAction(container, property, path.concat(valueAsArray.length)) : null}
588 </label>
589 </h3>
590 <span className={property.type + '-description description'}>{property.description}</span>
591 <val className="property-value">
592 {isCVNFD ? <span className={property.type + '-tip tip'}>Drag a VNFD from the Catalog to add more.</span> : null}
593 {fields}
594 </val>
595 </div>
596 );
597
598 }
599
600 const containerType = container.uiState['qualified-type'] || container.uiState.type;
601 const basicProperties = getDescriptorMetaBasicForType(containerType).properties;
602
603 function buildBasicGroup() {
604 if (basicProperties.length === 0) {
605 return null;
606 }
607 return (
608 <div className="basic-properties-group">
609 <h2>Basic</h2>
610 <div>
611 {basicProperties.map(property => {
612 const path = [property.name];
613 const value = container.model[property.name];
614 return build(container, property, path, value);
615 })}
616 </div>
617 </div>
618 );
619 }
620
621 function buildAdvancedGroup() {
622 const properties = getDescriptorMetaAdvancedForType(containerType).properties;
623 if (properties.length === 0) {
624 return null;
625 }
626 const hasBasicFields = basicProperties.length > 0;
627 const closeGroup = basicProperties.length > 0;
628 return (
629 <div className="advanced-properties-group">
630 <h1 data-toggle={closeGroup ? 'true' : 'false'} className={ClassNames({'-is-toggled': closeGroup})} onClick={toggle} style={{display: hasBasicFields ? 'block' : 'none'}}>
631 <a className="toggle-show-more" href="#show-more-properties">more&hellip;</a>
632 <a className="toggle-show-less" href="#show-more-properties">less&hellip;</a>
633 </h1>
634 <div className="toggleable">
635 {properties.map(property => {
636 const path = [property.name];
637 const value = container.model[property.name];
638 return build(container, property, path, value, {toggle: true, width: props.width});
639 })}
640 </div>
641 <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>
642 </div>
643 );
644 }
645
646 function buildMoreLess(d, i) {
647 return (
648 <span key={'bread-crumb-part-' + i}>
649 <a href="#select-item" onClick={onClickSelectItem.bind(d, null, null, d)}>{d.title}</a>
650 <i> / </i>
651 </span>
652 );
653 }
654
655 const path = [];
656 if (container.parent) {
657 path.push(container.parent);
658 }
659 path.push(container);
660
661 return (
662 <div className="EditDescriptorModelProperties -is-tree-view">
663 <h1>{path.map(buildMoreLess)}</h1>
664 {buildBasicGroup()}
665 {buildAdvancedGroup()}
666 </div>
667 );
668
669 }
670 // export buildElement;
671 // export buildChoice;