From 60d9b340e2f314a81aaeb0ab83933994268027b0 Mon Sep 17 00:00:00 2001 From: kurilova Date: Wed, 12 Jun 2024 12:52:25 +0000 Subject: [PATCH 1/5] Form from json init commit --- .../profile-form/profile-form.component.html | 1 - .../risk-assessment/profile-form/profile-form.component.ts | 7 +------ modules/ui/src/app/services/test-run.service.ts | 4 ++++ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html index 1738d5ef0..9043c52cb 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html @@ -15,7 +15,6 @@ -->
-

Profile name *

{ return this.http.get(`${API_URL}/profiles/format`); } + + fetchProfilesFormat(): Observable { + return this.http.get(`${API_URL}/profiles/format`); + } } From 6be603823720a6b59afad21ce9edc8ed34df93b6 Mon Sep 17 00:00:00 2001 From: kurilova Date: Thu, 13 Jun 2024 12:20:52 +0000 Subject: [PATCH 2/5] Fix design issues --- .../risk-assessment/profile-form/profile-form.component.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts index 9c2a716d8..85f93f546 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Input, + OnInit, +} from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; From de95369006fa92a7697079955313ccae9763ed5d Mon Sep 17 00:00:00 2001 From: kurilova Date: Fri, 14 Jun 2024 08:39:21 +0000 Subject: [PATCH 3/5] Add tests --- .../profile-form/profile-form.component.html | 1 + .../ui/src/app/services/test-run.service.spec.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html index 9043c52cb..1738d5ef0 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html @@ -15,6 +15,7 @@ -->
+

Profile name *

{ req.flush(result); }); }); + + describe('fetchProfilesFormat', () => { + it('should get system status data with no changes', () => { + const result = { ...PROFILE_FORM }; + + service.fetchProfilesFormat().subscribe(res => { + expect(res).toEqual(result); + }); + + const req = httpTestingController.expectOne( + 'http://localhost:8000/profiles/format' + ); + expect(req.request.method).toBe('GET'); + req.flush(result); + }); + }); }); From 35c1aace9bb75d393cced374fca0dfe86893bc10 Mon Sep 17 00:00:00 2001 From: kurilova Date: Mon, 17 Jun 2024 11:42:11 +0000 Subject: [PATCH 4/5] Adds field validation --- modules/ui/src/app/mocks/profile.mock.ts | 8 +- .../profile-form/profile-form.component.html | 31 ++++- .../profile-form/profile-form.component.scss | 3 + .../profile-form.component.spec.ts | 120 +++++++++++++++++- .../profile-form/profile-form.component.ts | 50 ++++++-- .../profile-form/profile.validators.ts | 42 +++++- .../ui/src/app/services/test-run.service.ts | 4 - 7 files changed, 237 insertions(+), 21 deletions(-) diff --git a/modules/ui/src/app/mocks/profile.mock.ts b/modules/ui/src/app/mocks/profile.mock.ts index 000be4337..596f922f8 100644 --- a/modules/ui/src/app/mocks/profile.mock.ts +++ b/modules/ui/src/app/mocks/profile.mock.ts @@ -40,6 +40,7 @@ export const PROFILE_FORM: ProfileFormat[] = [ type: FormControlType.TEXTAREA, validation: { required: true, + max: '28', }, description: 'This tells us about the device', }, @@ -47,9 +48,8 @@ export const PROFILE_FORM: ProfileFormat[] = [ question: 'Has this device already been through a criticality assessment with testrun?', type: FormControlType.SELECT, - options: [], + options: ['1', '2', '3'], validation: { - max: '128', required: true, }, }, @@ -67,5 +67,9 @@ export const PROFILE_FORM: ProfileFormat[] = [ question: 'Comments', type: FormControlType.TEXT, description: 'Please enter any comments here', + validation: { + max: '28', + required: true, + }, }, ]; diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html index 1738d5ef0..b00f89e67 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.html @@ -15,7 +15,6 @@ -->
-

Profile name *

{{ description }} + + Please, check. “ and \ are not allowed. + + + The field is required + @@ -187,6 +195,15 @@ id="{{ formControlName }}-group" [formControlName]="formControlName" /> {{ description }} + + Please, check. “ and \ are not allowed. + + + The field is required + @@ -204,6 +221,15 @@ id="{{ formControlName }}-group" [formControlName]="formControlName" /> {{ description }} + + The field is required + + + Please, check the email address. Valid e-mail can contain only latin + letters, numbers, @ and . (dot). + @@ -252,5 +278,8 @@ {{ description }} + + The field is required + diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss index f65dc04c0..8977b5a56 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss @@ -34,6 +34,9 @@ &:first-child { padding-top: 0; } + &:has(+.field-select-multiple.ng-invalid.ng-dirty){ + color: mat.get-color-from-palette($color-warn, 700); + } } mat-form-field { width: 100%; diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts index cc2a328cb..9cc94bb30 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.spec.ts @@ -173,10 +173,122 @@ describe('ProfileFormComponent', () => { }); if (item.type === FormControlType.SELECT) { - it(`should have default value if provided`, () => { - const fields = compiled.querySelectorAll('.profile-form-field'); - const select = fields[uiIndex].querySelector('mat-select'); - expect(select?.textContent?.trim()).toEqual(item.default || ''); + describe('select', () => { + it(`should have default value if provided`, () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const select = fields[uiIndex].querySelector('mat-select'); + expect(select?.textContent?.trim()).toEqual(item.default || ''); + }); + + it('should have "required" error when field is not filled', () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const select = fields[uiIndex].querySelector( + 'mat-select' + ) as HTMLElement; + + select.focus(); + select.blur(); + + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + + const error = fields[uiIndex].querySelector('mat-error')?.innerHTML; + + expect(error).toContain('The field is required'); + }); + }); + } + + if ( + (item.type === FormControlType.TEXT || + item.type === FormControlType.TEXTAREA) && + item.validation?.required + ) { + describe('text or text-long', () => { + it('should have "required" error when field is not filled', () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + const input: HTMLInputElement = fields[uiIndex].querySelector( + 'input' + ) as HTMLInputElement; + input.value = ''; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + + const error = + fields[uiIndex].querySelector('mat-error')?.textContent; + + expect(error).toContain('The field is required'); + }); + + it('should have "invalid_format" error when field does not satisfy validation rules', () => { + [ + 'very long value very long value very long value very long value very long value very long value very long value very long value very long value very long value', + 'as\\\\\\\\\\""""""""', + ].forEach(value => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + const input: HTMLInputElement = fields[uiIndex].querySelector( + 'input' + ) as HTMLInputElement; + input.value = value; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + + const error = compiled.querySelector('mat-error')?.textContent; + expect(error).toContain( + 'Please, check. “ and \\ are not allowed.' + ); + }); + }); + }); + } + + if ( + item.type === FormControlType.EMAIL_MULTIPLE && + item.validation?.required + ) { + describe('text or text-long', () => { + it('should have "required" error when field is not filled', () => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + const input: HTMLInputElement = fields[uiIndex].querySelector( + 'input' + ) as HTMLInputElement; + input.value = ''; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + + const error = + fields[uiIndex].querySelector('mat-error')?.textContent; + + expect(error).toContain('The field is required'); + }); + + it('should have "invalid_format" error when field does not satisfy validation rules', () => { + [ + 'very long value very long value very long value very long value very long value very long value very long value very long value very long value very long value', + 'as\\\\\\\\\\""""""""', + ].forEach(value => { + const fields = compiled.querySelectorAll('.profile-form-field'); + const uiIndex = index + 1; // as Profile name is at 0 position, the json items start from 1 i + const input: HTMLInputElement = fields[uiIndex].querySelector( + 'input' + ) as HTMLInputElement; + input.value = value; + input.dispatchEvent(new Event('input')); + component.getControl(index).markAsTouched(); + fixture.detectChanges(); + + const error = compiled.querySelector('mat-error')?.textContent; + expect(error).toContain( + 'Please, check the email address. Valid e-mail can contain only latin letters, numbers, @ and . (dot).' + ); + }); + }); }); } }); diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts index 85f93f546..3019df378 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.ts @@ -20,25 +20,28 @@ import { OnInit, } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; -import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatError, MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { CommonModule } from '@angular/common'; import { AbstractControl, - FormControl, FormBuilder, + FormControl, FormGroup, ReactiveFormsModule, + ValidatorFn, Validators, } from '@angular/forms'; import { MatInputModule } from '@angular/material/input'; import { DeviceValidators } from '../../devices/components/device-form/device.validators'; -import { Profile } from '../../../model/profile'; +import { + FormControlType, + Profile, + ProfileFormat, + Validation, +} from '../../../model/profile'; import { ProfileValidators } from './profile.validators'; -import { MatError } from '@angular/material/form-field'; - -import { FormControlType, ProfileFormat } from '../../../model/profile'; @Component({ selector: 'app-profile-form', @@ -73,7 +76,11 @@ export class ProfileFormComponent implements OnInit { } get nameControl() { - return this.profileForm.get('name') as AbstractControl; + return this.getControl('name'); + } + + getControl(name: string | number) { + return this.profileForm.get(name.toString()) as AbstractControl; } createProfileForm(questions: ProfileFormat[]): FormGroup { @@ -87,22 +94,47 @@ export class ProfileFormComponent implements OnInit { ]); questions.forEach((question, index) => { + const validators = this.getValidators(question.type, question.validation); if (question.type === FormControlType.SELECT_MULTIPLE) { group[index] = this.getMultiSelectGroup(question); } else { - group[index] = new FormControl(question.default || ''); + group[index] = new FormControl(question.default || '', validators); } }); return new FormGroup(group); } + getValidators(type: FormControlType, validation?: Validation): ValidatorFn[] { + const validators: ValidatorFn[] = []; + if (validation) { + if (validation.required) { + validators.push(Validators.required); + } + if (type === FormControlType.EMAIL_MULTIPLE) { + validators.push( + this.profileValidators.emailStringFormat(Number(validation.max)) + ); + } + if (type === FormControlType.TEXT || type === FormControlType.TEXTAREA) { + validators.push( + this.profileValidators.textFormat(Number(validation.max)) + ); + } + } + return validators; + } + getMultiSelectGroup(question: ProfileFormat): FormGroup { // eslint-disable-next-line @typescript-eslint/no-explicit-any const group: any = {}; question.options?.forEach((option, index) => { group[index] = false; }); - return this.fb.group(group); + return this.fb.group(group, { + validators: question.validation?.required + ? [this.profileValidators.multiSelectRequired] + : [], + }); } getFormGroup(name: string): FormGroup { diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts b/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts index 7de2cd353..bed826c45 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile.validators.ts @@ -14,10 +14,23 @@ * limitations under the License. */ import { Injectable } from '@angular/core'; -import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { + AbstractControl, + FormGroup, + ValidationErrors, + ValidatorFn, +} from '@angular/forms'; import { Profile } from '../../../model/profile'; + @Injectable({ providedIn: 'root' }) export class ProfileValidators { + readonly MULTIPLE_EMAIL_FORMAT_REGEXP = new RegExp( + '^(([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)(\\s*;\\s*|\\s*$))*$', + 'i' + ); + + readonly STRING_FORMAT_REGEXP = new RegExp('^[^"\\\\]*$', 'u'); + public differentProfileName(profiles: Profile[]): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const value = control.value?.trim(); @@ -29,6 +42,33 @@ export class ProfileValidators { }; } + public multiSelectRequired(g: FormGroup) { + if (Object.values(g.value).every(value => value === false)) { + return { required: true }; + } + return null; + } + + public emailStringFormat(maxLength: number = 128): ValidatorFn { + return this.stringFormat(this.MULTIPLE_EMAIL_FORMAT_REGEXP, maxLength); + } + + public textFormat(maxLength: number = 128): ValidatorFn { + return this.stringFormat(this.STRING_FORMAT_REGEXP, maxLength); + } + + private stringFormat(regExp: RegExp, maxLength: number = 28): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value?.trim(); + if (value) { + if (value.length > maxLength) return { invalid_format: true }; + const result = regExp.test(value); + return !result ? { invalid_format: true } : null; + } + return null; + }; + } + private hasSameProfileName( profileName: string, profiles: Profile[] diff --git a/modules/ui/src/app/services/test-run.service.ts b/modules/ui/src/app/services/test-run.service.ts index e1ac8c164..77d7a4440 100644 --- a/modules/ui/src/app/services/test-run.service.ts +++ b/modules/ui/src/app/services/test-run.service.ts @@ -283,8 +283,4 @@ export class TestRunService { fetchProfilesFormat(): Observable { return this.http.get(`${API_URL}/profiles/format`); } - - fetchProfilesFormat(): Observable { - return this.http.get(`${API_URL}/profiles/format`); - } } From 85797e9636340adb23c516b540e467a02b04b2d7 Mon Sep 17 00:00:00 2001 From: kurilova Date: Tue, 18 Jun 2024 09:13:35 +0000 Subject: [PATCH 5/5] Fix es lint --- .../risk-assessment/profile-form/profile-form.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss index 8977b5a56..baae7fb38 100644 --- a/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss +++ b/modules/ui/src/app/pages/risk-assessment/profile-form/profile-form.component.scss @@ -34,7 +34,7 @@ &:first-child { padding-top: 0; } - &:has(+.field-select-multiple.ng-invalid.ng-dirty){ + &:has(+ .field-select-multiple.ng-invalid.ng-dirty) { color: mat.get-color-from-palette($color-warn, 700); } }