diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/validators/cd-validators.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/validators/cd-validators.spec.ts new file mode 100644 index 00000000000..509932a077c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/validators/cd-validators.spec.ts @@ -0,0 +1,89 @@ +import { FormControl, FormGroup } from '@angular/forms'; + +import { CdValidators } from './cd-validators'; + +describe('CdValidators', () => { + describe('email', () => { + it('should not error on an empty email address', () => { + const control = new FormControl(''); + expect(CdValidators.email(control)).toBeNull(); + }); + + it('should not error on valid email address', () => { + const control = new FormControl('dashboard@ceph.com'); + expect(CdValidators.email(control)).toBeNull(); + }); + + it('should error on invalid email address', () => { + const control = new FormControl('xyz'); + expect(CdValidators.email(control)).toEqual({'email': true}); + }); + }); + + describe('requiredIf', () => { + let form: FormGroup; + + beforeEach(() => { + form = new FormGroup({ + x: new FormControl(true), + y: new FormControl('abc'), + z: new FormControl('') + }); + }); + + it('should not error because all conditions are fulfilled', () => { + form.get('z').setValue('zyx'); + const validatorFn = CdValidators.requiredIf({ + 'x': true, + 'y': 'abc' + }); + expect(validatorFn(form.controls['z'])).toBeNull(); + }); + + it('should not error because of unmet prerequisites', () => { + // Define prereqs that do not match the current values of the form fields. + const validatorFn = CdValidators.requiredIf({ + 'x': false, + 'y': 'xyz' + }); + // The validator must succeed because the prereqs do not match, so the + // validation of the 'z' control will be skipped. + expect(validatorFn(form.controls['z'])).toBeNull(); + }); + + it('should error because of an empty value', () => { + // Define prereqs that force the validator to validate the value of + // the 'z' control. + const validatorFn = CdValidators.requiredIf({ + 'x': true, + 'y': 'abc' + }); + // The validator must fail because the value of control 'z' is empty. + expect(validatorFn(form.controls['z'])).toEqual({'required': true}); + }); + + it('should not error because of unsuccessful condition', () => { + form.get('z').setValue('zyx'); + // Define prereqs that force the validator to validate the value of + // the 'z' control. + const validatorFn = CdValidators.requiredIf({ + 'x': true, + 'z': 'zyx' + }, () => false); + expect(validatorFn(form.controls['z'])).toBeNull(); + }); + + it('should error because of successful condition', () => { + const conditionFn = (value) => { + return value === 'abc'; + }; + // Define prereqs that force the validator to validate the value of + // the 'y' control. + const validatorFn = CdValidators.requiredIf({ + 'x': true, + 'z': '' + }, conditionFn); + expect(validatorFn(form.controls['y'])).toEqual({'required': true}); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/validators/cd-validators.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/validators/cd-validators.ts new file mode 100644 index 00000000000..88edeb8aef3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/validators/cd-validators.ts @@ -0,0 +1,65 @@ +import { + AbstractControl, + ValidationErrors, + ValidatorFn, + Validators +} from '@angular/forms'; + +import * as _ from 'lodash'; + +type Prerequisites = { // tslint:disable-line + [key: string]: any +}; + +export function isEmptyInputValue(value: any): boolean { + return value == null || value.length === 0; +} + +export class CdValidators { + /** + * Validator that performs email validation. In contrast to the Angular + * email validator an empty email will not be handled as invalid. + */ + static email(control: AbstractControl): ValidationErrors | null { + // Exit immediately if value is empty. + if (isEmptyInputValue(control.value)) { + return null; + } + return Validators.email(control); + } + + /** + * Validator that requires controls to fulfill the specified condition if + * the specified prerequisites matches. If the prerequisites are fulfilled, + * then the given function is executed and if it succeeds, the 'required' + * validation error will be returned, otherwise null. + * @param {Prerequisites} prerequisites An object containing the prerequisites. + * ### Example + * ```typescript + * { + * 'generate_key': true, + * 'username': 'Max Mustermann' + * } + * ``` + * Only if all prerequisites are fulfilled, then the validation of the + * control will be triggered. + * @param {Function | undefined} condition The function to be executed when all + * prerequisites are fulfilled. If not set, then the {@link isEmptyInputValue} + * function will be used by default. The control's value is used as function + * argument. The function must return true to set the validation error. + * @return {ValidatorFn} Returns the validator function. + */ + static requiredIf(prerequisites: Prerequisites, condition?: Function | undefined): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + // Check if all prerequisites matches. + if (!Object.keys(prerequisites).every((key) => { + return (control.parent && control.parent.get(key).value === prerequisites[key]); + })) { + return null; + } + const success = _.isFunction(condition) ? condition.call(condition, control.value) : + isEmptyInputValue(control.value); + return success ? {'required': true} : null; + }; + } +}