update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b third try
[osm/UI.git] / skyquake / plugins / composer / src / src / components / model / LeafEditor.jsx
1 /*
2  *
3  *   Copyright 2016-2017 RIFT.IO Inc
4  *
5  *   Licensed under the Apache License, Version 2.0 (the "License");
6  *   you may not use this file except in compliance with the License.
7  *   You may obtain a copy of the License at
8  *
9  *       http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *   Unless required by applicable law or agreed to in writing, software
12  *   distributed under the License is distributed on an "AS IS" BASIS,
13  *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *   See the License for the specific language governing permissions and
15  *   limitations under the License.
16  *
17  */
18
19 import _debounce from 'lodash/debounce';
20 import React from 'react'
21 import ClassNames from 'classnames'
22 import changeCase from 'change-case'
23 import Property from '../../libraries/model/DescriptorModelMetaProperty'
24 import CatalogDataStore from '../../stores/CatalogDataStore'
25 import _isInt from 'validator/lib/isInt'
26 import _toInt from 'validator/lib/toInt'
27 import _isFloat from 'validator/lib/isFloat'
28 import _toFloat from 'validator/lib/toFloat'
29 import _trim from 'validator/lib/trim'
30 import _isIP from 'validator/lib/isIP'
31
32 import { startEditing, endEditing, onFocusPropertyFormInputElement } from './EditDescriptorUtils'
33 import Select from './Select'
34
35 function validateRequired(isRequired, value) {
36         value = value.trim();
37         return isRequired && !value ? { success: false, message: "A value is required." } : { success: true, value: null };
38 }
39
40 function editorExitHandler(isValueRequired, onExit, onError, event) {
41         const value = event.target.value;
42         const result = validateRequired(isValueRequired, value);
43         onExit && onExit(result);
44         endEditing();
45 }
46
47 function Enumeration(props) {
48         const { id, property, title, readOnly, onChange, onError, onExit } = props;
49         let value = props.value;
50         const enumeration = Property.getEnumeration(property, value);
51         const hasDefaultValue = !!property['default-value'];
52         const required = property.mandatory || hasDefaultValue;
53         if (!value && hasDefaultValue) {
54                 value = property['default-value'];
55         }
56         return (
57                 <Select
58                         id={id}
59                         value={value}
60                         options={enumeration}
61                         title={title}
62                         placeholder={property.name}
63                         onChange={onChange}
64                         onExit={editorExitHandler.bind(null, required, onExit, onError)}
65                         required={required} r
66                         readOnly={readOnly} />
67         );
68 }
69
70 function Reference(props) {
71         const { id, property, title, path, container, readOnly, onChange, onError, onExit } = props;
72         let value = props.value;
73         const catalogs = props.catalogs || CatalogDataStore.getTransientCatalogs();
74         let fullPathString = container.key + ':' + path.join(':');
75         let containerRef = container;
76         while (containerRef.parent) {
77                 fullPathString = containerRef.parent.key + ':' + fullPathString;
78                 containerRef = containerRef.parent;
79         }
80         const leafRefPathValues = Property.getLeafRef(property, path, value, fullPathString, catalogs, container);
81         const required = property.mandatory;
82         if (value && !leafRefPathValues.find(option => option.isSelected)) {
83                 value = null;
84         }
85
86         return (
87                 <Select
88                         id={id}
89                         value={value}
90                         options={leafRefPathValues}
91                         title={title}
92                         placeholder={property.name}
93                         onChange={onChange}
94                         onExit={editorExitHandler.bind(null, required, onExit, onError)}
95                         required={required}
96                         readOnly={readOnly} />
97         );
98 }
99
100 function Boolean(props) {
101         const { id, property, title, readOnly, onChange, onError, onExit } = props;
102         let value = props.value;
103         const hasDefaultValue = !!property['default-value'];
104         const required = property.mandatory || hasDefaultValue;
105         const typeOfValue = typeof value;
106         if (typeOfValue === 'number' || typeOfValue === 'boolean') {
107                 value = value ? 'TRUE' : 'FALSE';
108         } else if (value) {
109                 value = value.toUpperCase();
110         } else {
111                 if (hasDefaultValue) {
112                         value = ('' + property['default-value']).toUpperCase();
113                 }
114         }
115         const options = [
116                 { name: "TRUE", value: 'TRUE' },
117                 { name: "FALSE", value: 'FALSE' }
118         ]
119         return (
120                 <Select
121                         id={id}
122                         value={value}
123                         options={options}
124                         title={title}
125                         placeholder={property.name}
126                         onChange={onChange}
127                         onExit={editorExitHandler.bind(null, required, onExit, onError)}
128                         required={required}
129                         readOnly={readOnly} />
130         );
131 }
132
133 function Empty(props) {
134         // A null value indicates the leaf exists (as opposed to undefined).
135         // We stick in a string when the user actually sets it to simplify things
136         // but the correct thing happens when we serialize to user data
137         const EMPTY_LEAF_PRESENT = '--empty-leaf-set--';
138         const { id, property, value, title, readOnly, onChange } = props;
139         let isEmptyLeafPresent = !!value;
140         let present = isEmptyLeafPresent ? EMPTY_LEAF_PRESENT : "";
141         const options = [
142                 { name: "Enabled", value: EMPTY_LEAF_PRESENT }
143         ]
144
145         return (
146                 <Select
147                         id={id}
148                         value={present}
149                         placeholder={"Not Enabled"}
150                         options={options}
151                         title={title}
152                         onChange={onChange}
153                         readOnly={readOnly} />
154         );
155 }
156
157 function getValidator(property) {
158         function validateInteger(constraints, value) {
159                 return _isInt(value, constraints) ? { success: true, value: _toInt(value) } :
160                         { success: false, value, message: "The value is not an integer or does not meet the property constraints." };
161         }
162         function validateDecimal(constraints, value) {
163                 return _isFloat(value, constraints) ? { success: true, value: _toFloat(value) } :
164                         { success: false, value, message: "The value is not a decimal number or does not meet the property constraints." };
165         }
166         function validateProperty(validator, errorMessage, value) {
167                 return validator(value) ? { success: true, value } :
168                         { success: false, value, message: errorMessage };
169         }
170         const name = property.name;
171         if (name === 'ip-address' || name.endsWith('-ip-address')) {
172                 return validateProperty.bind(null, _isIP, "The value is not a valid ip address.")
173         }
174         switch (property['data-type']) {
175                 case 'int8':
176                         return validateInteger.bind(null, { min: -128, max: 127 });
177                 case 'int16':
178                         return validateInteger.bind(null, { min: -32768, max: 32767 });
179                 case 'int32':
180                         return validateInteger.bind(null, { min: -2147483648, max: 2147483647 });
181                 case 'int64':
182                         return validateInteger.bind(null, null);
183                 case 'uint8':
184                         return validateInteger.bind(null, { min: 0, max: 255 });
185                 case 'uint16':
186                         return validateInteger.bind(null, { min: 0, max: 65535 });
187                 case 'uint32':
188                         return validateInteger.bind(null, { min: 0, max: 4294967295 });
189                 case 'uint64':
190                         return validateInteger.bind(null, { min: 0 });
191                 case 'decimal64':
192                         return validateDecimal.bind(null, null)
193                 case 'string':
194                 default:
195                         return function (value) { return { success: true, value } };
196         }
197 }
198
199 function messageTemplate(strings, ...keys) {
200         return (function (...vars) {
201                 let helpInfo = vars.reduce((o, info) => Object.assign(o, info), {});
202                 return keys.reduce((s, key, i) => {
203                         return s + helpInfo[key] + strings[i + 1];
204                 }, strings[0]);
205         });
206 }
207
208 const errorMessage = messageTemplate`"${'value'}" is ${'error'}. ${'message'}`;
209
210 class Input extends React.Component {
211         constructor(props) {
212                 super(props);
213                 let originalValue = props.value ? props.value : null; // normalize empty value
214                 this.state = { originalValue };
215         }
216
217         componentWillReceiveProps(nextProps) {
218                 const { value } = nextProps
219                 if (value !== this.state.originalValue) {
220                         let originalValue = value ? value : null; // normalize empty value
221                         this.setState({ originalValue })
222                 }
223         }
224
225         render() {
226                 const { id, property, value, title, readOnly, onChange, onError, onExit } = this.props;
227                 const { originalValue } = this.state;
228                 const isGuid = Property.isGuid(property);
229                 const className = ClassNames(property.name + '-input', { '-is-guid': isGuid, '-is-required': required });
230                 const placeholder = property.name;
231                 const required = property.mandatory;
232
233                 const validator = getValidator(property);
234                 function handleValueChanged(newValue) {
235                         newValue = newValue.trim();
236                         const result = !newValue ? validateRequired(required, newValue) : validator(newValue);
237                         result.success ? (originalValue !== result.value) && onChange(result.value) : onError(result.message);
238                 }
239                 const changeHandler = _debounce(handleValueChanged, 2000);
240                 function onInputChange(e) {
241                         e.preventDefault();
242                         changeHandler(_trim(e.target.value));
243                 }
244                 function onBlur(e) {
245                         changeHandler.cancel();
246                         const value = _trim(e.target.value);
247                         const result = !value ? validateRequired(required, value) : validator(value);
248                         if (result.success) {
249                                 // just in case we missed it by cancelling the debouncer
250                                 (originalValue !== result.value) && onChange(result.value);
251                         }
252                         onExit(result);
253                 }
254                 if (property['preserve-line-breaks']) {
255                         return (
256                                 <textarea
257                                         cols="5"
258                                         id={id}
259                                         defaultValue={value}
260                                         placeholder={placeholder}
261                                         className={className}
262                                         onChange={readOnly ? null : onInputChange}
263                                         onFocus={onFocusPropertyFormInputElement}
264                                         onBlur={readOnly ? null : editorExitHandler.bind(null, required, onExit, onError)}
265                                         onMouseDown={startEditing}
266                                         onMouseOver={startEditing}
267                                         onMouseOut={endEditing}
268                                         onMouseLeave={endEditing}
269                                         required={required} readOnly={readOnly} />
270                         );
271                 }
272                 return (
273                         <input
274                                 id={id}
275                                 type="text"
276                                 defaultValue={value}
277                                 className={className}
278                                 placeholder={placeholder}
279                                 onChange={readOnly ? null : onInputChange}
280                                 onFocus={onFocusPropertyFormInputElement}
281                                 onBlur={readOnly ? null : onBlur}
282                                 onMouseDown={startEditing}
283                                 onMouseOver={startEditing}
284                                 onMouseOut={endEditing}
285                                 onMouseLeave={endEditing}
286                                 required={required} readOnly={readOnly}
287                         />
288                 );
289         }
290 }
291
292 export {
293         Input,
294         Empty,
295         Boolean,
296         Reference,
297         Enumeration
298 };