Fix valid values constraints not showing
[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       if (constraint.hasOwnProperty(ConstraintTypes.valid_values)) {
354         constraint.validValues.forEach((val) => {
355           valRef.push(new FormControl({value: val, disabled: this.isViewOnly}, this.constraintValidators));
356         });
357       } else {
358         constraint.inRange.forEach((val) => {
359           valRef.push(new FormControl({value: val, disabled: this.isViewOnly}, this.constraintValidators));
360         });
361       }
362
363       console.log('new form', newForm);
364       return newForm;
365     }
366   }
367
368   private getConstraintsFormat(): any[] {
369     const constraintArray = new Array();
370     this.constraintsArray.controls.forEach((control: AbstractControl) => {
371       const type = control.get('type').value;
372       let constraint: Constraint;
373
374       if (type != ConstraintTypes.valid_values && type != ConstraintTypes.in_range) {
375         constraint = {
376           type,
377           value: control.get('value').value
378         };
379       } else {
380         const valArray = [];
381
382         control.get('value').value.forEach((val) => {
383           valArray.push(val);
384         });
385
386         constraint = {
387           type,
388           value: valArray
389         };
390       }
391
392       console.log('New constraint object', constraint);
393       constraintArray.push(this.getConstraintFormat(constraint));
394     });
395     return constraintArray;
396   }
397
398   private getConstraintFormat(constraint: Constraint): any {
399     switch (constraint.type) {
400       case ConstraintTypes.equal:
401         return {
402           [ConstraintTypes.equal]: constraint.value
403         };
404       case ConstraintTypes.less_or_equal:
405         return {
406           [ConstraintTypes.less_or_equal]: constraint.value
407         };
408       case ConstraintTypes.less_than:
409         return {
410           [ConstraintTypes.less_than]: constraint.value
411         };
412       case ConstraintTypes.greater_or_equal:
413         return {
414           [ConstraintTypes.greater_or_equal]: constraint.value
415         };
416       case ConstraintTypes.greater_than:
417         return {
418           [ConstraintTypes.greater_than]: constraint.value
419         };
420       case ConstraintTypes.in_range:
421         return {
422           [ConstraintTypes.in_range]: constraint.value
423         };
424       case ConstraintTypes.length:
425         return {
426           [ConstraintTypes.length]: constraint.value
427         };
428       case ConstraintTypes.max_length:
429         return {
430           [ConstraintTypes.max_length]: constraint.value
431         };
432       case ConstraintTypes.min_length:
433         return {
434           [ConstraintTypes.min_length]: constraint.value
435         };
436       case ConstraintTypes.pattern:
437         return {
438           [ConstraintTypes.pattern]: constraint.value
439         };
440       case ConstraintTypes.valid_values:
441         return {
442           [ConstraintTypes.valid_values]: constraint.value
443         };
444       default:
445         return;
446     }
447   }
448
449   private validateConstraints(): void {
450     this.valid = this.constraintsArray.controls.every((control: AbstractControl) => {
451       const value = control.get('value').value;
452       const type = control.get('type').value;
453       control.updateValueAndValidity();
454
455       if (Array.isArray(value)) {
456         return !(value.length == 0 || this.doesArrayContaintEmptyValues(value));
457       }
458       if (type == ConstraintTypes.pattern) {
459         try {
460           new RegExp(value);
461           this.valid = true;
462         } catch (e) {
463           this.valid = false;
464         }
465       } else {
466         this.valid = this.constraintForm.valid;
467       }
468
469       return value && type != ConstraintTypes.null;
470     });
471   }
472
473   private doesArrayContaintEmptyValues(arr) {
474     for (const element of arr) {
475       if (element === '') { return true; }
476     }
477     return false;
478   }
479
480   private emitOnConstraintChange(): void {
481     console.log('constraints', this.constraintsArray);
482
483     this.validateConstraints();
484     const newConstraints = this.getConstraintsFormat();
485
486     this.valid = this.constraintForm.valid;
487     console.log('emitOnConstraintChange.valid', this.valid);
488
489     this.onConstraintChange.emit({
490       constraints: newConstraints,
491       valid: this.valid
492     });
493   }
494
495 }
496
497 export enum ConstraintTypes {
498   null = '',
499   equal= 'equal',
500   greater_than = 'greaterThan',
501   greater_or_equal = 'greaterOrEqual',
502   less_than = 'lessThan',
503   less_or_equal = 'lessOrEqual',
504   in_range = 'inRange',
505   valid_values = 'validValues',
506   length = 'length',
507   min_length = 'minLength',
508   max_length = 'maxLength',
509   pattern = 'pattern'
510 }
511
512 export const ConstraintTypesMapping = {
513   [ConstraintTypes.equal]: 'equal',
514   [ConstraintTypes.greater_than]: 'greater_than',
515   [ConstraintTypes.greater_or_equal]: 'greater_or_equal',
516   [ConstraintTypes.less_than]: 'less_than',
517   [ConstraintTypes.less_or_equal]: 'less_or_equal',
518   [ConstraintTypes.in_range]: 'in_range',
519   [ConstraintTypes.valid_values]: 'valid_values',
520   [ConstraintTypes.length]: 'length',
521   [ConstraintTypes.min_length]: 'min_length',
522   [ConstraintTypes.max_length]: 'max_length',
523   [ConstraintTypes.pattern]: 'pattern'
524 };
525
526 export interface Constraint {
527   type: ConstraintTypes;
528   value: any;
529 }
530
531 export function intValidator(): ValidatorFn {
532   const intRegex = /^[-+]?\d+$/;
533   return (control: AbstractControl): ValidationErrors | null => {
534     if (control.value && !intRegex.test(control.value)) {
535       return {invalidInt: true};
536     }
537
538     return null;
539   };
540 }
541
542 export function floatValidator(): ValidatorFn {
543   const floatRegex = /^[-+]?\d+(\.\d+)?$/;
544
545   return (control: AbstractControl): ValidationErrors | null => {
546     if (control.value && !floatRegex.test(control.value)) {
547       return {invalidFloat: true};
548     }
549
550     return null;
551   };
552 }