Add validation for int and float constraints
[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       ],
99       type : [
100         { type: 'required', message: 'Constraint type is required'}
101       ]
102     };
103
104     this.init = false;
105   }
106
107   ngOnChanges(changes): void {
108     console.groupEnd();
109
110     // Changes fires before init so form has to be initialised here
111     if (this.init) {
112       this.constraintForm = this.formBuilder.group({
113         constraint: this.formBuilder.array([])
114       });
115
116       if (changes.propertyConstraints && changes.propertyConstraints.currentValue) {
117         changes.propertyConstraints.currentValue.forEach((constraint: any) => {
118           const prop = this.getConstraintFromPropertyBEModel(constraint);
119           console.log('constraint from BE model', prop);
120           this.constraintsArray.push(prop);
121         });
122       }
123     }
124
125     if (changes.propertyType) {
126       if (!this.init) {
127         // Reset constraints on property type change
128         console.warn('Property type changed. Resetting constraints');
129         this.constraintForm = this.formBuilder.group({
130           constraint: this.formBuilder.array([])
131         });
132       }
133
134       if (!this.propertyType || changes.propertyType.currentValue == this.propertyType) {
135         this.propertyType = changes.propertyType.currentValue;
136       } else {
137         this.propertyType = changes.propertyType;
138         this.emitOnConstraintChange();
139       }
140
141       this.constraintsArray.controls.forEach((control: AbstractControl) => {
142         control.get('value').setValidators(this.constraintValidators);
143       });
144     }
145
146     console.log('constraints', this.constraintsArray);
147   }
148
149   removeFromList(constraintIndex: number, valueIndex: number) {
150     this.constraintsArray.at(constraintIndex).get('value').value.splice(valueIndex, 1);
151     this.emitOnConstraintChange();
152   }
153
154   addToList(constraintIndex: number) {
155     const newConstraint = new FormControl('', this.constraintValidators);
156
157     this.constraintValuesArray(constraintIndex).push(newConstraint);
158     console.log('constraintsArray', this.constraintsArray);
159     console.log('constraintValuesArray', this.constraintValuesArray(constraintIndex));
160     this.emitOnConstraintChange();
161   }
162
163   onChangeConstraintType(constraintIndex: number, newType: ConstraintTypes) {
164     if ((newType == ConstraintTypes.valid_values)) {
165       const newConstraint = this.formBuilder.group({
166         type: new FormControl({
167           value: newType,
168           disabled: this.isViewOnly
169         }, Validators.required),
170         value: this.formBuilder.array([])});
171
172       this.constraintsArray.removeAt(constraintIndex);
173       this.constraintsArray.push(newConstraint);
174     } else if (newType == ConstraintTypes.in_range) {
175       const newConstraint = this.formBuilder.group({
176         type: new FormControl({
177           value: newType,
178           disabled: this.isViewOnly
179         }, Validators.required),
180         value: this.formBuilder.array([])});
181
182       const valRef = newConstraint.get('value') as FormArray;
183       valRef.push(new FormControl('', this.constraintValidators));
184       valRef.push(new FormControl('', this.constraintValidators));
185
186       this.constraintsArray.removeAt(constraintIndex);
187       this.constraintsArray.push(newConstraint);
188     } else {
189       this.constraintsArray.at(constraintIndex).value.type = newType;
190     }
191     this.emitOnConstraintChange();
192   }
193
194   onChangeConstraintValue(constraintIndex: number, newValue: any) {
195     this.constraintsArray.at(constraintIndex).get('value').setValue(newValue);
196     this.emitOnConstraintChange();
197   }
198
199   onChangeConstrainValueIndex(constraintIndex: number, newValue: any, valueIndex: number) {
200     this.constraintValuesArray(constraintIndex).controls[valueIndex].setValue(newValue);
201     this.emitOnConstraintChange();
202   }
203
204   removeConstraint(constraintIndex: number) {
205     this.constraintsArray.removeAt(constraintIndex);
206     this.emitOnConstraintChange();
207 }
208
209   addConstraint() {
210     const newConstraint = this.formBuilder.group({
211       type: new FormControl({
212         value: ConstraintTypes.null,
213         disabled: this.isViewOnly
214       }, Validators.required),
215       value: new FormControl({
216         value: '',
217         disabled: this.isViewOnly
218       }, this.constraintValidators)
219     });
220     this.constraintsArray.push(newConstraint);
221     this.valid = false;
222     this.emitOnConstraintChange();
223   }
224
225   getInRangeValue(constraintIndex: number, valueIndex: number): string {
226     const value = this.constraintsArray.at(constraintIndex).get('value').value;
227
228     if (!value || !value[valueIndex]) {
229       return '';
230     }
231
232     return value[valueIndex];
233   }
234
235   disableConstraint(optionConstraintType: ConstraintTypes): boolean {
236     const invalid = this.notAllowedConstraint(optionConstraintType);
237     return invalid ? invalid : this.getConstraintTypeIfPresent(optionConstraintType) ? true : false;
238   }
239
240   notAllowedConstraint(optionConstraintType: ConstraintTypes): boolean {
241     switch (optionConstraintType) {
242       case ConstraintTypes.less_or_equal:
243       case ConstraintTypes.less_than:
244       case ConstraintTypes.greater_or_equal:
245       case ConstraintTypes.greater_than:
246       case ConstraintTypes.in_range:
247         if (this.isComparable(this.propertyType)) {
248           return false;
249         }
250         break;
251       case ConstraintTypes.length:
252       case ConstraintTypes.max_length:
253       case ConstraintTypes.min_length:
254         if (this.propertyType == PROPERTY_TYPES.STRING || this.propertyType == PROPERTY_TYPES.MAP || this.propertyType == PROPERTY_TYPES.LIST) {
255           return false;
256         }
257         break;
258       case ConstraintTypes.pattern:
259         if (this.propertyType == PROPERTY_TYPES.STRING) {
260           return false;
261         }
262         break;
263       case ConstraintTypes.valid_values:
264       case ConstraintTypes.equal:
265         return false;
266     }
267     return true;
268   }
269
270   getConstraintTypeIfPresent(constraintType: ConstraintTypes): AbstractControl {
271     return this.constraintsArray.controls.find((control: AbstractControl) => {
272       const type = control.get('type').value;
273       return type == constraintType;
274     });
275   }
276
277   trackByFn(index) {
278     return index;
279   }
280
281   isComparable(propType: string): boolean {
282     if (PROPERTY_DATA.COMPARABLE_TYPES.indexOf(propType) >= 0) {
283       return true;
284     }
285     return false;
286   }
287
288   private getConstraintFromPropertyBEModel(constraint: any): AbstractControl {
289     console.log('be model constraints', constraint);
290     let constraintType: ConstraintTypes;
291     let constraintValue: any;
292     if (!constraint) {
293       constraintType = ConstraintTypes.null;
294       constraintValue = '';
295     } else if (constraint.hasOwnProperty(ConstraintTypes.valid_values)) {
296       constraintType = ConstraintTypes.valid_values;
297     } else if (constraint.hasOwnProperty(ConstraintTypes.equal)) {
298       constraintType = ConstraintTypes.equal;
299       constraintValue = constraint.equal;
300     } else if (constraint.hasOwnProperty(ConstraintTypes.greater_than)) {
301       constraintType = ConstraintTypes.greater_than;
302       constraintValue = constraint.greaterThan;
303     } else if (constraint.hasOwnProperty(ConstraintTypes.greater_or_equal)) {
304       constraintType = ConstraintTypes.greater_or_equal;
305       constraintValue = constraint.greaterOrEqual;
306     } else if (constraint.hasOwnProperty(ConstraintTypes.less_than)) {
307       constraintType = ConstraintTypes.less_than;
308       constraintValue = constraint.lessThan;
309     } else if (constraint.hasOwnProperty(ConstraintTypes.less_or_equal)) {
310       constraintType = ConstraintTypes.less_or_equal;
311       constraintValue = constraint.lessOrEqual;
312     } else if (constraint.hasOwnProperty(ConstraintTypes.in_range)) {
313       constraintType = ConstraintTypes.in_range;
314       constraintValue = new Array(constraint.inRange[0], constraint.inRange[1]);
315     } else if (constraint.rangeMaxValue || constraint.rangeMinValue) {
316       constraintType = ConstraintTypes.in_range;
317       constraintValue = new Array(constraint.rangeMinValue, constraint.rangeMaxValue);
318     } else if (constraint.hasOwnProperty(ConstraintTypes.length)) {
319       constraintType = ConstraintTypes.length;
320       constraintValue = constraint.length;
321     } else if (constraint.hasOwnProperty(ConstraintTypes.min_length)) {
322       constraintType = ConstraintTypes.min_length;
323       constraintValue = constraint.minLength;
324     } else if (constraint.hasOwnProperty(ConstraintTypes.max_length)) {
325       constraintType = ConstraintTypes.max_length;
326       constraintValue = constraint.maxLength;
327     } else if (constraint.hasOwnProperty(ConstraintTypes.pattern)) {
328       constraintType = ConstraintTypes.pattern;
329       constraintValue = constraint.pattern;
330     }
331
332     if (!constraint.hasOwnProperty(ConstraintTypes.valid_values) && !constraint.hasOwnProperty(ConstraintTypes.in_range)) {
333       return this.formBuilder.group({
334         type: new FormControl({
335           value: constraintType,
336           disabled: this.isViewOnly
337         }, Validators.required),
338         value: new FormControl({
339           value: constraintValue,
340           disabled: this.isViewOnly
341         }, this.constraintValidators)
342       });
343     } else {
344       const newForm = this.formBuilder.group({
345         type: new FormControl({
346           value: constraintType,
347           disabled: this.isViewOnly
348         }, Validators.required),
349         value: this.formBuilder.array([])
350       });
351
352       const valRef = newForm.get('value') as FormArray;
353
354       if (constraint.hasOwnProperty(ConstraintTypes.valid_values)) {
355         constraint.validValues.forEach((val) => {
356           valRef.push(new FormControl(val, this.constraintValidators));
357         });
358       } else {
359         constraint.inRange.forEach((val) => {
360           valRef.push(new FormControl(val, 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 }