-
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..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,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.spec.ts b/modules/ui/src/app/services/test-run.service.spec.ts
index 2d1246369..cf82508de 100644
--- a/modules/ui/src/app/services/test-run.service.spec.ts
+++ b/modules/ui/src/app/services/test-run.service.spec.ts
@@ -616,4 +616,20 @@ describe('TestRunService', () => {
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);
+ });
+ });
});