From 9253edfe62f070cd276a60b96e8cdff5320635cb Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Mon, 6 Sep 2021 10:53:25 +0200 Subject: [PATCH] feat(employee): Added functionality to invite multiple emailaddresses. (TV-512) Squashed commit of the following: commit 04baa8e9398016ffb0cba618a5b857230dc4ea9e Author: Erik Tiekstra Date: Mon Sep 6 10:52:12 2021 +0200 moved email-regex commit 0f54392c2e65386f4d0ae79493f6d33d84e313fe Author: Erik Tiekstra Date: Mon Sep 6 07:54:13 2021 +0200 Updated confirmation texts commit 4557c8203cf826caccff7ac174e15b547f041993 Merge: ec932ec ec7b4fc Author: Erik Tiekstra Date: Mon Sep 6 07:37:56 2021 +0200 Merge branch 'develop' into feature/TV-512-inbjudningar commit ec932ecad69b96504b2d25f937aa4b6def646f11 Author: Erik Tiekstra Date: Fri Sep 3 16:08:22 2021 +0200 Now possible to add commaseparated list of emails --- .../employee-invite.component.html | 124 ++++++++++-------- .../employee-invite.component.scss | 35 +++-- .../employee-invite.component.ts | 89 +++++++++---- .../employee-invite/employee-invite.module.ts | 4 +- .../api/employee-invite.response.model.ts | 2 + .../shared/services/api/employee.service.ts | 6 +- .../utils/validators/email.validator.ts | 29 +++- mock-api/mina-sidor-fa/server.js | 2 + 8 files changed, 184 insertions(+), 107 deletions(-) diff --git a/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.component.html b/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.component.html index 99360f6..69489e6 100644 --- a/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.component.html +++ b/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.component.html @@ -1,63 +1,79 @@ -
- -

Skapa personalkonto

-

Så här skapar du ett personalkonto:

-
    -
  1. Skicka en inbjudningslänk till personalens e-postadress.
  2. -
  3. Personalen öppnar inbjudningslänken via sin e-post och skapar ett personalkonto med sitt Bank-ID.
  4. -
  5. - När kontot är skapat ser du det i personallistan. Det nya personalkontot saknar fortfarande behörigheter. -
  6. -
  7. - Ge personalkontot behörigheter genom att klicka på namnet i personallistan och ange vilka behörigheter - personalen ska ha. Nu kan personalen logga in och arbeta. -
  8. -
-
-
-
- + +
+
+

Skapa personalkonto

+

Så här skapar du ett personalkonto:

+
    +
  1. Skicka en inbjudningslänk till personalens e-postadress.
  2. +
  3. Personalen öppnar inbjudningslänken via sin e-post och skapar ett personalkonto med sitt Bank-ID.
  4. +
  5. + När kontot är skapat ser du det i personallistan. Det nya personalkontot saknar fortfarande behörigheter. +
  6. +
  7. + Ge personalkontot behörigheter genom att klicka på namnet i personallistan och ange vilka behörigheter + personalen ska ha. Nu kan personalen logga in och arbeta. +
  8. +
+
+ +

Skicka en inbjudningslänk

Skicka en inbjudningslänk till personalen du vill lägga till som systemanvändare. Ange personalens e-postadress nedan och tryck på skicka inbjudningslänk.

- -
- - Skicka inbjudningslänk
-
- -

Inbjudan har skickats till {{lastInvitedNewUsers.join(', ')}}.

-

- Följande personal är redan registrerat: {{lastInvitedExistingUsers.join(', ')}}. -

-
+ -
- Tillbaka till personallistan -
- -
+ + +

+ Inbjudan skickades endast till vissa mottagare. Skicka inbjudningar igen till de e-postadresser där + inbjudan misslyckades. +

+
+ +

Något gick fel. Skicka inbjudningar igen.

+
+ +

Inbjudan kunde inte skickas till:

+

{{lastInvitedFailedInvites.join(', ')}}

+
+ +

Inbjudan har skickats till:

+

{{lastInvitedNewUsers.join(', ')}}

+
+ +

E-postadressen har redan fått en inbjudan:

+

{{lastInvitedAlreadyInvitedUsers.join(', ')}}

+
+ +

E-postadressen var redan registrerad:

+

{{lastInvitedExistingUsers.join(', ')}}

+
+
+ +
+ Skicka inbjudningslänk + Tillbaka till personallistan +
+ +
+
diff --git a/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.component.scss b/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.component.scss index 8c539af..31554ff 100644 --- a/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.component.scss +++ b/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.component.scss @@ -1,31 +1,30 @@ @import 'variables/gutters'; .employee-invite { - &__block { - max-width: var(--digi--typography--text--max-width); - margin-top: $digi--layout--gutter--xl; + max-width: var(--digi--typography--text--max-width); + + &__header { + margin-bottom: $digi--layout--gutter--xxl; + } + + &__description { margin-bottom: $digi--layout--gutter--xl; } - &__input-section { - display: flex; - margin-top: $digi--layout--gutter--xl; - } - - &__input { - display: block; - min-width: 240px; - margin-bottom: var(--digi--layout--gutter); - } - &__invitation-btn { - margin-top: 31px; - margin-left: 16px; + &__alert { + h4 { + margin-bottom: 0; + margin-top: $digi--layout--gutter; + } + p { + margin: 0; + } } &__footer { margin-top: $digi--layout--gutter--xl; display: flex; - gap: var(--digi--layout--gutter); + justify-content: space-between; + align-items: center; } - } diff --git a/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.component.ts b/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.component.ts index ea961ae..7469faf 100644 --- a/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.component.ts +++ b/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.component.ts @@ -1,7 +1,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms'; +import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; import { EmployeeInviteResponse } from '@msfa-models/api/employee-invite.response.model'; import { EmployeeService } from '@msfa-services/api/employee.service'; +import { CommaSeparatedEmailValidator } from '@msfa-utils/validators/email.validator'; import { RequiredValidator } from '@msfa-utils/validators/required.validator'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -12,48 +13,84 @@ import { BehaviorSubject, Observable } from 'rxjs'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class EmployeeInviteComponent { - form: FormGroup = new FormGroup({ - email: new FormControl('', [ - RequiredValidator('E-postadress'), - Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$'), - ]), + formGroup: FormGroup = new FormGroup({ + emails: new FormControl('', [RequiredValidator('E-postadresser'), CommaSeparatedEmailValidator()]), }); - private _lastInvite$ = new BehaviorSubject(null); - lastInvite$: Observable = this._lastInvite$.asObservable(); + private _lastInvites$ = new BehaviorSubject(null); + lastInvites$: Observable = this._lastInvites$.asObservable(); constructor(private employeeService: EmployeeService) {} - get emailControl(): AbstractControl { - return this.form.get('email'); + get emailsControl(): AbstractControl { + return this.formGroup.get('emails'); } - get lastInvite(): EmployeeInviteResponse { - return this._lastInvite$.getValue(); + get lastInvites(): EmployeeInviteResponse { + return this._lastInvites$.getValue(); + } + + get emailsControlValueAsArray(): string[] { + return (this.emailsControl.value as string) + .replaceAll(' ', '') + .split(',') + .filter(email => !!email); } get lastInvitedNewUsers(): string[] { - const invitedUsers = this.lastInvite?.invitedUsers || []; - const assignedUsers = this.lastInvite?.assignedUsers || []; - return [...invitedUsers, ...assignedUsers.map(assigned => assigned.email)]; + const invitedUsers = this.lastInvites?.invitedUsers || []; + const assignedUsers = this.lastInvites?.assignedUsers || []; + return [...invitedUsers, ...assignedUsers.map(user => user.email)]; + } + get lastInvitedExistingUsers(): string[] { + const existingUsersInCurrentOrg = this.lastInvites?.existingUsersInCurrentOrg || []; + return existingUsersInCurrentOrg.map(user => user.email); + } + get lastInvitedFailedInvites(): string[] { + const failedInvites = this.lastInvites?.failedInvite || []; + return failedInvites; + } + get lastInvitedAlreadyInvitedUsers(): string[] { + const alreadyInvitedUsers = this.lastInvites?.alreadyInvitedUsers || []; + return alreadyInvitedUsers.map(user => user.email); } - get lastInvitedExistingUsers(): string[] { - const existingUsersInCurrentOrg = this.lastInvite?.existingUsersInCurrentOrg || []; - return existingUsersInCurrentOrg.map(existing => existing.email); + get alertTexts(): { variation: string; heading: string } { + if (this.lastInvitedFailedInvites.length) { + if ( + this.lastInvitedNewUsers.length || + this.lastInvitedExistingUsers.length || + this.lastInvitedAlreadyInvitedUsers.length + ) { + return { + variation: 'warning', + heading: 'Något gick fel', + }; + } + + return { + variation: 'danger', + heading: 'Inbjudningar har inte skickats!', + }; + } + + return { + variation: 'success', + heading: 'Allt gick bra', + }; } submitForm(): void { - this._lastInvite$.next(null); - if (this.form.invalid) { - this.emailControl.markAsDirty(); - this.emailControl.markAsTouched(); + this._lastInvites$.next(null); + if (this.formGroup.invalid) { + this.emailsControl.markAsDirty(); + this.emailsControl.markAsTouched(); return; } - const post = this.employeeService.postEmployeeInvitation(this.emailControl.value).subscribe({ + const post = this.employeeService.postEmployeeInvitation(this.emailsControlValueAsArray).subscribe({ next: data => { - this._lastInvite$.next(data); - this.form.reset(); + this._lastInvites$.next(data); + this.formGroup.reset(); }, complete: () => { post.unsubscribe(); @@ -62,6 +99,6 @@ export class EmployeeInviteComponent { } onCloseAlert(): void { - this._lastInvite$.next(null); + this._lastInvites$.next(null); } } diff --git a/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.module.ts b/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.module.ts index cfb1749..b414464 100644 --- a/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.module.ts +++ b/apps/mina-sidor-fa/src/app/pages/administration/pages/employee-invite/employee-invite.module.ts @@ -1,4 +1,4 @@ -import { DigiNgFormInputModule } from '@af/digi-ng/_form/form-input'; +import { DigiNgFormTextareaModule } from '@af/digi-ng/_form/form-textarea'; import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; @@ -16,7 +16,7 @@ import { EmployeeInviteComponent } from './employee-invite.component'; LayoutModule, BackLinkModule, ReactiveFormsModule, - DigiNgFormInputModule, + DigiNgFormTextareaModule, ], }) export class EmployeeInviteModule {} diff --git a/apps/mina-sidor-fa/src/app/shared/models/api/employee-invite.response.model.ts b/apps/mina-sidor-fa/src/app/shared/models/api/employee-invite.response.model.ts index a24bb4a..720bd3a 100644 --- a/apps/mina-sidor-fa/src/app/shared/models/api/employee-invite.response.model.ts +++ b/apps/mina-sidor-fa/src/app/shared/models/api/employee-invite.response.model.ts @@ -15,4 +15,6 @@ export interface EmployeeInviteResponse { assignedUsers: ExistingUser[]; invitedUsers: string[]; existingUsersInCurrentOrg: ExistingUser[]; + failedInvite: string[]; + alreadyInvitedUsers: ExistingUser[]; } diff --git a/apps/mina-sidor-fa/src/app/shared/services/api/employee.service.ts b/apps/mina-sidor-fa/src/app/shared/services/api/employee.service.ts index b980cb3..ac169e0 100644 --- a/apps/mina-sidor-fa/src/app/shared/services/api/employee.service.ts +++ b/apps/mina-sidor-fa/src/app/shared/services/api/employee.service.ts @@ -182,11 +182,9 @@ export class EmployeeService extends UnsubscribeDirective { ); } - public postEmployeeInvitation(email: string): Observable { + public postEmployeeInvitation(emails: string[]): Observable { return this.httpClient - .patch<{ data: EmployeeInviteResponse }>(`${this._apiBaseUrl}/invite`, { - emails: [email], - }) + .patch<{ data: EmployeeInviteResponse }>(`${this._apiBaseUrl}/invite`, { emails }) .pipe( take(1), map(({ data }) => data), diff --git a/apps/mina-sidor-fa/src/app/shared/utils/validators/email.validator.ts b/apps/mina-sidor-fa/src/app/shared/utils/validators/email.validator.ts index c81578d..60da590 100644 --- a/apps/mina-sidor-fa/src/app/shared/utils/validators/email.validator.ts +++ b/apps/mina-sidor-fa/src/app/shared/utils/validators/email.validator.ts @@ -1,14 +1,37 @@ import { AbstractControl, ValidatorFn } from '@angular/forms'; import { ValidationError } from '@msfa-models/validation-error.model'; -export function EmailValidator(label = 'Fältet'): ValidatorFn { - const emailRegex = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/; +const EMAIL_REGEX = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/; +export function EmailValidator(label = 'Fältet'): ValidatorFn { return (control: AbstractControl): ValidationError => { - if (control && control.value && !emailRegex.test(control.value)) { + if (control && control.value && !EMAIL_REGEX.test(control.value)) { return { type: 'invalid', message: `Ogiltig ${label}` }; } return null; }; } + +export function CommaSeparatedEmailValidator(): ValidatorFn { + return (control: AbstractControl): ValidationError => { + if (control && control.value) { + const values: string[] = (control.value as string).replaceAll(' ', '').split(','); + const invalidEmailaddresses = []; + + values.forEach(value => { + if (value && !EMAIL_REGEX.test(value)) { + invalidEmailaddresses.push(value); + } + }); + + if (invalidEmailaddresses.length) { + const messagePrepend = + invalidEmailaddresses.length > 1 ? 'Ogiltiga e-postadresser: ' : 'Ogiltig e-postadress: '; + return { type: 'invalid', message: `${messagePrepend}${invalidEmailaddresses.join(', ')}` }; + } + } + + return null; + }; +} diff --git a/mock-api/mina-sidor-fa/server.js b/mock-api/mina-sidor-fa/server.js index ceb0fe6..f21166e 100644 --- a/mock-api/mina-sidor-fa/server.js +++ b/mock-api/mina-sidor-fa/server.js @@ -62,6 +62,8 @@ router.render = (req, res) => { invitedUsers: req.body.emails, assignedUsers: [], existingUsersInCurrentOrg: [], + failedInvite: [], + alreadyInvitedUsers: [], }, }); }