Merge branch 'master' into projects
[osm/UI.git] / skyquake / plugins / composer / src / src / components / EditDescriptorModelProperties.js
index f3b2c7a..4afa3ff 100644 (file)
  *
  * This class generates the form fields used to edit the CONFD JSON model.
  */
-'use strict';
 
 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 utils from '../libraries/utils'
 import React from 'react'
 import ClassNames from 'classnames'
@@ -48,6 +52,16 @@ import imgRemove from '../../../node_modules/open-iconic/svg/trash.svg'
 
 import '../styles/EditDescriptorModelProperties.scss'
 
+const EMPTY_LEAF_PRESENT = '--empty-leaf-set--';
+
+function resolveReactKey(value) {
+       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: []};
@@ -76,6 +90,8 @@ function getTitle(model = {}) {
 export default function EditDescriptorModelProperties(props) {
 
        const container = props.container;
+       const readonly = props.readonly;
+       const isEditable = !readonly; //true
 
        if (!(DescriptorModelFactory.isContainer(container))) {
                return
@@ -138,11 +154,17 @@ export default function EditDescriptorModelProperties(props) {
                                create(model, path, property);
                        } else {
                                const name = path.join('.');
-                               const value = Property.createModelInstance(property);
+                               // 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());
                }
+               if(readonly) {
+                       return null;
+               }
                return (
                                <Button className="inline-hint" onClick={onClickAddProperty.bind(container, property, path)} label="Add" src={imgAdd} />
                );
@@ -160,36 +182,50 @@ export default function EditDescriptorModelProperties(props) {
                        }
                        CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
                }
+               if(readonly) {
+                       return null;
+               }
                return (
                        <Button className="remove-property-action inline-hint" title="Remove" onClick={onClickRemoveProperty.bind(container, property, path)} label="Remove" src={imgRemove}/>
                );
        }
 
-       function onFormFieldValueChanged(event) {
-               if (DescriptorModelFactory.isContainer(this)) {
-                       event.preventDefault();
-                       const name = event.target.name;
-                       const value = event.target.value;
-                       utils.assignPathValue(this.model, name, value);
-                       CatalogItemsActions.catalogItemDescriptorChanged(this.getRoot());
-               }
-       }
-
-       function buildField(container, property, path, value, fieldId) {
+       function buildField(container, property, path, value, fieldKey) {
                let cds = CatalogDataStore;
                let catalogs = cds.getTransientCatalogs();
 
                const pathToProperty = path.join('.');
-               const isEditable = true;
                const isGuid = Property.isGuid(property);
                const isBoolean = Property.isBoolean(property);
-               const onChange = onFormFieldValueChanged.bind(container);
                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) => {
@@ -205,19 +241,18 @@ export default function EditDescriptorModelProperties(props) {
                                options.unshift(<option key={'(value-not-in-enum)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
                        }
                        return (
-                               <select 
-                                       key={fieldId} 
-                                       id={fieldId}
-                                       name={pathToProperty} 
-                                       className={ClassNames({'-value-not-set': !isValueSet})} 
-                                       value={value} 
-                                       title={pathToProperty} 
-                                       onChange={onChange} 
-                                       onFocus={onFocus} 
-                                       onBlur={endEditing} 
-                                       onMouseDown={startEditing} 
-                                       onMouseOver={startEditing} 
-                                       readOnly={!isEditable}>
+                               <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}
+                                       disabled={!isEditable}>
                                                {options}
                                </select>
                        );
@@ -241,19 +276,18 @@ export default function EditDescriptorModelProperties(props) {
                                options.unshift(<option key={'(value-not-in-leafref)'} value="" placeholder={placeholder}>{noValueDisplayText}</option>);
                        }
                        return (
-                               <select 
-                                       key={fieldId} 
-                                       id={fieldId} 
-                                       name={pathToProperty}
-                                       className={ClassNames({'-value-not-set': !isValueSet})} 
-                                       value={value} 
-                                       title={pathToProperty} 
-                                       onChange={onChange} 
-                                       onFocus={onFocus} 
-                                       onBlur={endEditing} 
-                                       onMouseDown={startEditing} 
-                                       onMouseOver={startEditing} 
-                                       readOnly={!isEditable}>
+                               <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}
+                                       disabled={!isEditable}>
                                                {options}
                                </select>
                        );
@@ -275,17 +309,47 @@ export default function EditDescriptorModelProperties(props) {
                        }
                        const isValueSet = (val != '' && val)
                        return (
-                               <select 
-                                       key={fieldId} 
-                                       id={fieldId} 
-                                       name={pathToProperty}
-                                       className={ClassNames({'-value-not-set': !isValueSet})} 
-                                       value={val && val.toUpperCase()} title={pathToProperty} 
-                                       onChange={onChange} onFocus={onFocus} 
-                                       onBlur={endEditing} 
-                                       onMouseDown={startEditing} 
-                                       onMouseOver={startEditing} 
-                                       readOnly={!isEditable}>
+                               <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}
+                                       disabled={!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}
+                                       disabled={!isEditable}>
                                                {options}
                                </select>
                        );
@@ -293,70 +357,77 @@ export default function EditDescriptorModelProperties(props) {
 
                if (property['preserve-line-breaks']) {
                        return (
-                               <textarea 
-                                       key={fieldId} 
-                                       cols="5" 
-                                       id={fieldId} 
-                                       name={pathToProperty}
-                                       value={value} 
-                                       placeholder={placeholder} 
-                                       onChange={onChange} 
-                                       onFocus={onFocus} 
-                                       onBlur={endEditing} 
-                                       onMouseDown={startEditing} 
-                                       onMouseOver={startEditing} 
-                                       onMouseOut={endEditing} 
-                                       onMouseLeave={endEditing} 
-                                       readOnly={!isEditable} />
+                               <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}
+                                       disabled={!isEditable} />
                        );
                }
 
                return (
-                       <input 
-                               key={fieldId}
-                               id={fieldId}
-                               name={pathToProperty}
+                       <input
+                               key={fieldKey}
+                               id={fieldKey}
                                type="text"
-                               value={fieldValue}
+                               defaultValue={fieldValue}
                                className={className}
                                placeholder={placeholder}
-                               onChange={onChange}
+                               onChange={onTextChange}
                                onFocus={onFocus}
                                onBlur={endEditing}
                                onMouseDown={startEditing}
                                onMouseOver={startEditing}
                                onMouseOut={endEditing}
                                onMouseLeave={endEditing}
-                               readOnly={!isEditable}
+                               disabled={!isEditable}
                        />
                );
 
        }
 
-       function buildElement(container, property, valuePath, value) {
-               return property.properties.map((property, index) => {
-                       let childValue;
-                       const childPath = valuePath.slice();
-                       if (typeof value === 'object') {
-                               childValue = value[property.name];
+       /**
+        * buiid and return an array of components representing an editor for each property.
+        * 
+        * @param {any} container the master document being edited
+        * @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) {
+               return properties.map((property) => {
+                       let value;
+                       let propertyPath = pathToProperties.slice();
+                       if (data && typeof data === 'object') {
+                               value = data[property.name];
                        }
                        if(property.type != 'choice'){
-                                               childPath.push(property.name);
+                               propertyPath.push(property.name);
                        }
-                       return build(container, property, childPath, childValue);
-
+                       return build(container, property, propertyPath, value, props);
                });
        }
 
-       function buildChoice(container, property, path, value, fieldId) {
-               function onFormFieldValueChanged(event) {
-                       if (DescriptorModelFactory.isContainer(this)) {
-
-                               event.preventDefault();
+       function buildElement(container, property, valuePath, value) {
+               return buildComponentsForProperties(container, property.properties, valuePath, value);
+       }
 
-                               let name = event.target.name;
-                               const value = event.target.value;
+       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.
@@ -390,7 +461,7 @@ export default function EditDescriptorModelProperties(props) {
                                // write the current choice value into the state
                                let choiceObject = utils.resolvePath(this.model, [name, selected].join('.'));
                                let isTopCase = false;
-                               if (!choiceObject) {
+                               if (choiceObject) {
                                        isTopCase = true;
                                        choiceObject = utils.resolvePath(this.model, [selected].join('.'));
                                }
@@ -416,7 +487,6 @@ export default function EditDescriptorModelProperties(props) {
                                        utils.assignPathValue(this.model, [value].join('.'), newChoiceObject)
                                }
 
-
                                // update the selected name
                                utils.assignPathValue(this.model, statePath.concat('selected').join('.'), value);
 
@@ -424,9 +494,15 @@ export default function EditDescriptorModelProperties(props) {
                        }
                }
 
+               const pathToChoice = path.join('.');
                const caseByNameMap = {};
 
-               const onChange = onFormFieldValueChanged.bind(container);
+               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') {
@@ -452,30 +528,29 @@ export default function EditDescriptorModelProperties(props) {
                        );
                });
 
-               const selectName = path.join('.');
-               let selectedOptionPath = ['uiState.choice', selectName, 'selected'].join('.');
+               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, selectName);
+                       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(fieldProperties.hasOwnProperty(c.optionValue.split('.')[1])) {
-                                               utils.assignPathValue(container.model, ['uiState.choice', selectName, 'selected'].join('.'), c.optionValue);
+                                       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', selectName, 'selected'].join('.'));
+                               selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'));
                        } else {
                                property.properties.map(function(p) {
-                                       let pname = p.properties[0].name;
+                                       let pname = p.properties[0] && p.properties[0].name;
                                        if(container.model.hasOwnProperty(pname)) {
-                                               utils.assignPathValue(container.model, ['uiState.choice', selectName, 'selected'].join('.'), [p.name, pname].join('.'));
+                                               utils.assignPathValue(container.model, ['uiState.choice', pathToChoice, 'selected'].join('.'), [p.name, pname].join('.'));
                                        }
                                })
-                               selectedOptionValue = utils.resolvePath(container.model, ['uiState.choice', selectName, 'selected'].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.
@@ -484,8 +559,15 @@ export default function EditDescriptorModelProperties(props) {
                const hasProperties = _isArray(valueProperty.properties) && valueProperty.properties.length;
                const isMissingDescriptorMeta = !hasProperties && !Property.isLeaf(valueProperty);
                //Some magic that prevents errors for arising
-               const valueResponse = valueProperty.properties && valueProperty.properties.length ? valueProperty.properties.map(valuePropertyFn) : (!isMissingDescriptorMeta) ? build(container, valueProperty, path.concat(valueProperty.name), utils.resolvePath(container.model, path.concat(valueProperty.name).join('.')) || container.model[valueProperty.name]) :
-               valueProperty.map && valueProperty.map(valuePropertyFn);
+               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 valuePropertyFn(d, i) {
                        const childPath = path.concat(valueProperty.name, d.name);
                        const childValue = utils.resolvePath(container.model, childPath.join('.'));
@@ -499,8 +581,20 @@ export default function EditDescriptorModelProperties(props) {
                const onFocus = onFocusPropertyFormInputElement.bind(container, property, path, value);
 
                return (
-                       <div key={fieldId} className="choice">
-                               <select id={fieldId} className={ClassNames({'-value-not-set': !selectedOptionValue})} name={selectName} value={selectedOptionValue} onChange={onChange} onFocus={onFocus} onBlur={endEditing} onMouseDown={startEditing} onMouseOver={startEditing} onMouseOut={endEditing} onMouseLeave={endEditing}>
+                       <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}
+                                       disabled={!isEditable}
+                               >
                                        {options}
                                </select>
                                {valueResponse}
@@ -537,7 +631,7 @@ export default function EditDescriptorModelProperties(props) {
                );
        }
 
-       function buildLeafListItem(container, property, valuePath, value, index) {
+       function buildLeafListItem(container, property, valuePath, value, uniqueId, index) {
                // look at the type to determine how to parse the value
                return (
                        <div key={uniqueId}>
@@ -596,14 +690,14 @@ export default function EditDescriptorModelProperties(props) {
                        let field;
                        const valuePath = path.slice();
                        // create a unique field Id for use as react component keys and html element ids
-                       // notes: 
+                       // 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 += index;
+                               fieldId = isLeafList ? fieldId + index + value : resolveReactKey(value);
                        }
 
                        if (isMetaField) {
@@ -635,7 +729,7 @@ export default function EditDescriptorModelProperties(props) {
                                event.preventDefault();
                                event.stopPropagation();
                                this.getRoot().uiState.focusedPropertyPath = path.join('.');
-                               console.log('property selected', path.join('.'));
+                               console.debug('property selected', path.join('.'));
                                ComposerAppActions.propertySelected([path.join('.')]);
                        }
 
@@ -661,7 +755,7 @@ export default function EditDescriptorModelProperties(props) {
                        value = utils.resolvePath(container.model, ['uiState.choice'].concat(path, 'selected').join('.'));
                        if(!value) {
                                property.properties.map(function(p) {
-                                       let pname = p.properties[0].name;
+                                       let pname = p.properties[0] && p.properties[0].name;
                                        if(container.model.hasOwnProperty(pname)) {
                                                value = container.model[pname];
                                        }
@@ -707,11 +801,7 @@ export default function EditDescriptorModelProperties(props) {
                        <div className="basic-properties-group">
                                <h2>Basic</h2>
                                <div>
-                                       {basicProperties.map(property => {
-                                               const path = [property.name];
-                                               const value = container.model[property.name];
-                                               return build(container, property, path, value);
-                                       })}
+                                       {buildComponentsForProperties(container, basicProperties, [], container.model)}
                                </div>
                        </div>
                );
@@ -731,11 +821,7 @@ export default function EditDescriptorModelProperties(props) {
                                        <a className="toggle-show-less" href="#show-more-properties">less&hellip;</a>
                                </h1>
                                <div className="toggleable">
-                                       {properties.map(property => {
-                                               const path = [property.name];
-                                               const value = container.model[property.name];
-                                               return build(container, property, path, value, {toggle: true, width: props.width});
-                                       })}
+                                       {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>
@@ -764,5 +850,4 @@ export default function EditDescriptorModelProperties(props) {
                        {buildAdvancedGroup()}
                </div>
        );
-
-}
+};