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