update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b third try
[osm/UI.git] / skyquake / plugins / composer / src / src / components / EditDescriptorModelProperties.js
index ad2bf3d..19e699d 100644 (file)
  * This class generates the form fields used to edit the CONFD JSON model.
  */
 
-import _includes from 'lodash/includes'
-import _isArray from 'lodash/isArray'
-import _cloneDeep from 'lodash/cloneDeep'
-import _debounce from 'lodash/debounce';
 import _uniqueId from 'lodash/uniqueId';
 import _set from 'lodash/set';
 import _get from 'lodash/get';
 import _has from 'lodash/has';
+import _keys from 'lodash/keys';
+import _isObject from 'lodash/isObject';
+import _isArray from 'lodash/isArray';
 import utils from '../libraries/utils'
 import React from 'react'
-import ClassNames from 'classnames'
 import changeCase from 'change-case'
 import toggle from '../libraries/ToggleElementHandler'
-import Button from './Button'
 import Property from '../libraries/model/DescriptorModelMetaProperty'
+import SelectionManager from '../libraries/SelectionManager'
 import ComposerAppActions from '../actions/ComposerAppActions'
 import CatalogItemsActions from '../actions/CatalogItemsActions'
-import DESCRIPTOR_MODEL_FIELDS from '../libraries/model/DescriptorModelFields'
+import DescriptorEditorActions from '../actions/DescriptorEditorActions'
 import DescriptorModelFactory from '../libraries/model/DescriptorModelFactory'
 import DescriptorModelMetaFactory from '../libraries/model/DescriptorModelMetaFactory'
-import SelectionManager from '../libraries/SelectionManager'
-import DeletionManager from '../libraries/DeletionManager'
-import DescriptorModelIconFactory from '../libraries/model/IconFactory'
-import getEventPath from '../libraries/getEventPath'
-import CatalogDataStore from '../stores/CatalogDataStore'
 
-import imgAdd from '../../../node_modules/open-iconic/svg/plus.svg'
-import imgRemove from '../../../node_modules/open-iconic/svg/trash.svg'
+import ModelBreadcrumb from './model/ModelBreadcrumb'
+import ListItemAsLink from './model/ListItemAsLink'
+import LeafField from './model/LeafField'
+import { List, ListItem } from './model/List'
+import ContainerWrapper from './model/Container'
+import Choice from './model/Choice'
 
 import '../styles/EditDescriptorModelProperties.scss'
 
-const EMPTY_LEAF_PRESENT = '--empty-leaf-set--';
 
 function resolveReactKey(value) {
-       const keyPath =  ['uiState', 'fieldKey'];
+       const keyPath = ['uiState', 'fieldKey'];
        if (!_has(value, keyPath)) {
                _set(value, keyPath, _uniqueId());
        }
        return _get(value, keyPath);
 }
 
-function getDescriptorMetaBasicForType(type) {
-       const basicPropertiesFilter = d => _includes(DESCRIPTOR_MODEL_FIELDS[type], d.name);
-       return DescriptorModelMetaFactory.getModelMetaForType(type, basicPropertiesFilter) || {properties: []};
+function getTipForProperty(property) {
+       return property.name === 'constituent-vnfd' ? "Drag a VNFD from the Catalog to add more." : null
 }
 
-function getDescriptorMetaAdvancedForType(type) {
-       const advPropertiesFilter = d => !_includes(DESCRIPTOR_MODEL_FIELDS[type], d.name);
-       return DescriptorModelMetaFactory.getModelMetaForType(type, advPropertiesFilter) || {properties: []};
+function selectModel(container, model, property) {
+       ComposerAppActions.selectModel(container.findChildByUid(model));
 }
 
-function getTitle(model = {}) {
-       if (typeof model['short-name'] === 'string' && model['short-name']) {
-               return model['short-name'];
-       }
-       if (typeof model.name === 'string' && model.name) {
-               return model.name;
-       }
-       if (model.uiState && typeof model.uiState.displayName === 'string' && model.uiState.displayName) {
-               return model.uiState.displayName
-       }
-       if (typeof model.id === 'string') {
-               return model.id;
-       }
+function removeListEntry(container, property, path) {
+       DescriptorEditorActions.removeListItem({ descriptor: container, property, path });
 }
 
-export default function EditDescriptorModelProperties(props) {
-
-       const container = props.container;
-
-       if (!(DescriptorModelFactory.isContainer(container))) {
-               return
-       }
-
-       function startEditing() {
-               DeletionManager.removeEventListeners();
-       }
-
-       function endEditing() {
-               DeletionManager.addEventListeners();
-       }
-
-       function onClickSelectItem(property, path, value, event) {
-               event.preventDefault();
-               const root = this.getRoot();
-               if (SelectionManager.select(value)) {
-                       CatalogItemsActions.catalogItemMetaDataChanged(root.model);
-               }
-       }
-
-       function onFocusPropertyFormInputElement(property, path, value, event) {
-
-               event.preventDefault();
-               startEditing();
-
-               function removeIsFocusedClass(event) {
-                       event.target.removeEventListener('blur', removeIsFocusedClass);
-                       Array.from(document.querySelectorAll('.-is-focused')).forEach(d => d.classList.remove('-is-focused'));
-               }
-
-               removeIsFocusedClass(event);
+function createAndAddItemToPath(container, property, path) {
+       DescriptorEditorActions.addListItem({ descriptor: container, property, path });
+}
 
-               const propertyWrapper = getEventPath(event).reduce((parent, element) => {
-                       if (parent) {
-                               return parent;
-                       }
-                       if (!element.classList) {
-                               return false;
-                       }
-                       if (element.classList.contains('property')) {
-                               return element;
-                       }
-               }, false);
+function notifyPropertyFocused(container, path) {
+       container.getRoot().uiState.focusedPropertyPath = path.join('.');
+       console.debug('property selected', path.join('.'));
+       ComposerAppActions.propertySelected([path.join('.')]);
+}
 
-               if (propertyWrapper) {
-                       propertyWrapper.classList.add('-is-focused');
-                       event.target.addEventListener('blur', removeIsFocusedClass);
-               }
+function setPropertyOpenState(container, path, property, isOpen) {
+       DescriptorEditorActions.setOpenState({ descriptor: container, property, path, isOpen });
+}
 
-       }
+function isDataProperty(property) {
+       return property.type === 'leaf' || property.type === 'leaf_list' || property.type === 'choice';
+}
 
-       function buildAddPropertyAction(container, property, path) {
-               function onClickAddProperty(property, path, event) {
-                       event.preventDefault();
-                       //SelectionManager.resume();
-                       const create = Property.getContainerCreateMethod(property, this);
-                       if (create) {
-                               const model = null;
-                               create(model, path, property);
-                       } else {
-                               const name = path.join('.');
-                               // get a unique name for the new list item based on the current list content
-                               // some lists, based on the key, may not get a uniqueName generated here
-                               const uniqueName = DescriptorModelMetaFactory.generateItemUniqueName(container.model[property.name], property);
-                               const value = Property.createModelInstance(property, uniqueName);
-                               utils.assignPathValue(this.model, name, value);
-                       }
-                       CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
+function checkIfValueEmpty(value) {
+       if (value === null || typeof value === 'undefined') {
+               return true;
+       } else if (_isArray(value) && !value.length) {
+               return true;
+       } else if (_isObject(value)) {
+               const keys = _keys(value);
+               if (keys.length < 2) {
+                       return !keys.length || (keys[0] === 'uiState')
                }
-               return (
-                               <Button className="inline-hint" onClick={onClickAddProperty.bind(container, property, path)} label="Add" src={imgAdd} />
-               );
        }
+       return false;
+}
 
-       function buildRemovePropertyAction(container, property, path) {
-               function onClickRemoveProperty(property, path, event) {
-                       event.preventDefault();
-                       const name = path.join('.');
-                       const removeMethod = Property.getContainerMethod(property, this, 'remove');
-                       if (removeMethod) {
-                               removeMethod(utils.resolvePath(this.model, name));
-                       } else {
-                               utils.removePathValue(this.model, name);
-                       }
-                       CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
+export default function EditDescriptorModelProperties(props) {
+       const { container, idMaker, showHelp, collapsePanelsByDefault, openPanelsWithData } = props;
+       const readOnly = props.readOnly || container.isReadOnly;
+       const showElementHelp = showHelp.forAll;
+       const uiState = container.uiState;
+
+       function getPanelOpenedCondition(value, path) {
+               const showOpened = container.getUiState('opened', path);
+               if (typeof showOpened === 'undefined') {
+                       return (openPanelsWithData && !checkIfValueEmpty(value)) ? true : !collapsePanelsByDefault;
                }
-               return (
-                       <Button className="remove-property-action inline-hint" title="Remove" onClick={onClickRemoveProperty.bind(container, property, path)} label="Remove" src={imgRemove}/>
-               );
+               return showOpened;
        }
 
-       function buildField(container, property, path, value, fieldKey) {
-               let cds = CatalogDataStore;
-               let catalogs = cds.getTransientCatalogs();
-
+       function buildField(property, path, value, fieldKey) {
                const pathToProperty = path.join('.');
-               const isEditable = true;
-               const isGuid = Property.isGuid(property);
-               const isBoolean = Property.isBoolean(property);
-               const isEnumeration = Property.isEnumeration(property);
-               const isLeafRef = Property.isLeafRef(property);
-               const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
-               const placeholder = changeCase.title(property.name);
-               const className = ClassNames(property.name + '-input', {'-is-guid': isGuid});
                const fieldValue = value ? (value.constructor.name != "Object") ? value : '' : (isNaN(value) ? undefined : value);
 
                // process the named field value change
                function processFieldValueChange(name, value) {
                        console.debug('processed change for -- ' + name + ' -- with value -- ' + value);
-                       // this = the container being edited
-                       if (DescriptorModelFactory.isContainer(this)) {
-                               utils.assignPathValue(this.model, name, value);
-                               CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
-                       }
-               }
-
-               // change handler used for onChange event
-               const changeHandler = (handleValueChange, event) => {
-                       event.preventDefault();
-                       console.debug(event.target.value);
-                       handleValueChange(event.target.value);
-               };
-               // create an onChange event handler for a text field for the specified field path (debounced to accumulate chars)
-               const onTextChange = changeHandler.bind(null, _debounce(
-                       processFieldValueChange.bind(container, pathToProperty), 2000, {maxWait: 5000})); // max wait for short-name
-               // create an onChange event handler for a select field for the specified field path
-               const onSelectChange = changeHandler.bind(null, processFieldValueChange.bind(container, pathToProperty));
-               
-               if (isEnumeration) {
-                       const enumeration = Property.getEnumeration(property, value);
-                       const options = enumeration.map((d, i) => {
-                               // note yangforge generates values for enums but the system does not use them
-                               // so we categorically ignore them
-                               // https://trello.com/c/uzEwVx6W/230-bug-enum-should-not-use-index-only-name
-                               //return <option key={fieldKey + ':' + i} value={d.value}>{d.name}</option>;
-                               return <option key={':' + i} value={d.name}>{d.name}</option>;
-                       });
-                       const isValueSet = enumeration.filter(d => d.isSelected).length > 0;
-                       if (!isValueSet || property.cardinality === '0..1') {
-                               const noValueDisplayText = changeCase.title(property.name);
-                               options.unshift(<option key={'(value-not-in-enum)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
-                       }
-                       return (
-                               <select 
-                                       key={fieldKey} 
-                                       id={fieldKey}
-                                       className={ClassNames({'-value-not-set': !isValueSet})} 
-                                       defaultValue={value} 
-                                       title={pathToProperty} 
-                                       onChange={onSelectChange} 
-                                       onFocus={onFocus} 
-                                       onBlur={endEditing} 
-                                       onMouseDown={startEditing} 
-                                       onMouseOver={startEditing} 
-                                       readOnly={!isEditable}>
-                                               {options}
-                               </select>
-                       );
-               }
-
-               if (isLeafRef) {
-                       let fullPathString = container.key + ':' + path.join(':');
-                       let containerRef = container;
-                       while (containerRef.parent) {
-                               fullPathString = containerRef.parent.key + ':' + fullPathString;
-                               containerRef = containerRef.parent;
-                       }
-                       const leafRefPathValues = Property.getLeafRef(property, path, value, fullPathString, catalogs, container);
-
-                       const options = leafRefPathValues && leafRefPathValues.map((d, i) => {
-                               return <option key={':' + i} value={d.value}>{d.value}</option>;
-                       });
-                       const isValueSet = leafRefPathValues.filter(d => d.isSelected).length > 0;
-                       if (!isValueSet || property.cardinality === '0..1') {
-                               const noValueDisplayText = changeCase.title(property.name);
-                               options.unshift(<option key={'(value-not-in-leafref)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
+                       if (DescriptorModelFactory.isContainer(container)) {
+                               DescriptorEditorActions.setValue({ descriptor: container, path, value });
                        }
-                       return (
-                               <select 
-                                       key={fieldKey} 
-                                       id={fieldKey} 
-                                       className={ClassNames({'-value-not-set': !isValueSet})} 
-                                       defaultValue={value} 
-                                       title={pathToProperty} 
-                                       onChange={onSelectChange} 
-                                       onFocus={onFocus} 
-                                       onBlur={endEditing} 
-                                       onMouseDown={startEditing} 
-                                       onMouseOver={startEditing} 
-                                       readOnly={!isEditable}>
-                                               {options}
-                               </select>
-                       );
-               }
-
-               if (isBoolean) {
-                       const options = [
-                               <option key={'true'} value="TRUE">TRUE</option>,
-                               <option key={'false'} value="FALSE">FALSE</option>
-                       ]
-
-                       // if (!isValueSet) {
-                               const noValueDisplayText = changeCase.title(property.name);
-                               options.unshift(<option key={'(value-not-in-leafref)'} value="" placeholder={placeholder}></option>);
-                       // }
-                       let val = value;
-                       if(typeof(val) == 'number') {
-                               val = value ? "TRUE" : "FALSE"
-                       }
-                       const isValueSet = (val != '' && val)
-                       return (
-                               <select 
-                                       key={fieldKey} 
-                                       id={fieldKey} 
-                                       className={ClassNames({'-value-not-set': !isValueSet})} 
-                                       defaultValue={val && val.toUpperCase()} 
-                                       title={pathToProperty} 
-                                       onChange={onSelectChange} 
-                                       onFocus={onFocus} 
-                                       onBlur={endEditing} 
-                                       onMouseDown={startEditing} 
-                                       onMouseOver={startEditing} 
-                                       readOnly={!isEditable}>
-                                               {options}
-                               </select>
-                       );
-               }
-               
-               if (Property.isLeafEmpty(property)) {
-                       // A null value indicates the leaf exists (as opposed to undefined).
-                       // We stick in a string when the user actually sets it to simplify things
-                       // but the correct thing happens when we serialize to user data
-                       let isEmptyLeafPresent = (value === EMPTY_LEAF_PRESENT || value === null); 
-                       let present = isEmptyLeafPresent ? EMPTY_LEAF_PRESENT : "";
-                       const options = [
-                               <option key={'true'} value={EMPTY_LEAF_PRESENT}>Enabled</option>,
-                               <option key={'false'} value="">Not Enabled</option>
-                       ]
-
-                       return (
-                               <select 
-                                       key={fieldKey} 
-                                       id={fieldKey} 
-                                       className={ClassNames({'-value-not-set': !isEmptyLeafPresent})} 
-                                       defaultValue={present} 
-                                       title={pathToProperty} 
-                                       onChange={onSelectChange} 
-                                       onFocus={onFocus} 
-                                       onBlur={endEditing} 
-                                       onMouseDown={startEditing} 
-                                       onMouseOver={startEditing} 
-                                       readOnly={!isEditable}>
-                                               {options}
-                               </select>
-                       );
                }
 
-               if (property['preserve-line-breaks']) {
-                       return (
-                               <textarea 
-                                       key={fieldKey} 
-                                       cols="5" 
-                                       id={fieldKey} 
-                                       defaultValue={value} 
-                                       placeholder={placeholder} 
-                                       onChange={onTextChange} 
-                                       onFocus={onFocus} 
-                                       onBlur={endEditing} 
-                                       onMouseDown={startEditing} 
-                                       onMouseOver={startEditing} 
-                                       onMouseOut={endEditing} 
-                                       onMouseLeave={endEditing} 
-                                       readOnly={!isEditable} />
-                       );
+               function onErrorHandler(message) {
+                       DescriptorEditorActions.setError({ descriptor: container, path, message });
                }
 
+               // create an onChange event handler for a select field for the specified field path
+               const onChangeHandler = processFieldValueChange.bind(null, pathToProperty);
                return (
-                       <input 
+                       <LeafField
                                key={fieldKey}
+                               container={container}
+                               property={property}
+                               path={path}
+                               value={value}
                                id={fieldKey}
-                               type="text"
-                               defaultValue={fieldValue}
-                               className={className}
-                               placeholder={placeholder}
-                               onChange={onTextChange}
-                               onFocus={onFocus}
-                               onBlur={endEditing}
-                               onMouseDown={startEditing}
-                               onMouseOver={startEditing}
-                               onMouseOut={endEditing}
-                               onMouseLeave={endEditing}
-                               readOnly={!isEditable}
+                               showHelp={showElementHelp}
+                               onChange={onChangeHandler}
+                               onError={onErrorHandler}
+                               readOnly={readOnly}
+                               errorMessage={_get(container.uiState, ['error'].concat(path))}
                        />
                );
-
        }
 
        /**
@@ -395,451 +158,288 @@ export default function EditDescriptorModelProperties(props) {
         * @param {[property]} properties 
         * @param {string} pathToProperties path within the container to the properties
         * @param {Object} data source for each property
-        * @param {any} props object containing main data panel information, e.g. panel width {width: 375}
-        * which may be useful/necessary to a components rendering.
         * @returns an array of react components
         */
-       function buildComponentsForProperties(container, properties, pathToProperties, data, props) {
+       function buildComponentsForProperties(properties, pathToProperties, data) {
                return properties.map((property) => {
                        let value;
                        let propertyPath = pathToProperties.slice();
+                       if (property.type != 'choice') {
+                               propertyPath.push(property.name);
+                       }
                        if (data && typeof data === 'object') {
-                               value = data[property.name];
+                               value = _get(data, property.name);
                        }
-                       if(property.type != 'choice'){
-                               propertyPath.push(property.name);
+                       let result = null;
+                       try {
+                               result = buildPropertyComponent(property, propertyPath, value);
+                       } catch (e) {
+                               console.error(e);
                        }
-                       return build(container, property, propertyPath, value, props);
+                       return result;
                });
        }
 
-       function buildElement(container, property, valuePath, value) {
-               return buildComponentsForProperties(container, property.properties, valuePath, value);
-       }
+       function buildChoice(property, path, value, uniqueId) {
+               const uiStatePath = path.concat(['uiState']);
+               const choiceStatePath = ['choice', property.name];
+               const fullChoiceStatePath = uiStatePath.concat(choiceStatePath);
 
-       function buildChoice(container, property, path, value, key) {
-
-               function processChoiceChange(name, value) {
-                       if (DescriptorModelFactory.isContainer(this)) {
-
-                               /*
-                                       Transient State is stored for convenience in the uiState field.
-                                       The choice yang type uses case elements to describe the "options".
-                                       A choice can only ever have one option selected which allows
-                                       the system to determine which type is selected by the name of
-                                       the element contained within the field.
-                                */
-                               /*
-                                       const stateExample = {
-                                               uiState: {
-                                                       choice: {
-                                                               'conf-config': {
-                                                                       selected: 'rest',
-                                                                       'case': {
-                                                                               rest: {},
-                                                                               netconf: {},
-                                                                               script: {}
-                                                                       }
-                                                               }
-                                                       }
-                                               }
-                                       };
-                               */
-                               const statePath = ['uiState.choice'].concat(name);
-                               const stateObject = utils.resolvePath(this.model, statePath.join('.')) || {};
-                               const selected = stateObject.selected ? stateObject.selected.split('.')[1] : undefined;
-                               // write state back to the model so the new state objects are captured
-                               utils.assignPathValue(this.model, statePath.join('.'), stateObject);
-
-                               // write the current choice value into the state
-                               let choiceObject = utils.resolvePath(this.model, [name, selected].join('.'));
-                               let isTopCase = false;
-                               if (!choiceObject) {
-                                       isTopCase = true;
-                                       choiceObject = utils.resolvePath(this.model, [selected].join('.'));
+               function determineSelectedChoice(model) {
+                       let choiceState = utils.resolvePath(container.model, fullChoiceStatePath.join('.'));
+                       if (choiceState) {
+                               return property.properties.find(c => c.name === choiceState.selected);
+                       }
+                       const selectedCase = property.properties.find(c =>
+                               c.properties && c.properties.find(p => _has(model, path.concat([p.name])))
+                       );
+                       // lets remember this
+                       let stateObject = utils.resolvePath(container.model, uiStatePath.join('.'));
+                       stateObject = _set(stateObject || {}, choiceStatePath, { selected: selectedCase ? selectedCase.name : "" });
+                       utils.assignPathValue(container.model, uiStatePath.join('.'), stateObject);
+                       return selectedCase;
+               }
+
+               function pullOutCaseModel(caseName) {
+                       const model = container.model;
+                       const properties = property.properties.find(c => c.name === caseName).properties;
+                       return properties.reduce((o, p) => {
+                               const valuePath = path.concat([p.name]).join('.');
+                               const value = utils.resolvePath(model, valuePath);
+                               if (value) {
+                                       o[p.name] = value;
                                }
-                               utils.assignPathValue(stateObject, [selected].join('.'), _cloneDeep(choiceObject));
+                               return o;
+                       }, {});
+               }
 
-                               if(selected) {
-                                       if(this.model.uiState.choice.hasOwnProperty(name)) {
-                                               delete this.model[selected];
-                                               utils.removePathValue(this.model, [name, selected].join('.'), isTopCase);
+               function processChoiceChange(value) {
+                       if (DescriptorModelFactory.isContainer(container)) {
+                               let uiState = utils.resolvePath(container.model, uiStatePath.join('.'));
+                               // const stateObject = utils.resolvePath(container.model, fullChoiceStatePath.join('.')) || {};
+                               let choiceState = _get(uiState, choiceStatePath);
+                               const previouslySelectedChoice = choiceState.selected;
+                               if (previouslySelectedChoice === value) {
+                                       return;
+                               }
+                               if (previouslySelectedChoice) {
+                                       choiceState[previouslySelectedChoice] = pullOutCaseModel(previouslySelectedChoice);
+                               }
+                               const modelUpdate = _keys(choiceState[previouslySelectedChoice]).reduce((o, k) => _set(o, k, null), {})
+                               choiceState.selected = value;
+                               _set(uiState, choiceStatePath, choiceState);
+                               _set(modelUpdate, 'uiState', uiState);
+                               if (choiceState.selected) {
+                                       const previous = choiceState[choiceState.selected];
+                                       if (previous) {
+                                               Object.assign(modelUpdate, previous);
                                        } else {
-                                               // remove the current choice value from the model
-                                               utils.removePathValue(this.model, [name, selected].join('.'), isTopCase);
+                                               const newChoice = property.properties.find(p => p.name === choiceState.selected);
+                                               if (newChoice.properties.length === 1) {
+                                                       const property = newChoice.properties[0];
+                                                       if (property.type === 'leaf' && property['data-type'] === 'empty') {
+                                                               let obj = {};
+                                                               obj[property.name] = [null];
+                                                               Object.assign(modelUpdate, obj);
+                                                       }
+                                               }
                                        }
                                }
-
-                               // get any state for the new selected choice
-                               const newChoiceObject = utils.resolvePath(stateObject, [value].join('.')) || {};
-
-                               // assign new choice value to the model
-                               if (isTopCase) {
-                                       utils.assignPathValue(this.model, [name, value].join('.'), newChoiceObject);
-                               } else {
-                                       utils.assignPathValue(this.model, [value].join('.'), newChoiceObject)
-                               }
-
-                               // update the selected name
-                               utils.assignPathValue(this.model, statePath.concat('selected').join('.'), value);
-
-                               CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
+                               DescriptorEditorActions.assignValue({ descriptor: container, path, source: modelUpdate });
                        }
                }
 
-               const pathToChoice = path.join('.');
-               const caseByNameMap = {};
-
-               const choiceChangeHandler = processChoiceChange.bind(container, pathToChoice);
-               const onChange = ((handleChoiceChange, event) => {
-                       event.preventDefault();
-                       handleChoiceChange(event.target.value);
-               }).bind(null, choiceChangeHandler);
-
-
-               const cases = property.properties.map(d => {
-                       if (d.type === 'case') {
-                               //Previous it was assumed that a case choice would have only one property. Now we pass on either the only item or the
-                               caseByNameMap[d.name] = d.properties && (d.properties.length == 1 ? d.properties[0] : d.properties);
-                               return {
-                                       optionName: d.name,
-                                       optionTitle: d.description,
-                                       //represents case name and case element name
-                                       optionValue: [d.name, d.properties[0].name].join('.')
-                               };
-                       }
-                       caseByNameMap[d.name] = d;
-                       return {optionName: d.name};
-               });
+               const selectedCase = determineSelectedChoice(container.model);
+               const children = selectedCase ?
+                       <ContainerWrapper property={selectedCase} readOnly={readOnly} showHelp={showElementHelp} showOpened={true}>
+                               {buildComponentsForProperties(selectedCase.properties, path, path.length ? _get(container.model, path) : container.model)}
+                       </ContainerWrapper>
+                       : null;
 
-               const options = [{optionName: '', optionValue: false}].concat(cases).map((d, i) => {
-                       return (
-                               <option key={i} value={d.optionValue} title={d.optionTitle}>
-                                       {d.optionName}
-                                       {i ? null : changeCase.title(property.name)}
-                               </option>
-                       );
-               });
+               return (
+                       <Choice key={uniqueId} id={uniqueId} onChange={processChoiceChange} readOnly={readOnly} showHelp={showElementHelp}
+                               property={property} value={selectedCase ? selectedCase.name : null}
+                       >
+                               {children}
+                       </Choice>
+               );
 
-               let selectedOptionPath = ['uiState.choice', pathToChoice, 'selected'].join('.');
-               //Currently selected choice/case statement on UI model
-               let selectedOptionValue = utils.resolvePath(container.model, selectedOptionPath);
-               //If first time loaded, and none is selected, check if there is a value corresponding to a case statement in the container model
-               if(!selectedOptionValue) {
-                       //get field properties for choice on container model
-                       let fieldProperties = utils.resolvePath(container.model, pathToChoice);
-                       if(fieldProperties) {
-                               //Check each case statement in model and see if it is present in container model.
-                               cases.map(function(c){
-                                       if(c.optionValue && fieldProperties.hasOwnProperty(c.optionValue.split('.')[1])) {
-                                               utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), c.optionValue);
-                                       }
-                               });
-                               selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'));
-                       } else {
-                               property.properties.map(function(p) {
-                                       let pname = p.properties[0] && p.properties[0].name;
-                                       if(container.model.hasOwnProperty(pname)) {
-                                               utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), [p.name, pname].join('.'));
-                                       }
-                               })
-                               selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'));
-                       }
-               }
-               //If selectedOptionValue is present, take first item in string which represents the case name.
-               const valueProperty = caseByNameMap[selectedOptionValue ? selectedOptionValue.split('.')[0] : undefined] || {properties: []};
-               const isLeaf = Property.isLeaf(valueProperty);
-               const hasProperties = _isArray(valueProperty.properties) && valueProperty.properties.length;
-               const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(valueProperty);
-               //Some magic that prevents errors for arising
-               let valueResponse = null;
-               if (valueProperty.properties && valueProperty.properties.length) { 
-                       valueResponse = valueProperty.properties.map(valuePropertyFn);
-               } else if (!isMissingDescriptorMeta) {
-                       let value = utils.resolvePath(container.model, path.concat(valueProperty.name).join('.')) || container.model[valueProperty.name];
-                       valueResponse = build(container, valueProperty, path.concat(valueProperty.name), value)
-               } else {
-                       valueResponse = valueProperty.map && valueProperty.map(valuePropertyFn);
+       }
+
+       function buildLeafList(property, path, value, uniqueId) {
+               if (!Array.isArray(value)) {
+                       value = [value];
                }
-               function valuePropertyFn(d, i) {
-                       const childPath = path.concat(valueProperty.name, d.name);
-                       const childValue = utils.resolvePath(container.model, childPath.join('.'));
+               const children = value && value.map((v, i) => {
+                       let itemPath = path.concat([i]);
+                       const field = buildField(property, itemPath, v, uniqueId + i);
                        return (
-                               <div key={childPath.concat('info', i).join(':')}>
-                                       {build(container, d, childPath, childValue, props)}
-                               </div>
-                       );
-               }
-               // end magic
-               const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
-
+                               <ListItem key={':' + i} index={i} property={property} readOnly={readOnly} showHelp={showElementHelp}
+                                       showOpened={true} removeItemHandler={removeListEntry.bind(null, container, property, itemPath)} >
+                                       {field}
+                               </ListItem>
+                       )
+               });
                return (
-                       <div key={key} className="choice">
-                               <select 
-                                       key={Date.now()} 
-                                       className={ClassNames({'-value-not-set': !selectedOptionValue})} 
-                                       defaultValue={selectedOptionValue} 
-                                       onChange={onChange} 
-                                       onFocus={onFocus} 
-                                       onBlur={endEditing} 
-                                       onMouseDown={startEditing} 
-                                       onMouseOver={startEditing} 
-                                       onMouseOut={endEditing} 
-                                       onMouseLeave={endEditing}
-                               >
-                                       {options}
-                               </select>
-                               {valueResponse}
-                       </div>
+                       <List key={uniqueId} id={uniqueId} property={property} value={value} readOnly={readOnly} showHelp={showElementHelp}
+                               showOpened={true} addItemHandler={createAndAddItemToPath.bind(null, container, property, path)}>
+                               {children}
+                       </List>
                );
-
        }
 
-       function buildSimpleListItem(container, property, path, value, uniqueId, index) {
-               // todo need to abstract this better
-               const title = getTitle(value);
-               var req = require.context("../", true, /\.svg/);
+       function buildList(property, path, value, uniqueId) {
+               if (value && !Array.isArray(value)) {
+                       value = [value];
+               }
+               function getListItemSummary(index, value) {
+                       const keys = property.key.map((key) => value[key]);
+                       const summary = keys.join(' ');
+                       return summary.length > 1 ? summary : '' + (index + 1);
+               }
+               const children = value && value.map((itemValue, i) => {
+                       const itemPath = path.concat([i]);
+                       const key = resolveReactKey(itemValue);
+                       const children = buildComponentsForProperties(property.properties, itemPath, itemValue);
+                       const showOpened = getPanelOpenedCondition(value, itemPath);
+                       return (
+                               <ListItem key={key} property={property} readOnly={readOnly} showHelp={showElementHelp}
+                                       summary={getListItemSummary(i, itemValue)} info={'' + (i + 1)}
+                                       removeItemHandler={removeListEntry.bind(null, container, property, itemPath)}
+                                       showOpened={showOpened} onChangeOpenState={setPropertyOpenState.bind(null, container, itemPath, property, !showOpened)}>
+                                       {children}
+                               </ListItem>
+                       )
+               });
+               const showOpened = getPanelOpenedCondition(value, path);
                return (
-                       <div key={uniqueId} >
-                               <a href="#select-list-item" className={property.name + '-list-item simple-list-item '} onClick={onClickSelectItem.bind(container, property, path, value)}>
-                                       <img src={req('./' + DescriptorModelIconFactory.getUrlForType(property.name))} width="20px" />
-                                       <span>{title}</span>
-                               </a>
-                               {buildRemovePropertyAction(container, property, path)}
-                       </div>
+                       <List key={uniqueId} id={uniqueId} property={property} value={value} readOnly={readOnly} showHelp={showElementHelp}
+                               addItemHandler={createAndAddItemToPath.bind(null, container, property, path)}
+                               showOpened={showOpened} onChangeOpenState={setPropertyOpenState.bind(null, container, path, property, !showOpened)}>
+                               {children}
+                       </List>
                );
        }
 
-       function buildRemoveListItem(container, property, valuePath, index) {
-               const className = ClassNames(property.name + '-remove actions');
+       function buildSimpleList(property, path, value, uniqueId) {
+               if (value && !Array.isArray(value)) {
+                       value = [value];
+               }
+               const children = value && value.map((v, i) => {
+                       let itemPath = path.concat([i]);
+                       return (
+                               <ListItemAsLink key={':' + i} property={property} value={v}
+                                       removeItemHandler={removeListEntry.bind(null, container, property, itemPath)}
+                                       selectLinkHandler={selectModel.bind(null, container, v, property)} />
+                       )
+               });
+               const tip = getTipForProperty(property);
+               const showOpened = getPanelOpenedCondition(value, path);
+               const changeOpenState = setPropertyOpenState.bind(null, container, path, property, !showOpened);
                return (
-                       <div className={className}>
-                               <h3>
-                                       <span className={property.type + '-name name'}>{changeCase.title(property.name)}</span>
-                                       <span className="info">{index + 1}</span>
-                                       {buildRemovePropertyAction(container, property, valuePath)}
-                               </h3>
-                       </div>
+                       <List name={uniqueId} id={uniqueId} key={uniqueId} tip={tip}
+                               property={property} value={value} readOnly={readOnly} showHelp={showElementHelp}
+                               addItemHandler={createAndAddItemToPath.bind(null, container, property, path)}
+                               showOpened={showOpened} onChangeOpenState={changeOpenState}>
+                               {children}
+                       </List>
                );
        }
 
-       function buildLeafListItem(container, property, valuePath, value, uniqueId, index) {
-               // look at the type to determine how to parse the value
+       function buildContainer(property, path, value, uniqueId) {
+               const children = buildComponentsForProperties(property.properties, path, value);
+               const showOpened = getPanelOpenedCondition(value, path);
+               const changeOpenState = setPropertyOpenState.bind(null, container, path, property, !showOpened);
                return (
-                       <div key={uniqueId}>
-                               {buildRemoveListItem(container, property, valuePath, index)}
-                               {buildField(container, property, valuePath, value, uniqueId)}
-                       </div>
-
+                       <ContainerWrapper key={uniqueId} id={uniqueId} property={property} readOnly={readOnly}
+                               showHelp={showElementHelp} summary={checkIfValueEmpty(value) ? null : '*'}
+                               showOpened={showOpened} onChangeOpenState={changeOpenState}>
+                               {children}
+                       </ContainerWrapper>
                );
        }
 
-       function build(container, property, path, value, props = {}) {
+       function buildPropertyComponent(property, path, value) {
 
                const fields = [];
-               const isLeaf = Property.isLeaf(property);
                const isArray = Property.isArray(property);
                const isObject = Property.isObject(property);
-               const isLeafList = Property.isLeafList(property);
-               const isRequired = Property.isRequired(property);
                const title = changeCase.titleCase(property.name);
-               const columnCount = property.properties.length || 1;
-               const isColumnar = isArray && (Math.round(props.width / columnCount) > 155);
-               const classNames = {'-is-required': isRequired, '-is-columnar': isColumnar};
 
                // create a unique Id for use as react component keys and html element ids
                // use uid (from ui info) instead of id property (which is not stable)
-               let uniqueId = container.uid;
-               let containerRef = container;
-               while (containerRef.parent) {
-                       uniqueId = containerRef.parent.uid + ':' + uniqueId;
-                       containerRef = containerRef.parent;
-               }
-               uniqueId += ':' + path.join(':')
+               let uniqueId = idMaker(container, path);
 
                if (!property.properties && isObject) {
+                       console.debug('no properties', property);
                        const uiState = DescriptorModelMetaFactory.getModelMetaForType(property.name) || {};
                        property.properties = uiState.properties;
                }
 
-               const hasProperties = _isArray(property.properties) && property.properties.length;
-               const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(property);
-
-               // ensure value is not undefined for non-leaf property types
-               if (isObject) {
-                       if (typeof value !== 'object') {
-                               value = isArray ? [] : {};
-                       }
-               }
-               const valueAsArray = _isArray(value) ? value : isLeafList && typeof value === 'undefined' ? [] : [value];
-
-               const isMetaField = property.name === 'meta';
-               const isCVNFD = property.name === 'constituent-vnfd';
-               const isSimpleListView = Property.isSimpleList(property);
-
-               valueAsArray.forEach((value, index) => {
-
-                       let field;
-                       const valuePath = path.slice();
-                       // create a unique field Id for use as react component keys and html element ids
-                       // notes: 
-                       //   keys only need to be unique on components in the same array
-                       //   html element ids should be unique with the document (or form)
-                       let fieldId = uniqueId;
-
-                       if (isArray) {
-                               valuePath.push(index);
-                               fieldId = isLeafList ? fieldId + index + value : resolveReactKey(value);
-                       }
-
-                       if (isMetaField) {
-                               if (typeof value === 'object') {
-                                       value = JSON.stringify(value, undefined, 12);
-                               } else if (typeof value !== 'string') {
-                                       value = '{}';
-                               }
-                       }
-
-                       if (isMissingDescriptorMeta) {
-                               field = <span key={fieldId} className="warning">No Descriptor Meta for {property.name}</span>;
-                       } else if (property.type === 'choice') {
-                               field = buildChoice(container, property, valuePath, value, fieldId);
-                       } else if (isSimpleListView) {
-                               field = buildSimpleListItem(container, property, valuePath, value, fieldId, index);
-                       } else if (isLeafList) {
-                               field = buildLeafListItem(container, property, valuePath, value, fieldId, index);
-                       } else if (hasProperties) {
-                               field = buildElement(container, property, valuePath, value, fieldId)
-                       } else {
-                               field = buildField(container, property, valuePath, value, fieldId);
-                       }
-
-                       function onClickLeaf(property, path, value, event) {
-                               if (event.isDefaultPrevented()) {
-                                       return;
-                               }
-                               event.preventDefault();
-                               event.stopPropagation();
-                               this.getRoot().uiState.focusedPropertyPath = path.join('.');
-                               console.debug('property selected', path.join('.'));
-                               ComposerAppActions.propertySelected([path.join('.')]);
-                       }
-
-                       const clickHandler = isLeaf ? onClickLeaf : () => {};
-                       const isContainerList = isArray && !(isSimpleListView || isLeafList);
-
-                       fields.push(
-                               <div key={fieldId}
-                                        className={ClassNames('property-content', {'simple-list': isSimpleListView})}
-                                        onClick={clickHandler.bind(container, property, valuePath, value)}>
-                                       {isContainerList ? buildRemoveListItem(container, property, valuePath, index) : null}
-                                       {field}
-                               </div>
+               if (property.type === 'leaf') {
+                       return buildField(property, path, value, uniqueId);
+               } else if (property.type === 'leaf_list') {
+                       return buildLeafList(property, path, value, uniqueId);
+               } else if (property.type === 'list') {
+                       return Property.isSimpleList(property) ?
+                               buildSimpleList(property, path, value, uniqueId)
+                               :
+                               buildList(property, path, value, uniqueId);
+               } else if (property.type === 'container') {
+                       return buildContainer(property, path, value, uniqueId);
+               } else if (property.type === 'choice') {
+                       return buildChoice(property, path, value, uniqueId);
+               } else {
+                       return (
+                               <span key={fieldId} className="warning">No Descriptor Meta for {property.name}</span>
                        );
-
-               });
-
-               classNames['-is-leaf'] = isLeaf;
-               classNames['-is-array'] = isArray;
-               classNames['cols-' + columnCount] = isColumnar;
-
-               if (property.type === 'choice') {
-                       value = utils.resolvePath(container.model, ['uiState.choice'].concat(path, 'selected').join('.'));
-                       if(!value) {
-                               property.properties.map(function(p) {
-                                       let pname = p.properties[0] && p.properties[0].name;
-                                       if(container.model.hasOwnProperty(pname)) {
-                                               value = container.model[pname];
-                                       }
-                               })
-                       }
                }
+       }
 
-               let displayValue = typeof value === 'object' ? '' : value;
-               const displayValueInfo = isArray ? valueAsArray.filter(d => typeof d !== 'undefined').length + ' items' : '';
-
-               const onFocus = isLeaf ? event => event.target.classList.add('-is-focused') : false;
-
-               return (
-                       <div key={uniqueId} className={ClassNames(property.type + '-property property', classNames)} onFocus={onFocus}>
-                               <h3 className="property-label">
-                                       <label htmlFor={uniqueId}>
-                                               <span className={property.type + '-name name'}>{title}</span>
-                                               <small>
-                                                       <span className={property.type + '-info info'}>{displayValueInfo}</span>
-                                                       <span className={property.type + '-value value'}>{displayValue}</span>
-                                               </small>
-                                               {isArray ? buildAddPropertyAction(container, property, path.concat(valueAsArray.length)) : null}
-                                       </label>
-                               </h3>
-                               <span className={property.type + '-description description'}>{property.description}</span>
-                               <val className="property-value">
-                                       {isCVNFD ? <span className={property.type + '-tip tip'}>Drag a VNFD from the Catalog to add more.</span> : null}
-                                       {fields}
-                               </val>
-                       </div>
-               );
 
+       if (!(DescriptorModelFactory.isContainer(container))) {
+               return
        }
 
        const containerType = container.uiState['qualified-type'] || container.uiState.type;
-       const basicProperties = getDescriptorMetaBasicForType(containerType).properties;
-
-       function buildBasicGroup() {
-               if (basicProperties.length === 0) {
-                       return null;
-               }
-               return (
-                       <div className="basic-properties-group">
-                               <h2>Basic</h2>
-                               <div>
-                                       {buildComponentsForProperties(container, basicProperties, [], container.model)}
-                               </div>
-                       </div>
-               );
+       let properties = DescriptorModelMetaFactory.getModelMetaForType(containerType).properties;
+       const breadcrumb = [];
+       if (container.parent) {
+               breadcrumb.push(container.parent);
        }
-
-       function buildAdvancedGroup() {
-               const properties = getDescriptorMetaAdvancedForType(containerType).properties;
-               if (properties.length === 0) {
-                       return null;
+       breadcrumb.push(container);
+       // bubble all data properties to top of list
+       let twoLists = properties.reduce((o, property) => {
+               const value = _get(container.model, [property.name]);
+               if (isDataProperty(property)) {
+                       o.listOne.push(property);
+               } else {
+                       o.listTwo.push(property);
                }
-               const hasBasicFields = basicProperties.length > 0;
-               const closeGroup = basicProperties.length > 0;
-               return (
-                       <div className="advanced-properties-group">
-                               <h1 data-toggle={closeGroup ? 'true' : 'false'} className={ClassNames({'-is-toggled': closeGroup})} onClick={toggle} style={{display: hasBasicFields ? 'block' : 'none'}}>
-                                       <a className="toggle-show-more" href="#show-more-properties">more&hellip;</a>
-                                       <a className="toggle-show-less" href="#show-more-properties">less&hellip;</a>
-                               </h1>
-                               <div className="toggleable">
-                                       {buildComponentsForProperties(container, properties, [], container.model, {toggle: true, width: props.width})}
-                               </div>
-                               <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>
-                       </div>
-               );
-       }
-
-       function buildMoreLess(d, i) {
-               return (
-                       <span key={'bread-crumb-part-' + i}>
-                               <a href="#select-item" onClick={onClickSelectItem.bind(d, null, null, d)}>{d.title}</a>
-                               <i> / </i>
-                       </span>
-               );
+               return o;
+       }, { listOne: [], listTwo: [] });
+       properties = twoLists.listOne.concat(twoLists.listTwo);
+       const children = buildComponentsForProperties(properties, [], container.model);
+
+       function onClick(event) {
+               console.debug(event.target);
+               if (event.isDefaultPrevented()) {
+                       return;
+               }
+               event.preventDefault();
+               event.stopPropagation();
+               // notifyFocusedHandler();
        }
 
-       const path = [];
-       if (container.parent) {
-               path.push(container.parent);
+       function onWrapperFocus(event) {
+               console.debug(event.target);
+               //notifyFocusedHandler();
        }
-       path.push(container);
 
        return (
-               <div className="EditDescriptorModelProperties -is-tree-view">
-                       <h1>{path.map(buildMoreLess)}</h1>
-                       {buildBasicGroup()}
-                       {buildAdvancedGroup()}
+               <div className="EditDescriptorModelProperties -is-tree-view" onClick={onClick} onFocus={onWrapperFocus}>
+                       <ModelBreadcrumb path={breadcrumb} />
+                       {children}
                </div>
        );
 };