Fix certain default values breaking VFC
[sdc.git] / catalog-ui / src / app / ng2 / pages / properties-assignment / constraints / constraints.component.ts
1 /*
2  * ============LICENSE_START=======================================================
3  *  Copyright (C) 2022 Nordix Foundation
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  *  Unless required by applicable law or agreed to in writing, software
11  *  distributed under the License is distributed on an "AS IS" BASIS,
12  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *  See the License for the specific language governing permissions and
14  *  limitations under the License.
15  *
16  *  SPDX-License-Identifier: Apache-2.0
17  *  ============LICENSE_END=========================================================
18  */
19
20 import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
21 import {
22   AbstractControl, FormArray,
23   FormBuilder,
24   FormControl,
25   FormGroup, ValidationErrors,
26   ValidatorFn,
27   Validators
28 } from '@angular/forms';
29 import { PROPERTY_DATA, PROPERTY_TYPES } from 'app/utils/constants';
30
31 @Component({
32   selector: 'app-constraints',
33   templateUrl: './constraints.component.html',
34   styleUrls: ['./constraints.component.less']
35 })
36 export class ConstraintsComponent implements OnInit {
37
38   @Input() propertyConstraints: any[];
39   @Input() propertyType: string;
40   @Input() isViewOnly: boolean = false;
41   @Output() onConstraintChange: EventEmitter<any> = new EventEmitter<any>();
42
43   constraintTypes: string[];
44   ConstraintTypesMapping = ConstraintTypesMapping;
45   valid: boolean = false;
46   constraintForm: FormGroup;
47   validationMessages;
48   init: boolean = true;
49
50   constructor(private formBuilder: FormBuilder) {}
51
52   get constraintsArray() {
53     return this.constraintForm.get('constraint') as FormArray;
54   }
55
56   get constraintValidators(): ValidatorFn {
57     switch (this.propertyType) {
58       case PROPERTY_TYPES.INTEGER:
59         console.warn('Add int validator');
60         return Validators.compose([
61           Validators.required,
62           intValidator()
63         ]);
64       case PROPERTY_TYPES.FLOAT:
65         console.warn('Add float validator');
66         return Validators.compose([
67           Validators.required,
68           floatValidator()
69         ]);
70       default:
71         console.warn('Only required validator');
72         return Validators.compose([
73           Validators.required
74         ]);
75     }
76 }
77
78   public constraintValuesArray(index: number): FormArray {
79     return this.constraintsArray.at(index).get('value') as FormArray;
80   }
81
82   ngOnInit() {
83     console.groupEnd();
84     this.constraintTypes = Object.keys(ConstraintTypes).map((key) => ConstraintTypes[key]);
85
86     // This is only used by the spec test
87     if (!this.constraintForm) {
88       this.constraintForm = this.formBuilder.group({
89         constraint: this.formBuilder.array([])
90       });
91     }
92
93     this.validationMessages = {
94       constraint: [
95         { type: 'required', message: 'Constraint value is required'},
96         { type: 'invalidInt', message: 'Constraint value is not a valid integer'},
97         { type: 'invalidFloat', message: 'Constraint value is not a valid floating point value'},
98         { type: 'invalidString', message: 'String contains invalid characters'}
99       ],
100       type : [
101         { type: 'required', message: 'Constraint type is required'}
102       ]
103     };
104
105     this.init = false;
106   }
107
108   ngOnChanges(changes): void {
109     console.groupEnd();
110
111     // Changes fires before init so form has to be initialised here
112     if (this.init) {
113       this.constraintForm = this.formBuilder.group({
114         constraint: this.formBuilder.array([])
115       });
116
117       if (changes.propertyConstraints && changes.propertyConstraints.currentValue) {
118         changes.propertyConstraints.currentValue.forEach((constraint: any) => {
119           const prop = this.getConstraintFromPropertyBEModel(constraint);
120           console.log('constraint from BE model', prop);
121           this.constraintsArray.push(prop);
122         });
123       }
124     }
125
126     if (changes.propertyType) {
127       if (!this.init) {
128         // Reset constraints on property type change
129         console.warn('Property type changed. Resetting constraints');
130         this.constraintForm = this.formBuilder.group({
131           constraint: this.formBuilder.array([])
132         });
133       }
134
135       if (!this.propertyType || changes.propertyType.currentValue == this.propertyType) {
136         this.propertyType = changes.propertyType.currentValue;
137       } else {
138         this.propertyType = changes.propertyType;
139         this.emitOnConstraintChange();
140       }
141
142       this.constraintsArray.controls.forEach((control: AbstractControl) => {
143         control.get('value').setValidators(this.constraintValidators);
144       });
145     }
146
147     console.log('constraints', this.constraintsArray);
148   }
149
150   removeFromList(constraintIndex: number, valueIndex: number) {
151     this.constraintsArray.at(constraintIndex).get('value').value.splice(valueIndex, 1);
152     this.emitOnConstraintChange();
153   }
154
155   addToList(constraintIndex: number) {
156     const newConstraint = new FormControl('', this.constraintValidators);
157
158     this.constraintValuesArray(constraintIndex).push(newConstraint);
159     console.log('constraintsArray', this.constraintsArray);
160     console.log('constraintValuesArray', this.constraintValuesArray(constraintIndex));
161     this.emitOnConstraintChange();
162   }
163
164   onChangeConstraintType(constraintIndex: number, newType: ConstraintTypes) {
165     if ((newType == ConstraintTypes.valid_values)) {
166       const newConstraint = this.formBuilder.group({
167         type: new FormControl({
168           value: newType,
169           disabled: this.isViewOnly
170         }, Validators.required),
171         value: this.formBuilder.array([])});
172
173       this.constraintsArray.removeAt(constraintIndex);
174       this.constraintsArray.push(newConstraint);
175     } else if (newType == ConstraintTypes.in_range) {
176       const newConstraint = this.formBuilder.group({
177         type: new FormControl({
178           value: newType,
179           disabled: this.isViewOnly
180         }, Validators.required),
181         value: this.formBuilder.array([])});
182
183       const valRef = newConstraint.get('value') as FormArray;
184       valRef.push(new FormControl('', this.constraintValidators));
185       valRef.push(new FormControl('', this.constraintValidators));
186
187       this.constraintsArray.removeAt(constraintIndex);
188       this.constraintsArray.push(newConstraint);
189     } else {
190       this.constraintsArray.at(constraintIndex).value.type = newType;
191     }
192     this.emitOnConstraintChange();
193   }
194
195   onChangeConstraintValue(constraintIndex: number, newValue: any) {
196     this.constraintsArray.at(constraintIndex).get('value').setValue(newValue);
197     this.emitOnConstraintChange();
198   }
199
200   onChangeConstrainValueIndex(constraintIndex: number, newValue: any, valueIndex: number) {
201     this.constraintValuesArray(constraintIndex).controls[valueIndex].setValue(newValue);
202     this.emitOnConstraintChange();
203   }
204
205   removeConstraint(constraintIndex: number) {
206     this.constraintsArray.removeAt(constraintIndex);
207     this.emitOnConstraintChange();
208 }
209
210   addConstraint() {
211     const newConstraint = this.formBuilder.group({
212       type: new FormControl({
213         value: ConstraintTypes.null,
214         disabled: this.isViewOnly
215       }, Validators.required),
216       value: new FormControl({
217         value: '',
218         disabled: this.isViewOnly
219       }, this.constraintValidators)
220     });
221     this.constraintsArray.push(newConstraint);
222     this.valid = false;
223     this.emitOnConstraintChange();
224   }
225
226   getInRangeValue(constraintIndex: number, valueIndex: number): string {
227     const value = this.constraintsArray.at(constraintIndex).get('value').value;
228
229     if (!value || !value[valueIndex]) {
230       return '';
231     }
232
233     return value[valueIndex];
234   }
235
236   disableConstraint(optionConstraintType: ConstraintTypes): boolean {
237     const invalid = this.notAllowedConstraint(optionConstraintType);
238     return invalid ? invalid : this.getConstraintTypeIfPresent(optionConstraintType) ? true : false;
239   }
240
241   notAllowedConstraint(optionConstraintType: ConstraintTypes): boolean {
242     switch (optionConstraintType) {
243       case ConstraintTypes.less_or_equal:
244       case ConstraintTypes.less_than:
245       case ConstraintTypes.greater_or_equal:
246       case ConstraintTypes.greater_than:
247       case ConstraintTypes.in_range:
248         if (this.isComparable(this.propertyType)) {
249           return false;
250         }
251         break;
252       case ConstraintTypes.length:
253       case ConstraintTypes.max_length:
254       case ConstraintTypes.min_length:
255         if (this.propertyType == PROPERTY_TYPES.STRING || this.propertyType == PROPERTY_TYPES.MAP || this.propertyType == PROPERTY_TYPES.LIST) {
256           return false;
257         }
258         break;
259       case ConstraintTypes.pattern:
260         if (this.propertyType == PROPERTY_TYPES.STRING) {
261           return false;
262         }
263         break;
264       case ConstraintTypes.valid_values:
265       case ConstraintTypes.equal:
266         return false;
267     }
268     return true;
269   }
270
271   getConstraintTypeIfPresent(constraintType: ConstraintTypes): AbstractControl {
272     return this.constraintsArray.controls.find((control: AbstractControl) => {
273       const type = control.get('type').value;
274       return type == constraintType;
275     });
276   }
277
278   trackByFn(index) {
279     return index;
280   }
281
282   isComparable(propType: string): boolean {
283     if (PROPERTY_DATA.COMPARABLE_TYPES.indexOf(propType) >= 0) {
284       return true;
285     }
286     return false;
287   }
288
289   private getConstraintFromPropertyBEModel(constraint: any): AbstractControl {
290     console.log('be model constraints', constraint);
291     let constraintType: ConstraintTypes;
292     let constraintValue: any;
293     if (!constraint) {
294       constraintType = ConstraintTypes.null;
295       constraintValue = '';
296     } else if (constraint.hasOwnProperty(ConstraintTypes.valid_values)) {
297       constraintType = ConstraintTypes.valid_values;
298     } else if (constraint.hasOwnProperty(ConstraintTypes.equal)) {
299       constraintType = ConstraintTypes.equal;
300       constraintValue = constraint.equal;
301     } else if (constraint.hasOwnProperty(ConstraintTypes.greater_than)) {
302       constraintType = ConstraintTypes.greater_than;
303       constraintValue = constraint.greaterThan;
304     } else if (constraint.hasOwnProperty(ConstraintTypes.greater_or_equal)) {
305       constraintType = ConstraintTypes.greater_or_equal;
306       constraintValue = constraint.greaterOrEqual;
307     } else if (constraint.hasOwnProperty(ConstraintTypes.less_than)) {
308       constraintType = ConstraintTypes.less_than;
309       constraintValue = constraint.lessThan;
310     } else if (constraint.hasOwnProperty(ConstraintTypes.less_or_equal)) {
311       constraintType = ConstraintTypes.less_or_equal;
312       constraintValue = constraint.lessOrEqual;
313     } else if (constraint.hasOwnProperty(ConstraintTypes.in_range)) {
314       constraintType = ConstraintTypes.in_range;
315       constraintValue = new Array(constraint.inRange[0], constraint.inRange[1]);
316     } else if (constraint.rangeMaxValue || constraint.rangeMinValue) {
317       constraintType = ConstraintTypes.in_range;
318       constraintValue = new Array(constraint.rangeMinValue, constraint.rangeMaxValue);
319     } else if (constraint.hasOwnProperty(ConstraintTypes.length)) {
320       constraintType = ConstraintTypes.length;
321       constraintValue = constraint.length;
322     } else if (constraint.hasOwnProperty(ConstraintTypes.min_length)) {
323       constraintType = ConstraintTypes.min_length;
324       constraintValue = constraint.minLength;
325     } else if (constraint.hasOwnProperty(ConstraintTypes.max_length)) {
326       constraintType = ConstraintTypes.max_length;
327       constraintValue = constraint.maxLength;
328     } else if (constraint.hasOwnProperty(ConstraintTypes.pattern)) {
329       constraintType = ConstraintTypes.pattern;
330       constraintValue = constraint.pattern;
331     }
332
333     if (!constraint.hasOwnProperty(ConstraintTypes.valid_values) && !constraint.hasOwnProperty(ConstraintTypes.in_range)) {
334       return this.formBuilder.group({
335         type: new FormControl({
336           value: constraintType,
337           disabled: this.isViewOnly
338         }, Validators.required),
339         value: new FormControl({
340           value: constraintValue,
341           disabled: this.isViewOnly
342         }, this.constraintValidators)
343       });
344     } else {
345       const newForm = this.formBuilder.group({
346         type: new FormControl({
347           value: constraintType,
348           disabled: this.isViewOnly
349         }, Validators.required),
350         value: this.formBuilder.array([])
351       });
352
353       const valRef = newForm.get('value') as FormArray;
354       if (constraint.hasOwnProperty(ConstraintTypes.valid_values)) {
355         constraint.validValues.forEach((val) => {
356           valRef.push(new FormControl({value: val, disabled: this.isViewOnly}, this.constraintValidators));
357         });
358       } else {
359         constraint.inRange.forEach((val) => {
360           valRef.push(new FormControl({value: val, disabled: this.isViewOnly}, this.constraintValidators));
361         });
362       }
363
364       console.log('new form', newForm);
365       return newForm;
366     }
367   }
368
369   private getConstraintsFormat(): any[] {
370     const constraintArray = new Array();
371     this.constraintsArray.controls.forEach((control: AbstractControl) => {
372       const type = control.get('type').value;
373       let constraint: Constraint;
374
375       if (type != ConstraintTypes.valid_values && type != ConstraintTypes.in_range) {
376         constraint = {
377           type,
378           value: control.get('value').value
379         };
380       } else {
381         const valArray = [];
382
383         control.get('value').value.forEach((val) => {
384           valArray.push(val);
385         });
386
387         constraint = {
388           type,
389           value: valArray
390         };
391       }
392
393       console.log('New constraint object', constraint);
394       constraintArray.push(this.getConstraintFormat(constraint));
395     });
396     return constraintArray;
397   }
398
399   private getConstraintFormat(constraint: Constraint): any {
400     switch (constraint.type) {
401       case ConstraintTypes.equal:
402         return {
403           [ConstraintTypes.equal]: constraint.value
404         };
405       case ConstraintTypes.less_or_equal:
406         return {
407           [ConstraintTypes.less_or_equal]: constraint.value
408         };
409       case ConstraintTypes.less_than:
410         return {
411           [ConstraintTypes.less_than]: constraint.value
412         };
413       case ConstraintTypes.greater_or_equal:
414         return {
415           [ConstraintTypes.greater_or_equal]: constraint.value
416         };
417       case ConstraintTypes.greater_than:
418         return {
419           [ConstraintTypes.greater_than]: constraint.value
420         };
421       case ConstraintTypes.in_range:
422         return {
423           [ConstraintTypes.in_range]: constraint.value
424         };
425       case ConstraintTypes.length:
426         return {
427           [ConstraintTypes.length]: constraint.value
428         };
429       case ConstraintTypes.max_length:
430         return {
431           [ConstraintTypes.max_length]: constraint.value
432         };
433       case ConstraintTypes.min_length:
434         return {
435           [ConstraintTypes.min_length]: constraint.value
436         };
437       case ConstraintTypes.pattern:
438         return {
439           [ConstraintTypes.pattern]: constraint.value
440         };
441       case ConstraintTypes.valid_values:
442         return {
443           [ConstraintTypes.valid_values]: constraint.value
444         };
445       default:
446         return;
447     }
448   }
449
450   private validateConstraints(): void {
451     this.valid = this.constraintsArray.controls.every((control: AbstractControl) => {
452       const value = control.get('value').value;
453       const type = control.get('type').value;
454       control.updateValueAndValidity();
455
456       if (Array.isArray(value)) {
457         return !(value.length == 0 || this.doesArrayContaintEmptyValues(value));
458       }
459       if (type == ConstraintTypes.pattern) {
460         try {
461           new RegExp(value);
462           this.valid = true;
463         } catch (e) {
464           this.valid = false;
465         }
466       } else {
467         this.valid = this.constraintForm.valid;
468       }
469
470       return value && type != ConstraintTypes.null;
471     });
472   }
473
474   private doesArrayContaintEmptyValues(arr) {
475     for (const element of arr) {
476       if (element === '') { return true; }
477     }
478     return false;
479   }
480
481   private emitOnConstraintChange(): void {
482     console.log('constraints', this.constraintsArray);
483
484     this.validateConstraints();
485     const newConstraints = this.getConstraintsFormat();
486
487     this.valid = this.constraintForm.valid;
488     console.log('emitOnConstraintChange.valid', this.valid);
489
490     this.onConstraintChange.emit({
491       constraints: newConstraints,
492       valid: this.valid
493     });
494   }
495
496 }
497
498 export enum ConstraintTypes {
499   null = '',
500   equal= 'equal',
501   greater_than = 'greaterThan',
502   greater_or_equal = 'greaterOrEqual',
503   less_than = 'lessThan',
504   less_or_equal = 'lessOrEqual',
505   in_range = 'inRange',
506   valid_values = 'validValues',
507   length = 'length',
508   min_length = 'minLength',
509   max_length = 'maxLength',
510   pattern = 'pattern'
511 }
512
513 export const ConstraintTypesMapping = {
514   [ConstraintTypes.equal]: 'equal',
515   [ConstraintTypes.greater_than]: 'greater_than',
516   [ConstraintTypes.greater_or_equal]: 'greater_or_equal',
517   [ConstraintTypes.less_than]: 'less_than',
518   [ConstraintTypes.less_or_equal]: 'less_or_equal',
519   [ConstraintTypes.in_range]: 'in_range',
520   [ConstraintTypes.valid_values]: 'valid_values',
521   [ConstraintTypes.length]: 'length',
522   [ConstraintTypes.min_length]: 'min_length',
523   [ConstraintTypes.max_length]: 'max_length',
524   [ConstraintTypes.pattern]: 'pattern'
525 };
526
527 export interface Constraint {
528   type: ConstraintTypes;
529   value: any;
530 }
531
532 export function intValidator(): ValidatorFn {
533   const intRegex = /^[-+]?\d+$/;
534   return (control: AbstractControl): ValidationErrors | null => {
535     if (control.value && !intRegex.test(control.value)) {
536       return {invalidInt: true};
537     }
538
539     return null;
540   };
541 }
542
543 export function floatValidator(): ValidatorFn {
544   const floatRegex = /^[-+]?\d+(\.\d+)?$/;
545
546   return (control: AbstractControl): ValidationErrors | null => {
547     if (control.value && !floatRegex.test(control.value)) {
548       return {invalidFloat: true};
549     }
550
551     return null;
552   };
553 }