2 * ============LICENSE_START=======================================================
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
13 * http://www.apache.org/licenses/LICENSE-2.0
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=========================================================
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';
29 export class PropertyFEModel extends PropertyBEModel {
31 expandedChildPropertyId: string;
32 flattenedChildren: Array<DerivedFEProperty>;
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;
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;
47 constructor(property: PropertyBEModel){
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;
59 this.updateValueObjOrig();
60 this.resetValueObjValidation();
61 this.origName = this.name;
65 public updateValueObj(valueObj:any, isValid:boolean) {
66 this.valueObj = PropertyFEModel.cleanValueObj(valueObj);
67 this.valueObjValidation = this.valueObjIsValid = isValid;
68 this.valueObjIsChanged = this.hasValueObjChanged();
71 public updateValueObjOrig() {
72 this.valueObjOrig = _.cloneDeep(this.valueObj);
73 this.valueObjIsChanged = false;
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]));
83 return Boolean(valueObjValidation);
86 public resetValueObjValidation() {
87 if (this.derivedDataType === DerivedPropertyType.SIMPLE) {
88 this.valueObjValidation = null;
89 } else if (this.derivedDataType === DerivedPropertyType.LIST) {
90 this.valueObjValidation = [];
92 this.valueObjValidation = {};
94 this.valueObjIsValid = true;
97 public getJSONValue = (): string => {
98 return PropertyFEModel.stringifyValueObj(this.valueObj, this.schema.property.type, this.derivedDataType);
101 public getValueObj = (): any => {
102 return PropertyFEModel.parseValueObj(this.value, this.type, this.derivedDataType, this.isToscaFunction(), this.defaultValue);
105 public setNonDeclared = (childPath?: string): void => {
106 if (!childPath) { //un-declaring a child prop
107 this.isDeclared = false;
109 let childProp: DerivedFEProperty = this.flattenedChildren.find(child => child.propertiesName == childPath);
110 childProp.isDeclared = false;
114 public setAsDeclared = (childNameToDeclare?:string): void => {
115 if (!childNameToDeclare) { //declaring a child prop
116 this.isSelected = false;
117 this.isDeclared = true;
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;
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;
131 this.expandedChildPropertyId = this.name;
135 public getIndexOfChild = (childPropName: string): number => {
136 return this.flattenedChildren.findIndex(prop => prop.propertiesName.indexOf(childPropName) === 0);
139 public getCountOfChildren = (childPropName: string):number => {
140 let matchingChildren:Array<DerivedFEProperty> = this.flattenedChildren.filter(prop => prop.propertiesName.indexOf(childPropName) === 0) || [];
141 return matchingChildren.length;
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)
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);
160 _.set(this.valueObj, childPropName, null);
163 _.set(this.valueObj, childPropName, childProp.valueObj);
165 if (childProp.valueObjIsChanged) {
166 _.set(this.valueObjValidation, childPropName, childProp.valueObjIsValid);
167 this.valueObjIsValid = childProp.valueObjIsValid && this.calculateValueObjIsValid();
168 this.valueObjIsChanged = true;
170 _.unset(this.valueObjValidation, childPropName);
171 this.valueObjIsValid = this.calculateValueObjIsValid();
172 this.valueObjIsChanged = this.hasValueObjChanged();
177 childPropMapKeyUpdated = (childProp: DerivedFEProperty, newMapKey: string, forceValidate: boolean = false) => {
178 if (!childProp.isChildOfListOrMap || childProp.derivedDataType !== DerivedPropertyType.MAP) {
181 const childParentNames = this.getParentNamesArray(childProp.parentName);
182 const oldActualMapKey = childProp.getActualMapKey();
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.';
196 childProp.mapKeyError = null;
198 const newActualMapKey = childProp.getActualMapKey();
199 const newMapKeyIsValid = !childProp.mapKeyError;
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;
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));
215 // remove map key from valueObj and valueObjValidation
216 _.unset(this.valueObj, oldChildPropNames);
217 _.unset(this.valueObjValidation, oldChildPropNames);
219 // force validate after map key change
220 forceValidate = true;
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);
229 _.unset(this.valueObjValidation, childMapKeyNames);
232 this.valueObjIsValid = newMapKeyIsValid && this.calculateValueObjIsValid();
233 this.valueObjIsChanged = this.hasValueObjChanged();
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
242 let parentProp: DerivedFEProperty = this.flattenedChildren.find(prop => prop.propertiesName === parentPropName);
243 let nameToInsert: string = parentProp.name;
245 if (parentProp.isChildOfListOrMap) {
246 if (!noHashKeys && parentProp.derivedDataType == DerivedPropertyType.MAP && !parentProp.mapInlist) {
247 nameToInsert = parentProp.getActualMapKey();
249 let siblingProps = this.flattenedChildren.filter(prop => prop.parentName == parentProp.parentName).map(prop => prop.propertiesName);
250 nameToInsert = siblingProps.indexOf(parentProp.propertiesName).toString();
254 parentNames.splice(0, 0, nameToInsert); //add prop name to array
255 return this.getParentNamesArray(parentProp.parentName, parentNames, noHashKeys); //continue recursing
258 public hasValueObjChanged() {
259 return !_.isEqual(this.valueObj, this.valueObjOrig);
262 static stringifyValueObj(valueObj: any, propertyType: PROPERTY_TYPES, propertyDerivedType: DerivedPropertyType): string {
263 // if valueObj is null, return null
264 if (valueObj === null || valueObj === undefined) {
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) {
272 return JSON.stringify(valueObj.map(item => (typeof item == 'string') ? JSON.parse(item) : item));
276 // if type is anything but string, then stringify valueObj
277 if ((typeof valueObj) !== 'string') {
278 return JSON.stringify(valueObj);
281 // return trimmed string value
282 return valueObj.trim();
285 static parseValueObj(value: string, propertyType: PROPERTY_TYPES, propertyDerivedType: DerivedPropertyType, isToscaFunction: boolean,
286 defaultValue?: string): any {
287 if (isToscaFunction) {
288 return jsYaml.load(value);
290 if (propertyDerivedType === DerivedPropertyType.SIMPLE) {
291 const valueObj = value || defaultValue || null; // use null for empty value object
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
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.
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.
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 === '') {
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) {
324 // if unsetEmpty flag is true and valueObj is empty
325 if (unsetEmpty && !Object.keys(valueObj).length) {