Allow multiple entry for map/list when tosca function is selected
[sdc.git] / catalog-ui / src / app / models / properties-inputs / property-fe-model.ts
1 /*-
2  * ============LICENSE_START=======================================================
3  * SDC
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Modifications Copyright (C) 2020 Nokia
8  * ================================================================================
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  * 
13  *      http://www.apache.org/licenses/LICENSE-2.0
14  * 
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  * ============LICENSE_END=========================================================
21  */
22
23 import * as _ from "lodash";
24 import {PROPERTY_DATA, PROPERTY_TYPES} from 'app/utils';
25 import {DerivedFEProperty, DerivedPropertyType, PropertyBEModel} from 'app/models';
26 import * as jsYaml from 'js-yaml';
27
28
29 export class PropertyFEModel extends PropertyBEModel {
30
31     expandedChildPropertyId: string;
32     flattenedChildren:  Array<DerivedFEProperty>;
33     isDeclared: boolean;
34     isDisabled: boolean;
35     isSelected: boolean;
36     isSimpleType: boolean; //for convenience only - we can really just check if derivedDataType == derivedPropertyTypes.SIMPLE to know if the prop is simple
37     propertiesName: string;
38     uniqueId: string;
39     valueObj: any; //this is the only value we relate to in the html templates
40     valueObjValidation: any;
41     valueObjIsValid: boolean;
42     valueObjOrig: any; //this is valueObj representation as saved in server
43     valueObjIsChanged: boolean;
44     derivedDataType: DerivedPropertyType;
45     origName: string;
46
47     constructor(property: PropertyBEModel){
48         super(property);
49         if (!property) {
50             return;
51         }
52         this.value = property.value ? property.value : property.defaultValue;//In FE if a property doesn't have value - display the default value
53         this.isSimpleType = PROPERTY_DATA.SIMPLE_TYPES.indexOf(this.type) > -1;
54         this.setNonDeclared();
55         this.derivedDataType = this.getDerivedPropertyType();
56         this.flattenedChildren = [];
57         this.propertiesName = this.name;
58         this.valueObj = null;
59         this.updateValueObjOrig();
60         this.resetValueObjValidation();
61         this.origName = this.name;
62     }
63
64
65     public updateValueObj(valueObj:any, isValid:boolean) {
66         this.valueObj = PropertyFEModel.cleanValueObj(valueObj);
67         this.valueObjValidation = this.valueObjIsValid = isValid;
68         this.valueObjIsChanged = this.hasValueObjChanged();
69     }
70
71     public updateValueObjOrig() {
72         this.valueObjOrig = _.cloneDeep(this.valueObj);
73         this.valueObjIsChanged = false;
74     }
75
76     public calculateValueObjIsValid(valueObjValidation?: any) {
77         valueObjValidation = (valueObjValidation !== undefined) ? valueObjValidation : this.valueObjValidation;
78         if (valueObjValidation instanceof Array) {
79             return valueObjValidation.every((v) => this.calculateValueObjIsValid(v));
80         } else if (valueObjValidation instanceof Object) {
81             return Object.keys(valueObjValidation).every((k) => this.calculateValueObjIsValid(valueObjValidation[k]));
82         }
83         return Boolean(valueObjValidation);
84     }
85
86     public resetValueObjValidation() {
87         if (this.derivedDataType === DerivedPropertyType.SIMPLE) {
88             this.valueObjValidation = null;
89         } else if (this.derivedDataType === DerivedPropertyType.LIST) {
90             this.valueObjValidation = [];
91         } else {
92             this.valueObjValidation = {};
93         }
94         this.valueObjIsValid = true;
95     }
96
97     public getJSONValue = (): string => {
98         return PropertyFEModel.stringifyValueObj(this.valueObj, this.schema.property.type, this.derivedDataType);
99     }
100
101     public getValueObj = (): any => {
102         return PropertyFEModel.parseValueObj(this.value, this.type, this.derivedDataType, this.isToscaFunction(), this.defaultValue);
103     }
104
105     public setNonDeclared = (childPath?: string): void => {
106         if (!childPath) { //un-declaring a child prop
107             this.isDeclared = false;
108         } else {
109             let childProp: DerivedFEProperty = this.flattenedChildren.find(child => child.propertiesName == childPath);
110             childProp.isDeclared = false;
111         }
112     }
113
114     public setAsDeclared = (childNameToDeclare?:string): void => {
115         if (!childNameToDeclare) { //declaring a child prop
116             this.isSelected = false;
117             this.isDeclared = true;
118         } else {
119             let childProp: DerivedFEProperty = this.flattenedChildren.find(child => child.propertiesName == childNameToDeclare);
120             if (!childProp) { console.log("ERROR: Unabled to find child: " + childNameToDeclare, this); return; }
121             childProp.isSelected = false;
122             childProp.isDeclared = true;
123         }
124     }
125
126     //For expand-collapse functionality - used within HTML template
127     public updateExpandedChildPropertyId = (childPropertyId: string): void => {
128         if (childPropertyId.lastIndexOf('#') > -1) {
129             this.expandedChildPropertyId = (this.expandedChildPropertyId == childPropertyId) ? (childPropertyId.substring(0, childPropertyId.lastIndexOf('#'))) : childPropertyId;
130         } else {
131             this.expandedChildPropertyId = this.name;
132         }
133     }
134
135     public getIndexOfChild = (childPropName: string): number => {
136         return this.flattenedChildren.findIndex(prop => prop.propertiesName.indexOf(childPropName) === 0);
137     }
138
139     public getCountOfChildren = (childPropName: string):number => {
140         let matchingChildren:Array<DerivedFEProperty> = this.flattenedChildren.filter(prop => prop.propertiesName.indexOf(childPropName) === 0) || [];
141         return matchingChildren.length;
142     }
143
144     // public getListIndexOfChild = (childPropName: string): number => { //gets list of siblings and then the index within that list
145     //     this.flattenedChildren.filter(prop => prop.parentName == item.parentName).map(prop => prop.propertiesName).indexOf(item.propertiesName)
146     // }
147
148     /* Updates parent valueObj when a child prop's value has changed */
149     public childPropUpdated = (childProp: DerivedFEProperty): void => {
150         let parentNames = this.getParentNamesArray(childProp.propertiesName, []);
151         if (parentNames.length) {
152             const childPropName = parentNames.join('.');
153             // unset value only if is null and valid, and not in a list
154             if (childProp.valueObj === null && childProp.valueObjIsValid) {
155                 const parentChildProp = this.flattenedChildren.find((ch) => ch.propertiesName === childProp.parentName) || this;
156                 if (parentChildProp.derivedDataType !== DerivedPropertyType.LIST) {
157                     _.unset(this.valueObj, childPropName);
158                     this.valueObj = PropertyFEModel.cleanValueObj(this.valueObj);
159                 } else {
160                     _.set(this.valueObj, childPropName, null);
161                 }
162             } else {
163                 _.set(this.valueObj, childPropName, childProp.valueObj);        
164             }
165             if (childProp.valueObjIsChanged) {
166                 _.set(this.valueObjValidation, childPropName, childProp.valueObjIsValid);
167                 this.valueObjIsValid = childProp.valueObjIsValid && this.calculateValueObjIsValid();
168                 this.valueObjIsChanged = true;
169             } else {
170                 _.unset(this.valueObjValidation, childPropName);
171                 this.valueObjIsValid = this.calculateValueObjIsValid();
172                 this.valueObjIsChanged = this.hasValueObjChanged();
173             }
174         }
175     };
176
177     childPropMapKeyUpdated = (childProp: DerivedFEProperty, newMapKey: string, forceValidate: boolean = false) => {
178         if (!childProp.isChildOfListOrMap || childProp.derivedDataType !== DerivedPropertyType.MAP) {
179             return;
180         }
181         const childParentNames = this.getParentNamesArray(childProp.parentName);
182         const oldActualMapKey = childProp.getActualMapKey();
183
184         childProp.mapKey = newMapKey;
185         childProp.toscaPath[childProp.toscaPath.length - 1] = newMapKey;
186         if (childProp.mapKey === null) {  // null -> remove map key
187             childProp.mapKeyError = null;
188         } else if (!childProp.mapKey) {
189             childProp.mapKeyError = 'Key cannot be empty.';
190         } else if (this.flattenedChildren
191                 .filter((fch) => fch !== childProp && fch.parentName === childProp.parentName)  // filter sibling child props
192                 .map((fch) => fch.mapKey)
193                 .indexOf(childProp.mapKey) !== -1) {
194             childProp.mapKeyError = 'This key already exists.';
195         } else {
196             childProp.mapKeyError = null;
197         }
198         const newActualMapKey = childProp.getActualMapKey();
199         const newMapKeyIsValid = !childProp.mapKeyError;
200
201         // if mapKey was changed, then replace the old key with the new one
202         if (newActualMapKey !== oldActualMapKey) {
203             const oldChildPropNames = childParentNames.concat([oldActualMapKey]);
204             const newChildPropNames = (newActualMapKey) ? childParentNames.concat([newActualMapKey]) : null;
205
206             // add map key to valueObj and valueObjValidation
207             if (newChildPropNames) {
208                 const newChildVal = _.get(this.valueObj, oldChildPropNames);
209                 if (newChildVal !== undefined) {
210                     _.set(this.valueObj, newChildPropNames, newChildVal);
211                     _.set(this.valueObjValidation, newChildPropNames, _.get(this.valueObjValidation, oldChildPropNames, childProp.valueObjIsValid));
212                 }
213             }
214
215             // remove map key from valueObj and valueObjValidation
216             _.unset(this.valueObj, oldChildPropNames);
217             _.unset(this.valueObjValidation, oldChildPropNames);
218
219             // force validate after map key change
220             forceValidate = true;
221         }
222
223         if (forceValidate) {
224             // add custom entry for map key validation:
225             const childMapKeyNames = childParentNames.concat(`%%KEY:${childProp.name}%%`);
226             if (newActualMapKey) {
227                 _.set(this.valueObjValidation, childMapKeyNames, newMapKeyIsValid);
228             } else {
229                 _.unset(this.valueObjValidation, childMapKeyNames);
230             }
231
232             this.valueObjIsValid = newMapKeyIsValid && this.calculateValueObjIsValid();
233             this.valueObjIsChanged = this.hasValueObjChanged();
234         }
235     };
236
237     /* Returns array of individual parents for given prop path, with list/map UUIDs replaced with index/mapkey */
238     public getParentNamesArray = (parentPropName: string, parentNames?: Array<string>, noHashKeys:boolean = false): Array<string> => {
239         parentNames = parentNames || [];
240         if (parentPropName.indexOf("#") == -1) { return parentNames; } //finished recursing parents. return
241
242         let parentProp: DerivedFEProperty = this.flattenedChildren.find(prop => prop.propertiesName === parentPropName);
243         let nameToInsert: string = parentProp.name;
244
245         if (parentProp.isChildOfListOrMap) {
246             if (!noHashKeys && parentProp.derivedDataType == DerivedPropertyType.MAP && !parentProp.mapInlist) {
247                 nameToInsert = parentProp.getActualMapKey();
248             } else { //LIST
249                 let siblingProps = this.flattenedChildren.filter(prop => prop.parentName == parentProp.parentName).map(prop => prop.propertiesName);
250                 nameToInsert = siblingProps.indexOf(parentProp.propertiesName).toString();
251             }
252         }
253
254         parentNames.splice(0, 0, nameToInsert); //add prop name to array
255         return this.getParentNamesArray(parentProp.parentName, parentNames, noHashKeys); //continue recursing
256     }
257
258     public hasValueObjChanged() {
259         return !_.isEqual(this.valueObj, this.valueObjOrig);
260     }
261
262     static stringifyValueObj(valueObj: any, propertyType: PROPERTY_TYPES, propertyDerivedType: DerivedPropertyType): string {
263         // if valueObj is null, return null
264         if (valueObj === null || valueObj === undefined) {
265             return null;
266         }
267
268         //If type is JSON, need to try parsing it before we stringify it so that it appears property in TOSCA - change per Bracha due to AMDOCS
269         //TODO: handle this.derivedDataType == DerivedPropertyType.MAP
270         if (propertyDerivedType == DerivedPropertyType.LIST && propertyType == PROPERTY_TYPES.JSON) {
271             try {
272                 return JSON.stringify(valueObj.map(item => (typeof item == 'string') ? JSON.parse(item) : item));
273             } catch (e){}
274         }
275
276         // if type is anything but string, then stringify valueObj
277         if ((typeof valueObj) !== 'string') {
278             return JSON.stringify(valueObj);
279         }
280
281         // return trimmed string value
282         return valueObj.trim();
283     }
284
285     static parseValueObj(value: string, propertyType: PROPERTY_TYPES, propertyDerivedType: DerivedPropertyType, isToscaFunction: boolean,
286                          defaultValue?: string): any {
287         if (isToscaFunction) {
288             return jsYaml.load(value);
289         }
290         if (propertyDerivedType === DerivedPropertyType.SIMPLE) {
291             const valueObj = value || defaultValue || null;  // use null for empty value object
292             if (valueObj &&
293                 propertyType !== PROPERTY_TYPES.STRING &&
294                 propertyType !== PROPERTY_TYPES.TIMESTAMP &&
295                 propertyType !== PROPERTY_TYPES.JSON &&
296                 PROPERTY_DATA.SCALAR_TYPES.indexOf(<string>propertyType) == -1) {
297                 return JSON.parse(valueObj);  // the value object contains the real value ans not the value as string
298             }
299             return valueObj;
300         }
301         if (propertyDerivedType == DerivedPropertyType.LIST) {
302             return _.merge([], JSON.parse(defaultValue || '[]'), JSON.parse(value || '[]'));  // value object should be merged value and default value. Value takes higher precedence. Set value object to empty obj if undefined.
303         }
304
305         return _.merge({}, JSON.parse(defaultValue || '{}'), JSON.parse(value || '{}'));  // value object should be merged value and default value. Value takes higher precedence. Set value object to empty obj if undefined.
306     };
307
308     static cleanValueObj(valueObj: any, unsetEmpty?: boolean): any {
309         // By default - unsetEmpty undefined - will make valueObj cleaned (no null or empty objects, but array will keep null or empty objects).
310         if (valueObj === undefined || valueObj === null || valueObj === '') {
311             return null;
312         }
313         if (valueObj instanceof Array) {
314             const cleanArr = valueObj.map((v) => PropertyFEModel.cleanValueObj(v)).filter((v) => v !== null);
315             valueObj.splice(0, valueObj.length, ...cleanArr)
316         } else if (valueObj instanceof Object) {
317             Object.keys(valueObj).forEach((k) => {
318                 // clean each item in the valueObj (by default, unset empty objects)
319                 valueObj[k] = PropertyFEModel.cleanValueObj(valueObj[k], unsetEmpty !== undefined ? unsetEmpty : true);
320                 if (valueObj[k] === null) {
321                     delete valueObj[k];
322                 }
323             });
324             // if unsetEmpty flag is true and valueObj is empty
325             if (unsetEmpty && !Object.keys(valueObj).length) {
326                 return null;
327             }
328         }
329         return valueObj;
330     }
331 }