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