Commit 1c28c200 authored by Eduardo Sanz García's avatar Eduardo Sanz García
Browse files

feat (app): more clear error form reporting

parent aeb1241f
......@@ -7,3 +7,14 @@
margin-top: 2em;
}
.warning {
color: var(--warning);
}
.ng-valid[required], .ng-valid.required {
border-left: 5px solid var(--ok);
}
.ng-invalid:not(form) {
border-left: 5px solid var(--warning);
}
......@@ -6,9 +6,7 @@
</div>
<div class="spread separator">
<button (click)="auth.openLoginWindow({ttl: '1'})">ELIXIR login small
window
(expire
in 1 minute)</button>
window (expire in 1 minute)</button>
<button (click)="auth.openLoginTab()">ELIXIR login new tab (default
expiration)</button>
<button (click)="auth.logOut()">Logout</button>
......@@ -22,11 +20,11 @@
</label>
<label>
Username*:
<input formControlName="username" />
<input formControlName="username" required />
</label>
<label>
Password*:
<input formControlName="password" />
<input formControlName="password" required />
</label>
<label>
Email:
......@@ -37,20 +35,29 @@
<input formControlName="organization" />
</label>
</form>
<button type="button" [disabled]="createAAP.invalid"
(click)="createAAPaccount()">Create account</button>
<div class="warning" *ngIf="createAAPErrors$ | async as errors">
<ul>
<li *ngFor="let error of errors">{{ error }}</li>
</ul>
</div>
<button type="button" (click)="createAAPaccount()">Create account</button>
<h3 class="separator">Login AAP account:</h3>
<form [formGroup]="loginAAP">
<label>
Username*:
<input formControlName="username" />
<input formControlName="username" required />
</label>
<label>
Password*:
<input formControlName="password" />
<input formControlName="password" required />
</label>
</form>
<div class="warning" *ngIf="loginAAPErrors$ | async as errors">
<ul>
<li *ngFor="let error of errors">{{ error }}</li>
</ul>
</div>
<button type="button" [disabled]="loginAAP.invalid"
(click)="loginAAPaccount()">AAP login</button>
......@@ -58,17 +65,22 @@
<form [formGroup]="changePasswordAAP">
<label>
Username*:
<input formControlName="username" />
<input formControlName="username" required />
</label>
<label>
Old password*:
<input formControlName="oldPassword" />
<input formControlName="oldPassword" required />
</label>
<label>
New password*:
<input formControlName="newPassword" />
<input formControlName="newPassword" required />
</label>
</form>
<div class="warning" *ngIf="changePasswordAAPErrors$ | async as errors">
<ul>
<li *ngFor="let error of errors">{{ error }}</li>
</ul>
</div>
<button type="button" [disabled]="changePasswordAAP.invalid"
(click)="changePasswordAAPaccount()">AAP login</button>
......@@ -80,15 +92,19 @@
<h3 class="separator">Create new domain (group/team):</h3>
<form [formGroup]="domain">
<label>
Name:
<input formControlName="domainName" />
Name*:
<input formControlName="domainName" required />
</label>
<label>
Description:
<input formControlName="domainDesc" />
</label>
</form>
<div class="warning" *ngIf="domainErrors$ | async as errors">
<ul>
<li *ngFor="let error of errors">{{ error }}</li>
</ul>
</div>
<button type="button" [disabled]="domain.invalid"
(click)="createDomain(user.uid)">Create
domain</button>
......@@ -119,8 +135,9 @@
</ul>
</div>
<ng-template #loggedOut>
<h2>Please, log in, either through the federated ELIXIR system or through the
AAP</h2>
<h2>Please, log in, either through the federated ELIXIR system or through
the
AAP</h2>
</ng-template>
<h3 class="separator">Advanced handling of tokens:</h3>
......@@ -130,4 +147,3 @@
<p>Expiration date of the token: {{ expiration }}</p>
</div>
</div>
......@@ -273,4 +273,52 @@ describe('AppComponent', () => {
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Auth testing app');
});
it('should trigger hasForbiddenSpaces', () => {
app.createAAP.reset({
username: ' 12345'
});
expect(app.createAAP.controls['username'].getError('hasForbiddenSpaces')).toBeTruthy();
app.createAAP.reset({
username: '12345'
});
expect(app.createAAP.controls['username'].getError('hasForbiddenSpaces')).toBeFalsy();
app.createAAP.reset({
username: '12345 '
});
expect(app.createAAP.controls['username'].getError('hasForbiddenSpaces')).toBeTruthy();
});
it('should trigger hasForbiddenSpaces', () => {
app.createAAP.patchValue({
username: ' 1 ',
email: 'a@',
password: '',
// tslint:disable-next-line:max-line-length
name: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
});
app.createAAP.controls['username'].markAsTouched();
app.createAAP.controls['email'].markAsTouched();
app.createAAP.controls['password'].markAsTouched();
app.createAAP.controls['name'].markAsTouched();
const getErrors = spyOn((app as any), '_getErrors').and.callThrough();
const result = (app as any)._getErrors(app.createAAP);
expect(result).toEqual(
['Name: maximum 255 characters', 'Username: minimum 5 characters',
'Username: white space is not allowed at the begining or end', 'Password: required', 'Email: not valid']
);
expect(getErrors).toHaveBeenCalledTimes(1);
});
it('should capitalise correctly', () => {
const capitalise = spyOn((app as any), '_capitalise').and.callThrough();
const result = (app as any)._capitalise('oldPasswordShouldWork');
expect(result).toBe('Old password should work');
expect(capitalise).toHaveBeenCalledTimes(1);
});
});
......@@ -6,7 +6,8 @@ import {
import {
FormGroup,
FormBuilder,
Validators
Validators,
ValidatorFn
} from '@angular/forms';
import {
HttpClient,
......@@ -47,6 +48,15 @@ interface Domain {
domainReference: string;
}
function spacesNoAllowedStartEnd(): ValidatorFn {
return (control) => {
const hasForbiddenSpaces = /^\s+|\s+$/.test(control.value);
return hasForbiddenSpaces ? {
hasForbiddenSpaces: 'white space is not allowed at the begining or end'
} : null;
};
}
@Component({
selector: 'auth-root',
templateUrl: './app.component.html',
......@@ -59,49 +69,47 @@ export class AppComponent implements OnInit {
domains$: Observable < string[] > ;
managedDomains$: Observable < Domain[] > ;
// TODO:
// * only trigger logout callbacks if it was previously login
// * display validation messages
// * test forms
// * add custom sync validator for username
createAAP = this._fb.group({
name: [null, {
validators: [Validators.minLength(5), Validators.maxLength(255)]
validators: [Validators.minLength(1), Validators.maxLength(255)]
}],
username: [null, {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255), spacesNoAllowedStartEnd()]
}],
password: [null, {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
validators: [Validators.required, Validators.maxLength(255)]
}],
email: [null, {
validators: [Validators.email, Validators.minLength(5), Validators.maxLength(255)]
validators: [Validators.email, Validators.maxLength(255)]
}],
organization: [null, {
validators: [Validators.maxLength(255)]
}],
});
createAAPErrors$!: Observable<string[]>;
loginAAP = this._fb.group({
username: [null, {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255), spacesNoAllowedStartEnd()]
}],
password: [null, {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
validators: [Validators.required, Validators.maxLength(255)]
}],
});
loginAAPErrors$!: Observable<string[]>;
changePasswordAAP = this._fb.group({
username: [null, {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255), spacesNoAllowedStartEnd()]
}],
oldPassword: [null, {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
validators: [Validators.required, Validators.maxLength(255)]
}],
newPassword: [null, {
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(255)]
validators: [Validators.required, Validators.maxLength(255)]
}],
});
changePasswordAAPErrors$!: Observable<string[]>;
domain = this._fb.group({
domainName: [null, {
......@@ -110,6 +118,8 @@ export class AppComponent implements OnInit {
domainDesc: [null]
});
domainErrors$!: Observable<string[]>;
private readonly _domainsURL: string;
private readonly _authURL: string;
private readonly _managementURL: string;
......@@ -160,6 +170,22 @@ export class AppComponent implements OnInit {
}
ngOnInit() {
this.createAAPErrors$ = this.createAAP.valueChanges.pipe(
map( _ => this._getErrors(this.createAAP))
);
this.loginAAPErrors$ = this.loginAAP.valueChanges.pipe(
map( _ => this._getErrors(this.loginAAP))
);
this.changePasswordAAPErrors$ = this.changePasswordAAP.valueChanges.pipe(
map( _ => this._getErrors(this.changePasswordAAP))
);
this.domainErrors$ = this.domain.valueChanges.pipe(
map( _ => this._getErrors(this.domain))
);
// Demonstration of register and unregister login events
this.auth.addLogInEventListener(() => alert('Welcome'));
this.auth.addLogInEventListener(() => console.log('Welcome'));
......@@ -256,4 +282,42 @@ export class AppComponent implements OnInit {
catchError(error => of ([]))
);
}
private _getErrors(form: FormGroup): string[] {
const message: string[] = [];
Object.entries(form.controls).forEach(([name, control]) => {
if (control.errors && control.invalid && (control.dirty || control.touched)) {
Object.entries(control.errors).forEach(([error, content]) => {
const namePretty = this._capitalise(name);
switch (error) {
case 'minlength':
message.push(`${namePretty}: minimum ${content['requiredLength']} characters`);
break;
case 'maxlength':
message.push(`${namePretty}: maximum ${content['requiredLength']} characters`);
break;
case 'required':
message.push(`${namePretty}: required`);
break;
case 'email':
message.push(`${namePretty}: not valid`);
break;
case 'hasForbiddenSpaces':
message.push(`${namePretty}: ${content}`);
break;
default:
console.warn(namePretty, error, content);
}
});
}
});
return message;
}
private _capitalise(word: string, locale?: string): string {
// charAt is problematic with unicode
return word.charAt(0).toLocaleUpperCase() + word.slice(1).replace(/([A-Z])/g, (u) => ` ${u.toLocaleLowerCase()}`);
}
}
/* You can add global styles to this file, and also import other style files */
:root {
--warning: #e22626;
--ok: #42A948;
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment