update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b third try
[osm/UI.git] / skyquake / plugins / composer / src / src / components / EditDescriptorModelProperties.js
1 /*
2 *
3 * Copyright 2016 RIFT.IO Inc
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18 /**
19 * Created by onvelocity on 1/18/16.
20 *
21 * This class generates the form fields used to edit the CONFD JSON model.
22 */
23
24 import _uniqueId from 'lodash/uniqueId';
25 import _set from 'lodash/set';
26 import _get from 'lodash/get';
27 import _has from 'lodash/has';
28 import _keys from 'lodash/keys';
29 import _isObject from 'lodash/isObject';
30 import _isArray from 'lodash/isArray';
31 import utils from '../libraries/utils'
32 import React from 'react'
33 import changeCase from 'change-case'
34 import toggle from '../libraries/ToggleElementHandler'
35 import Property from '../libraries/model/DescriptorModelMetaProperty'
36 import SelectionManager from '../libraries/SelectionManager'
37 import ComposerAppActions from '../actions/ComposerAppActions'
38 import CatalogItemsActions from '../actions/CatalogItemsActions'
39 import DescriptorEditorActions from '../actions/DescriptorEditorActions'
40 import DescriptorModelFactory from '../libraries/model/DescriptorModelFactory'
41 import DescriptorModelMetaFactory from '../libraries/model/DescriptorModelMetaFactory'
42
43 import ModelBreadcrumb from './model/ModelBreadcrumb'
44 import ListItemAsLink from './model/ListItemAsLink'
45 import LeafField from './model/LeafField'
46 import { List, ListItem } from './model/List'
47 import ContainerWrapper from './model/Container'
48 import Choice from './model/Choice'
49
50 import '../styles/EditDescriptorModelProperties.scss'
51
52
53 function resolveReactKey(value) {
54 const keyPath = ['uiState', 'fieldKey'];
55 if (!_has(value, keyPath)) {
56 _set(value, keyPath, _uniqueId());
57 }
58 return _get(value, keyPath);
59 }
60
61 function getTipForProperty(property) {
62 return property.name === 'constituent-vnfd' ? "Drag a VNFD from the Catalog to add more." : null
63 }
64
65 function selectModel(container, model, property) {
66 ComposerAppActions.selectModel(container.findChildByUid(model));
67 }
68
69 function removeListEntry(container, property, path) {
70 DescriptorEditorActions.removeListItem({ descriptor: container, property, path });
71 }
72
73 function createAndAddItemToPath(container, property, path) {
74 DescriptorEditorActions.addListItem({ descriptor: container, property, path });
75 }
76
77 function notifyPropertyFocused(container, path) {
78 container.getRoot().uiState.focusedPropertyPath = path.join('.');
79 console.debug('property selected', path.join('.'));
80 ComposerAppActions.propertySelected([path.join('.')]);
81 }
82
83 function setPropertyOpenState(container, path, property, isOpen) {
84 DescriptorEditorActions.setOpenState({ descriptor: container, property, path, isOpen });
85 }
86
87 function isDataProperty(property) {
88 return property.type === 'leaf' || property.type === 'leaf_list' || property.type === 'choice';
89 }
90
91 function checkIfValueEmpty(value) {
92 if (value === null || typeof value === 'undefined') {
93 return true;
94 } else if (_isArray(value) && !value.length) {
95 return true;
96 } else if (_isObject(value)) {
97 const keys = _keys(value);
98 if (keys.length < 2) {
99 return !keys.length || (keys[0] === 'uiState')
100 }
101 }
102 return false;
103 }
104
105 export default function EditDescriptorModelProperties(props) {
106 const { container, idMaker, showHelp, collapsePanelsByDefault, openPanelsWithData } = props;
107 const readOnly = props.readOnly || container.isReadOnly;
108 const showElementHelp = showHelp.forAll;
109 const uiState = container.uiState;
110
111 function getPanelOpenedCondition(value, path) {
112 const showOpened = container.getUiState('opened', path);
113 if (typeof showOpened === 'undefined') {
114 return (openPanelsWithData && !checkIfValueEmpty(value)) ? true : !collapsePanelsByDefault;
115 }
116 return showOpened;
117 }
118
119 function buildField(property, path, value, fieldKey) {
120 const pathToProperty = path.join('.');
121 const fieldValue = value ? (value.constructor.name != "Object") ? value : '' : (isNaN(value) ? undefined : value);
122
123 // process the named field value change
124 function processFieldValueChange(name, value) {
125 console.debug('processed change for -- ' + name + ' -- with value -- ' + value);
126 if (DescriptorModelFactory.isContainer(container)) {
127 DescriptorEditorActions.setValue({ descriptor: container, path, value });
128 }
129 }
130
131 function onErrorHandler(message) {
132 DescriptorEditorActions.setError({ descriptor: container, path, message });
133 }
134
135 // create an onChange event handler for a select field for the specified field path
136 const onChangeHandler = processFieldValueChange.bind(null, pathToProperty);
137 return (
138 <LeafField
139 key={fieldKey}
140 container={container}
141 property={property}
142 path={path}
143 value={value}
144 id={fieldKey}
145 showHelp={showElementHelp}
146 onChange={onChangeHandler}
147 onError={onErrorHandler}
148 readOnly={readOnly}
149 errorMessage={_get(container.uiState, ['error'].concat(path))}
150 />
151 );
152 }
153
154 /**
155 * buiid and return an array of components representing an editor for each property.
156 *
157 * @param {any} container the master document being edited
158 * @param {[property]} properties
159 * @param {string} pathToProperties path within the container to the properties
160 * @param {Object} data source for each property
161 * @returns an array of react components
162 */
163 function buildComponentsForProperties(properties, pathToProperties, data) {
164 return properties.map((property) => {
165 let value;
166 let propertyPath = pathToProperties.slice();
167 if (property.type != 'choice') {
168 propertyPath.push(property.name);
169 }
170 if (data && typeof data === 'object') {
171 value = _get(data, property.name);
172 }
173 let result = null;
174 try {
175 result = buildPropertyComponent(property, propertyPath, value);
176 } catch (e) {
177 console.error(e);
178 }
179 return result;
180 });
181 }
182
183 function buildChoice(property, path, value, uniqueId) {
184 const uiStatePath = path.concat(['uiState']);
185 const choiceStatePath = ['choice', property.name];
186 const fullChoiceStatePath = uiStatePath.concat(choiceStatePath);
187
188 function determineSelectedChoice(model) {
189 let choiceState = utils.resolvePath(container.model, fullChoiceStatePath.join('.'));
190 if (choiceState) {
191 return property.properties.find(c => c.name === choiceState.selected);
192 }
193 const selectedCase = property.properties.find(c =>
194 c.properties && c.properties.find(p => _has(model, path.concat([p.name])))
195 );
196 // lets remember this
197 let stateObject = utils.resolvePath(container.model, uiStatePath.join('.'));
198 stateObject = _set(stateObject || {}, choiceStatePath, { selected: selectedCase ? selectedCase.name : "" });
199 utils.assignPathValue(container.model, uiStatePath.join('.'), stateObject);
200 return selectedCase;
201 }
202
203 function pullOutCaseModel(caseName) {
204 const model = container.model;
205 const properties = property.properties.find(c => c.name === caseName).properties;
206 return properties.reduce((o, p) => {
207 const valuePath = path.concat([p.name]).join('.');
208 const value = utils.resolvePath(model, valuePath);
209 if (value) {
210 o[p.name] = value;
211 }
212 return o;
213 }, {});
214 }
215
216 function processChoiceChange(value) {
217 if (DescriptorModelFactory.isContainer(container)) {
218 let uiState = utils.resolvePath(container.model, uiStatePath.join('.'));
219 // const stateObject = utils.resolvePath(container.model, fullChoiceStatePath.join('.')) || {};
220 let choiceState = _get(uiState, choiceStatePath);
221 const previouslySelectedChoice = choiceState.selected;
222 if (previouslySelectedChoice === value) {
223 return;
224 }
225 if (previouslySelectedChoice) {
226 choiceState[previouslySelectedChoice] = pullOutCaseModel(previouslySelectedChoice);
227 }
228 const modelUpdate = _keys(choiceState[previouslySelectedChoice]).reduce((o, k) => _set(o, k, null), {})
229 choiceState.selected = value;
230 _set(uiState, choiceStatePath, choiceState);
231 _set(modelUpdate, 'uiState', uiState);
232 if (choiceState.selected) {
233 const previous = choiceState[choiceState.selected];
234 if (previous) {
235 Object.assign(modelUpdate, previous);
236 } else {
237 const newChoice = property.properties.find(p => p.name === choiceState.selected);
238 if (newChoice.properties.length === 1) {
239 const property = newChoice.properties[0];
240 if (property.type === 'leaf' && property['data-type'] === 'empty') {
241 let obj = {};
242 obj[property.name] = [null];
243 Object.assign(modelUpdate, obj);
244 }
245 }
246 }
247 }
248 DescriptorEditorActions.assignValue({ descriptor: container, path, source: modelUpdate });
249 }
250 }
251
252 const selectedCase = determineSelectedChoice(container.model);
253 const children = selectedCase ?
254 <ContainerWrapper property={selectedCase} readOnly={readOnly} showHelp={showElementHelp} showOpened={true}>
255 {buildComponentsForProperties(selectedCase.properties, path, path.length ? _get(container.model, path) : container.model)}
256 </ContainerWrapper>
257 : null;
258
259 return (
260 <Choice key={uniqueId} id={uniqueId} onChange={processChoiceChange} readOnly={readOnly} showHelp={showElementHelp}
261 property={property} value={selectedCase ? selectedCase.name : null}
262 >
263 {children}
264 </Choice>
265 );
266
267 }
268
269 function buildLeafList(property, path, value, uniqueId) {
270 if (!Array.isArray(value)) {
271 value = [value];
272 }
273 const children = value && value.map((v, i) => {
274 let itemPath = path.concat([i]);
275 const field = buildField(property, itemPath, v, uniqueId + i);
276 return (
277 <ListItem key={':' + i} index={i} property={property} readOnly={readOnly} showHelp={showElementHelp}
278 showOpened={true} removeItemHandler={removeListEntry.bind(null, container, property, itemPath)} >
279 {field}
280 </ListItem>
281 )
282 });
283 return (
284 <List key={uniqueId} id={uniqueId} property={property} value={value} readOnly={readOnly} showHelp={showElementHelp}
285 showOpened={true} addItemHandler={createAndAddItemToPath.bind(null, container, property, path)}>
286 {children}
287 </List>
288 );
289 }
290
291 function buildList(property, path, value, uniqueId) {
292 if (value && !Array.isArray(value)) {
293 value = [value];
294 }
295 function getListItemSummary(index, value) {
296 const keys = property.key.map((key) => value[key]);
297 const summary = keys.join(' ');
298 return summary.length > 1 ? summary : '' + (index + 1);
299 }
300 const children = value && value.map((itemValue, i) => {
301 const itemPath = path.concat([i]);
302 const key = resolveReactKey(itemValue);
303 const children = buildComponentsForProperties(property.properties, itemPath, itemValue);
304 const showOpened = getPanelOpenedCondition(value, itemPath);
305 return (
306 <ListItem key={key} property={property} readOnly={readOnly} showHelp={showElementHelp}
307 summary={getListItemSummary(i, itemValue)} info={'' + (i + 1)}
308 removeItemHandler={removeListEntry.bind(null, container, property, itemPath)}
309 showOpened={showOpened} onChangeOpenState={setPropertyOpenState.bind(null, container, itemPath, property, !showOpened)}>
310 {children}
311 </ListItem>
312 )
313 });
314 const showOpened = getPanelOpenedCondition(value, path);
315 return (
316 <List key={uniqueId} id={uniqueId} property={property} value={value} readOnly={readOnly} showHelp={showElementHelp}
317 addItemHandler={createAndAddItemToPath.bind(null, container, property, path)}
318 showOpened={showOpened} onChangeOpenState={setPropertyOpenState.bind(null, container, path, property, !showOpened)}>
319 {children}
320 </List>
321 );
322 }
323
324 function buildSimpleList(property, path, value, uniqueId) {
325 if (value && !Array.isArray(value)) {
326 value = [value];
327 }
328 const children = value && value.map((v, i) => {
329 let itemPath = path.concat([i]);
330 return (
331 <ListItemAsLink key={':' + i} property={property} value={v}
332 removeItemHandler={removeListEntry.bind(null, container, property, itemPath)}
333 selectLinkHandler={selectModel.bind(null, container, v, property)} />
334 )
335 });
336 const tip = getTipForProperty(property);
337 const showOpened = getPanelOpenedCondition(value, path);
338 const changeOpenState = setPropertyOpenState.bind(null, container, path, property, !showOpened);
339 return (
340 <List name={uniqueId} id={uniqueId} key={uniqueId} tip={tip}
341 property={property} value={value} readOnly={readOnly} showHelp={showElementHelp}
342 addItemHandler={createAndAddItemToPath.bind(null, container, property, path)}
343 showOpened={showOpened} onChangeOpenState={changeOpenState}>
344 {children}
345 </List>
346 );
347 }
348
349 function buildContainer(property, path, value, uniqueId) {
350 const children = buildComponentsForProperties(property.properties, path, value);
351 const showOpened = getPanelOpenedCondition(value, path);
352 const changeOpenState = setPropertyOpenState.bind(null, container, path, property, !showOpened);
353 return (
354 <ContainerWrapper key={uniqueId} id={uniqueId} property={property} readOnly={readOnly}
355 showHelp={showElementHelp} summary={checkIfValueEmpty(value) ? null : '*'}
356 showOpened={showOpened} onChangeOpenState={changeOpenState}>
357 {children}
358 </ContainerWrapper>
359 );
360 }
361
362 function buildPropertyComponent(property, path, value) {
363
364 const fields = [];
365 const isArray = Property.isArray(property);
366 const isObject = Property.isObject(property);
367 const title = changeCase.titleCase(property.name);
368
369 // create a unique Id for use as react component keys and html element ids
370 // use uid (from ui info) instead of id property (which is not stable)
371 let uniqueId = idMaker(container, path);
372
373 if (!property.properties && isObject) {
374 console.debug('no properties', property);
375 const uiState = DescriptorModelMetaFactory.getModelMetaForType(property.name) || {};
376 property.properties = uiState.properties;
377 }
378
379 if (property.type === 'leaf') {
380 return buildField(property, path, value, uniqueId);
381 } else if (property.type === 'leaf_list') {
382 return buildLeafList(property, path, value, uniqueId);
383 } else if (property.type === 'list') {
384 return Property.isSimpleList(property) ?
385 buildSimpleList(property, path, value, uniqueId)
386 :
387 buildList(property, path, value, uniqueId);
388 } else if (property.type === 'container') {
389 return buildContainer(property, path, value, uniqueId);
390 } else if (property.type === 'choice') {
391 return buildChoice(property, path, value, uniqueId);
392 } else {
393 return (
394 <span key={fieldId} className="warning">No Descriptor Meta for {property.name}</span>
395 );
396 }
397 }
398
399
400 if (!(DescriptorModelFactory.isContainer(container))) {
401 return
402 }
403
404 const containerType = container.uiState['qualified-type'] || container.uiState.type;
405 let properties = DescriptorModelMetaFactory.getModelMetaForType(containerType).properties;
406 const breadcrumb = [];
407 if (container.parent) {
408 breadcrumb.push(container.parent);
409 }
410 breadcrumb.push(container);
411 // bubble all data properties to top of list
412 let twoLists = properties.reduce((o, property) => {
413 const value = _get(container.model, [property.name]);
414 if (isDataProperty(property)) {
415 o.listOne.push(property);
416 } else {
417 o.listTwo.push(property);
418 }
419 return o;
420 }, { listOne: [], listTwo: [] });
421 properties = twoLists.listOne.concat(twoLists.listTwo);
422 const children = buildComponentsForProperties(properties, [], container.model);
423
424 function onClick(event) {
425 console.debug(event.target);
426 if (event.isDefaultPrevented()) {
427 return;
428 }
429 event.preventDefault();
430 event.stopPropagation();
431 // notifyFocusedHandler();
432 }
433
434 function onWrapperFocus(event) {
435 console.debug(event.target);
436 //notifyFocusedHandler();
437 }
438
439 return (
440 <div className="EditDescriptorModelProperties -is-tree-view" onClick={onClick} onFocus={onWrapperFocus}>
441 <ModelBreadcrumb path={breadcrumb} />
442 {children}
443 </div>
444 );
445 };
446