3 * Copyright 2016 RIFT.IO Inc
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
19 * Created by onvelocity on 1/18/16.
21 * This class generates the form fields used to edit the CONFD JSON model.
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'
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'
50 import '../styles/EditDescriptorModelProperties.scss'
53 function resolveReactKey(value
) {
54 const keyPath
= ['uiState', 'fieldKey'];
55 if (!_has(value
, keyPath
)) {
56 _set(value
, keyPath
, _uniqueId());
58 return _get(value
, keyPath
);
61 function getTipForProperty(property
) {
62 return property
.name
=== 'constituent-vnfd' ? "Drag a VNFD from the Catalog to add more." : null
65 function selectModel(container
, model
, property
) {
66 ComposerAppActions
.selectModel(container
.findChildByUid(model
));
69 function removeListEntry(container
, property
, path
) {
70 DescriptorEditorActions
.removeListItem({ descriptor
: container
, property
, path
});
73 function createAndAddItemToPath(container
, property
, path
) {
74 DescriptorEditorActions
.addListItem({ descriptor
: container
, property
, path
});
77 function notifyPropertyFocused(container
, path
) {
78 container
.getRoot().uiState
.focusedPropertyPath
= path
.join('.');
79 console
.debug('property selected', path
.join('.'));
80 ComposerAppActions
.propertySelected([path
.join('.')]);
83 function setPropertyOpenState(container
, path
, property
, isOpen
) {
84 DescriptorEditorActions
.setOpenState({ descriptor
: container
, property
, path
, isOpen
});
87 function isDataProperty(property
) {
88 return property
.type
=== 'leaf' || property
.type
=== 'leaf_list' || property
.type
=== 'choice';
91 function checkIfValueEmpty(value
) {
92 if (value
=== null || typeof value
=== 'undefined') {
94 } else if (_isArray(value
) && !value
.length
) {
96 } else if (_isObject(value
)) {
97 const keys
= _keys(value
);
98 if (keys
.length
< 2) {
99 return !keys
.length
|| (keys
[0] === 'uiState')
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
;
111 function getPanelOpenedCondition(value
, path
) {
112 const showOpened
= container
.getUiState('opened', path
);
113 if (typeof showOpened
=== 'undefined') {
114 return (openPanelsWithData
&& !checkIfValueEmpty(value
)) ? true : !collapsePanelsByDefault
;
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
);
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
});
131 function onErrorHandler(message
) {
132 DescriptorEditorActions
.setError({ descriptor
: container
, path
, message
});
135 // create an onChange event handler for a select field for the specified field path
136 const onChangeHandler
= processFieldValueChange
.bind(null, pathToProperty
);
140 container
={container
}
145 showHelp
={showElementHelp
}
146 onChange
={onChangeHandler
}
147 onError
={onErrorHandler
}
149 errorMessage
={_get(container
.uiState
, ['error'].concat(path
))}
155 * buiid and return an array of components representing an editor for each property.
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
163 function buildComponentsForProperties(properties
, pathToProperties
, data
) {
164 return properties
.map((property
) => {
166 let propertyPath
= pathToProperties
.slice();
167 if (property
.type
!= 'choice') {
168 propertyPath
.push(property
.name
);
170 if (data
&& typeof data
=== 'object') {
171 value
= _get(data
, property
.name
);
175 result
= buildPropertyComponent(property
, propertyPath
, value
);
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
);
188 function determineSelectedChoice(model
) {
189 let choiceState
= utils
.resolvePath(container
.model
, fullChoiceStatePath
.join('.'));
191 return property
.properties
.find(c
=> c
.name
=== choiceState
.selected
);
193 const selectedCase
= property
.properties
.find(c
=>
194 c
.properties
&& c
.properties
.find(p
=> _has(model
, path
.concat([p
.name
])))
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
);
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
);
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
) {
225 if (previouslySelectedChoice
) {
226 choiceState
[previouslySelectedChoice
] = pullOutCaseModel(previouslySelectedChoice
);
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
];
235 Object
.assign(modelUpdate
, previous
);
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') {
242 obj
[property
.name
] = [null];
243 Object
.assign(modelUpdate
, obj
);
248 DescriptorEditorActions
.assignValue({ descriptor
: container
, path
, source
: modelUpdate
});
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
)}
260 <Choice key
={uniqueId
} id
={uniqueId
} onChange
={processChoiceChange
} readOnly
={readOnly
} showHelp
={showElementHelp
}
261 property
={property
} value
={selectedCase
? selectedCase
.name
: null}
269 function buildLeafList(property
, path
, value
, uniqueId
) {
270 if (!Array
.isArray(value
)) {
273 const children
= value
&& value
.map((v
, i
) => {
274 let itemPath
= path
.concat([i
]);
275 const field
= buildField(property
, itemPath
, v
, uniqueId
+ i
);
277 <ListItem key
={':' + i
} index
={i
} property
={property
} readOnly
={readOnly
} showHelp
={showElementHelp
}
278 showOpened
={true} removeItemHandler
={removeListEntry
.bind(null, container
, property
, itemPath
)} >
284 <List key
={uniqueId
} id
={uniqueId
} property
={property
} value
={value
} readOnly
={readOnly
} showHelp
={showElementHelp
}
285 showOpened
={true} addItemHandler
={createAndAddItemToPath
.bind(null, container
, property
, path
)}>
291 function buildList(property
, path
, value
, uniqueId
) {
292 if (value
&& !Array
.isArray(value
)) {
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);
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
);
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
)}>
314 const showOpened
= getPanelOpenedCondition(value
, path
);
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
)}>
324 function buildSimpleList(property
, path
, value
, uniqueId
) {
325 if (value
&& !Array
.isArray(value
)) {
328 const children
= value
&& value
.map((v
, i
) => {
329 let itemPath
= path
.concat([i
]);
331 <ListItemAsLink key
={':' + i
} property
={property
} value
={v
}
332 removeItemHandler
={removeListEntry
.bind(null, container
, property
, itemPath
)}
333 selectLinkHandler
={selectModel
.bind(null, container
, v
, property
)} />
336 const tip
= getTipForProperty(property
);
337 const showOpened
= getPanelOpenedCondition(value
, path
);
338 const changeOpenState
= setPropertyOpenState
.bind(null, container
, path
, property
, !showOpened
);
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
}>
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
);
354 <ContainerWrapper key
={uniqueId
} id
={uniqueId
} property
={property
} readOnly
={readOnly
}
355 showHelp
={showElementHelp
} summary
={checkIfValueEmpty(value
) ? null : '*'}
356 showOpened
={showOpened
} onChangeOpenState
={changeOpenState
}>
362 function buildPropertyComponent(property
, path
, value
) {
365 const isArray
= Property
.isArray(property
);
366 const isObject
= Property
.isObject(property
);
367 const title
= changeCase
.titleCase(property
.name
);
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
);
373 if (!property
.properties
&& isObject
) {
374 console
.debug('no properties', property
);
375 const uiState
= DescriptorModelMetaFactory
.getModelMetaForType(property
.name
) || {};
376 property
.properties
= uiState
.properties
;
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
)
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
);
394 <span key
={fieldId
} className
="warning">No Descriptor Meta
for {property
.name
}</span
>
400 if (!(DescriptorModelFactory
.isContainer(container
))) {
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
);
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
);
417 o
.listTwo
.push(property
);
420 }, { listOne
: [], listTwo
: [] });
421 properties
= twoLists
.listOne
.concat(twoLists
.listTwo
);
422 const children
= buildComponentsForProperties(properties
, [], container
.model
);
424 function onClick(event
) {
425 console
.debug(event
.target
);
426 if (event
.isDefaultPrevented()) {
429 event
.preventDefault();
430 event
.stopPropagation();
431 // notifyFocusedHandler();
434 function onWrapperFocus(event
) {
435 console
.debug(event
.target
);
436 //notifyFocusedHandler();
440 <div className
="EditDescriptorModelProperties -is-tree-view" onClick
={onClick
} onFocus
={onWrapperFocus
}>
441 <ModelBreadcrumb path
={breadcrumb
} />