[CST-4634] Change email validator in order to show error also on focus

This commit is contained in:
Giuseppe Digilio
2021-10-13 23:13:39 +02:00
parent 91218bb534
commit 9c47583a0a
3 changed files with 135 additions and 104 deletions

View File

@@ -1,5 +1,5 @@
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
import { FormGroup, Validators } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { import {
DynamicCheckboxModel, DynamicCheckboxModel,
DynamicFormControlModel, DynamicFormControlModel,
@@ -8,7 +8,7 @@ import {
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
import { switchMap, take } from 'rxjs/operators'; import { debounceTime, switchMap, take } from 'rxjs/operators';
import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
@@ -37,7 +37,7 @@ import { ValidateEmailNotTaken } from './validators/email-taken.validator';
@Component({ @Component({
selector: 'ds-eperson-form', selector: 'ds-eperson-form',
templateUrl: './eperson-form.component.html' templateUrl: './eperson-form.component.html',
}) })
/** /**
* A form used for creating and editing EPeople * A form used for creating and editing EPeople
@@ -162,7 +162,13 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
*/ */
isImpersonated = false; isImpersonated = false;
constructor(public epersonService: EPersonDataService, /**
* Subscription to email field value change
*/
emailValueChangeSubscribe: Subscription;
constructor(protected changeDetectorRef: ChangeDetectorRef,
public epersonService: EPersonDataService,
public groupsDataService: GroupDataService, public groupsDataService: GroupDataService,
private formBuilderService: FormBuilderService, private formBuilderService: FormBuilderService,
private translateService: TranslateService, private translateService: TranslateService,
@@ -225,8 +231,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
}, },
required: true, required: true,
errorMessages: { errorMessages: {
emailTaken: 'error.validation.emailTaken', emailTaken: 'error.validation.emailTaken',
pattern: 'error.validation.NotValidEmail' pattern: 'error.validation.NotValidEmail'
}, },
hint: emailHint hint: emailHint
}); });
@@ -266,15 +272,18 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
canLogIn: eperson != null ? eperson.canLogIn : true, canLogIn: eperson != null ? eperson.canLogIn : true,
requireCertificate: eperson != null ? eperson.requireCertificate : false requireCertificate: eperson != null ? eperson.requireCertificate : false
}); });
}));
if (!!this.formGroup.controls.email) { if (eperson === null && !!this.formGroup.controls.email) {
this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService)); this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService));
} this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => {
this.changeDetectorRef.detectChanges();
});
}
}));
const activeEPerson$ = this.epersonService.getActiveEPerson(); const activeEPerson$ = this.epersonService.getActiveEPerson();
this.groups = activeEPerson$.pipe( this.groups = activeEPerson$.pipe(
switchMap((eperson) => { switchMap((eperson) => {
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, { return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
currentPage: 1, currentPage: 1,
@@ -353,10 +362,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
getFirstCompletedRemoteData() getFirstCompletedRemoteData()
).subscribe((rd: RemoteData<EPerson>) => { ).subscribe((rd: RemoteData<EPerson>) => {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', {name: ePersonToCreate.name})); this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name }));
this.submitForm.emit(ePersonToCreate); this.submitForm.emit(ePersonToCreate);
} else { } else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', {name: ePersonToCreate.name})); this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: ePersonToCreate.name }));
this.cancelForm.emit(); this.cancelForm.emit();
} }
}); });
@@ -392,10 +401,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
const response = this.epersonService.updateEPerson(editedEperson); const response = this.epersonService.updateEPerson(editedEperson);
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<EPerson>) => { response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<EPerson>) => {
if (rd.hasSucceeded) { if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', {name: editedEperson.name})); this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name }));
this.submitForm.emit(editedEperson); this.submitForm.emit(editedEperson);
} else { } else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', {name: editedEperson.name})); this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: editedEperson.name }));
this.cancelForm.emit(); this.cancelForm.emit();
} }
}); });
@@ -405,6 +414,87 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
} }
} }
/**
* Event triggered when the user changes page
* @param event
*/
onPageChange(event) {
this.updateGroups({
currentPage: event,
elementsPerPage: this.config.pageSize
});
}
/**
* Start impersonating the EPerson
*/
impersonate() {
this.authService.impersonate(this.epersonInitial.id);
this.isImpersonated = true;
}
/**
* Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing.
* It'll either show a success or error message depending on whether the delete was successful or not.
*/
delete() {
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = eperson;
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
modalRef.componentInstance.brandColor = 'danger';
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
if (confirm) {
if (hasValue(eperson.id)) {
this.epersonService.deleteEPerson(eperson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
if (restResponse.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: eperson.name }));
this.submitForm.emit();
} else {
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
}
this.cancelForm.emit();
});
}
}
});
});
}
/**
* Stop impersonating the EPerson
*/
stopImpersonating() {
this.authService.stopImpersonatingAndRefresh();
this.isImpersonated = false;
}
/**
* Cancel the current edit when component is destroyed & unsub all subscriptions
*/
ngOnDestroy(): void {
this.onCancel();
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
this.paginationService.clearPagination(this.config.id);
if (hasValue(this.emailValueChangeSubscribe)) {
this.emailValueChangeSubscribe.unsubscribe();
}
}
/**
* This method will ensure that the page gets reset and that the cache is cleared
*/
reset() {
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
this.requestService.removeByHrefSubstring(eperson.self);
});
this.initialisePage();
}
/** /**
* Checks for the given ePerson if there is already an ePerson in the system with that email * Checks for the given ePerson if there is already an ePerson in the system with that email
* and shows notification if this is the case * and shows notification if this is the case
@@ -427,17 +517,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
})); }));
} }
/**
* Event triggered when the user changes page
* @param event
*/
onPageChange(event) {
this.updateGroups({
currentPage: event,
elementsPerPage: this.config.pageSize
});
}
/** /**
* Update the list of groups by fetching it from the rest api or cache * Update the list of groups by fetching it from the rest api or cache
*/ */
@@ -446,71 +525,4 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, options); this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, options);
})); }));
} }
/**
* Start impersonating the EPerson
*/
impersonate() {
this.authService.impersonate(this.epersonInitial.id);
this.isImpersonated = true;
}
/**
* Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing.
* It'll either show a success or error message depending on whether the delete was successful or not.
*/
delete() {
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
const modalRef = this.modalService.open(ConfirmationModalComponent);
modalRef.componentInstance.dso = eperson;
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
modalRef.componentInstance.brandColor = 'danger';
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
if (confirm) {
if (hasValue(eperson.id)) {
this.epersonService.deleteEPerson(eperson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
if (restResponse.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: eperson.name }));
this.submitForm.emit();
} else {
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
}
this.cancelForm.emit();
});
}}
});
});
}
/**
* Stop impersonating the EPerson
*/
stopImpersonating() {
this.authService.stopImpersonatingAndRefresh();
this.isImpersonated = false;
}
/**
* Cancel the current edit when component is destroyed & unsub all subscriptions
*/
ngOnDestroy(): void {
this.onCancel();
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
this.paginationService.clearPagination(this.config.id);
}
/**
* This method will ensure that the page gets reset and that the cache is cleared
*/
reset() {
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
this.requestService.removeByHrefSubstring(eperson.self);
});
this.initialisePage();
}
} }

View File

@@ -11,13 +11,15 @@ export class ValidateEmailNotTaken {
* This method will create the validator with the ePersonDataService requested from component * This method will create the validator with the ePersonDataService requested from component
* @param ePersonDataService the service with DI in the component that this validator is being utilized. * @param ePersonDataService the service with DI in the component that this validator is being utilized.
*/ */
static createValidator(ePersonDataService: EPersonDataService) { static createValidator(ePersonDataService: EPersonDataService) {
return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => { return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
return ePersonDataService.getEPersonByEmail(control.value) return ePersonDataService.getEPersonByEmail(control.value)
.pipe( .pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
map(res => { return !!res.payload ? { emailTaken: true } : null; }) map(res => {
); return !!res.payload ? { emailTaken: true } : null;
}; })
} );
};
}
} }

View File

@@ -7,7 +7,11 @@ import { EffectsModule } from '@ngrx/effects';
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store'; import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
import { MetaReducer, Store, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store'; import { MetaReducer, Store, StoreModule, USER_PROVIDED_META_REDUCERS } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { DYNAMIC_MATCHER_PROVIDERS } from '@ng-dynamic-forms/core'; import {
DYNAMIC_ERROR_MESSAGES_MATCHER,
DYNAMIC_MATCHER_PROVIDERS,
DynamicErrorMessagesMatcher
} from '@ng-dynamic-forms/core';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
@@ -52,6 +56,7 @@ import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
import { UUIDService } from './core/shared/uuid.service'; import { UUIDService } from './core/shared/uuid.service';
import { CookieService } from './core/services/cookie.service'; import { CookieService } from './core/services/cookie.service';
import { AbstractControl } from '@angular/forms';
export function getBase() { export function getBase() {
return environment.ui.nameSpace; return environment.ui.nameSpace;
@@ -61,6 +66,14 @@ export function getMetaReducers(): MetaReducer<AppState>[] {
return environment.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers; return environment.debug ? [...appMetaReducers, ...debugMetaReducers] : appMetaReducers;
} }
/**
* Condition for displaying error messages on email form field
*/
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
(control: AbstractControl, model: any, hasFocus: boolean) => {
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
};
const IMPORTS = [ const IMPORTS = [
CommonModule, CommonModule,
SharedModule, SharedModule,
@@ -146,6 +159,10 @@ const PROVIDERS = [
multi: true, multi: true,
deps: [ CookieService, UUIDService ] deps: [ CookieService, UUIDService ]
}, },
{
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
useValue: ValidateEmailErrorStateMatcher
},
...DYNAMIC_MATCHER_PROVIDERS, ...DYNAMIC_MATCHER_PROVIDERS,
]; ];