Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions modules/ui/src/app/mocks/profile.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ export const PROFILE_FORM: ProfileFormat[] = [
type: FormControlType.TEXTAREA,
validation: {
required: true,
max: '28',
},
description: 'This tells us about the device',
},
{
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,
},
},
Expand All @@ -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,
},
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
-->
<form [formGroup]="profileForm" class="profile-form">
<div class="field-container">
<!-- TODO move profile name to json (in scope of validation) -->
<p class="field-label">Profile name *</p>
<mat-form-field
appearance="outline"
Expand Down Expand Up @@ -170,6 +169,15 @@
id="{{ formControlName }}-group"
[formControlName]="formControlName" />
<mat-hint *ngIf="description">{{ description }}</mat-hint>
<mat-error
*ngIf="getControl(formControlName).hasError('invalid_format')"
role="alert"
aria-live="assertive">
<span>Please, check. “ and \ are not allowed.</span>
</mat-error>
<mat-error *ngIf="getControl(formControlName).hasError('required')">
<span>The field is required</span>
</mat-error>
</mat-form-field>
</ng-template>

Expand All @@ -187,6 +195,15 @@
id="{{ formControlName }}-group"
[formControlName]="formControlName" />
<mat-hint *ngIf="description">{{ description }}</mat-hint>
<mat-error
*ngIf="getControl(formControlName).hasError('invalid_format')"
role="alert"
aria-live="assertive">
<span>Please, check. “ and \ are not allowed.</span>
</mat-error>
<mat-error *ngIf="getControl(formControlName).hasError('required')">
<span>The field is required</span>
</mat-error>
</mat-form-field>
</ng-template>

Expand All @@ -204,6 +221,15 @@
id="{{ formControlName }}-group"
[formControlName]="formControlName" />
<mat-hint *ngIf="description">{{ description }}</mat-hint>
<mat-error *ngIf="getControl(formControlName).hasError('required')">
<span>The field is required</span>
</mat-error>
<mat-error *ngIf="getControl(formControlName).hasError('invalid_format')">
<span
>Please, check the email address. Valid e-mail can contain only latin
letters, numbers, &#64; and . (dot).</span
>
</mat-error>
</mat-form-field>
</ng-template>

Expand Down Expand Up @@ -252,5 +278,8 @@
<mat-hint *ngIf="description" class="field-hint">{{
description
}}</mat-hint>
<mat-error *ngIf="getControl(formControlName).hasError('required')">
<span>The field is required</span>
</mat-error>
</mat-form-field>
</ng-template>
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).'
);
});
});
});
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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[]
Expand Down
Loading