feat(employee): Added functionality to invite multiple emailaddresses. (TV-512)

Squashed commit of the following:

commit 04baa8e9398016ffb0cba618a5b857230dc4ea9e
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Sep 6 10:52:12 2021 +0200

    moved email-regex

commit 0f54392c2e65386f4d0ae79493f6d33d84e313fe
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Sep 6 07:54:13 2021 +0200

    Updated confirmation texts

commit 4557c8203cf826caccff7ac174e15b547f041993
Merge: ec932ec ec7b4fc
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Mon Sep 6 07:37:56 2021 +0200

    Merge branch 'develop' into feature/TV-512-inbjudningar

commit ec932ecad69b96504b2d25f937aa4b6def646f11
Author: Erik Tiekstra <erik.tiekstra@arbetsformedlingen.se>
Date:   Fri Sep 3 16:08:22 2021 +0200

    Now possible to add commaseparated list of emails
This commit is contained in:
Erik Tiekstra
2021-09-06 10:53:25 +02:00
parent ec7b4fcd8e
commit 9253edfe62
8 changed files with 184 additions and 107 deletions

View File

@@ -1,6 +1,7 @@
<msfa-layout>
<section class="employee-invite">
<digi-typography>
<section class="employee-invite">
<header class="employee-invite__header">
<h1>Skapa personalkonto</h1>
<p>Så här skapar du ett personalkonto:</p>
<ol>
@@ -14,50 +15,65 @@
personalen ska ha. Nu kan personalen logga in och arbeta.
</li>
</ol>
</digi-typography>
<form [formGroup]="form" (ngSubmit)="submitForm()">
<div class="employee-invite__block">
<digi-typography>
</header>
<form [formGroup]="formGroup" (ngSubmit)="submitForm()">
<div class="employee-invite__description">
<h2>Skicka en inbjudningslänk</h2>
<p>
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.
</p>
</digi-typography>
<div class="employee-invite__input-section">
<digi-ng-form-input
afId="employee-invite-email"
class="employee-invite__input"
formControlName="email"
afLabel="E-postadress"
afType="email"
</div>
<digi-ng-form-textarea
formControlName="emails"
afSize="m"
afDescription="Fältet accepterar kommaseparera mailadresserna för att skicka inbjudan till flera e-postadresser i taget."
[afRequired]="true"
[afInvalidMessage]="emailControl.errors?.message || 'Ogiltig e-postadress'"
[afDisableValidStyle]="true"
[afInvalid]="emailControl.invalid && emailControl.dirty"
></digi-ng-form-input>
<digi-button af-size="m" af-type="submit" class="employee-invite__invitation-btn"
>Skicka inbjudningslänk</digi-button
>
</div>
</div>
[afInvalidMessage]="emailsControl.errors?.message || 'Ogiltig e-postadress'"
[afInvalid]="emailsControl.invalid && emailsControl.dirty"
afLabel="E-postadresser"
></digi-ng-form-textarea>
<digi-notification-alert
*ngIf="(lastInvite$ | async) as lastInvite"
af-variation="success"
af-heading="Allt gick bra"
*ngIf="(lastInvites$ | async) && !emailsControl.dirty"
class="employee-invite__alert"
[afVariation]="alertTexts.variation"
[afHeading]="alertTexts.heading"
af-heading-level="h3"
af-closeable="true"
(click)="onCloseAlert()"
>
<p *ngIf="lastInvitedNewUsers?.length">Inbjudan har skickats till {{lastInvitedNewUsers.join(', ')}}.</p>
<p *ngIf="lastInvitedExistingUsers?.length">
Följande personal är redan registrerat: {{lastInvitedExistingUsers.join(', ')}}.
<ng-container *ngIf="alertTexts.variation === 'warning'">
<p>
Inbjudan skickades endast till vissa mottagare. Skicka inbjudningar igen till de e-postadresser där
inbjudan misslyckades.
</p>
</ng-container>
<ng-container *ngIf="alertTexts.variation === 'danger'">
<p>Något gick fel. Skicka inbjudningar igen.</p>
</ng-container>
<ng-container *ngIf="lastInvitedFailedInvites.length">
<h4>Inbjudan kunde inte skickas till:</h4>
<p>{{lastInvitedFailedInvites.join(', ')}}</p>
</ng-container>
<ng-container *ngIf="lastInvitedNewUsers.length">
<h4>Inbjudan har skickats till:</h4>
<p>{{lastInvitedNewUsers.join(', ')}}</p>
</ng-container>
<ng-container *ngIf="lastInvitedAlreadyInvitedUsers.length">
<h4>E-postadressen har redan fått en inbjudan:</h4>
<p>{{lastInvitedAlreadyInvitedUsers.join(', ')}}</p>
</ng-container>
<ng-container *ngIf="lastInvitedExistingUsers.length">
<h4>E-postadressen var redan registrerad:</h4>
<p>{{lastInvitedExistingUsers.join(', ')}}</p>
</ng-container>
</digi-notification-alert>
<footer class="employee-invite__footer">
<digi-button af-size="m" af-type="submit"> Skicka inbjudningslänk </digi-button>
<msfa-back-link [route]="['/administration/personal']">Tillbaka till personallistan</msfa-back-link>
</footer>
</form>
</section>
</digi-typography>
</msfa-layout>

View File

@@ -1,31 +1,30 @@
@import 'variables/gutters';
.employee-invite {
&__block {
max-width: var(--digi--typography--text--max-width);
margin-top: $digi--layout--gutter--xl;
&__header {
margin-bottom: $digi--layout--gutter--xxl;
}
&__description {
margin-bottom: $digi--layout--gutter--xl;
}
&__input-section {
display: flex;
margin-top: $digi--layout--gutter--xl;
&__alert {
h4 {
margin-bottom: 0;
margin-top: $digi--layout--gutter;
}
&__input {
display: block;
min-width: 240px;
margin-bottom: var(--digi--layout--gutter);
p {
margin: 0;
}
&__invitation-btn {
margin-top: 31px;
margin-left: 16px;
}
&__footer {
margin-top: $digi--layout--gutter--xl;
display: flex;
gap: var(--digi--layout--gutter);
justify-content: space-between;
align-items: center;
}
}

View File

@@ -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<EmployeeInviteResponse>(null);
lastInvite$: Observable<EmployeeInviteResponse> = this._lastInvite$.asObservable();
private _lastInvites$ = new BehaviorSubject<EmployeeInviteResponse>(null);
lastInvites$: Observable<EmployeeInviteResponse> = 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);
}
}

View File

@@ -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 {}

View File

@@ -15,4 +15,6 @@ export interface EmployeeInviteResponse {
assignedUsers: ExistingUser[];
invitedUsers: string[];
existingUsersInCurrentOrg: ExistingUser[];
failedInvite: string[];
alreadyInvitedUsers: ExistingUser[];
}

View File

@@ -182,11 +182,9 @@ export class EmployeeService extends UnsubscribeDirective {
);
}
public postEmployeeInvitation(email: string): Observable<EmployeeInviteResponse> {
public postEmployeeInvitation(emails: string[]): Observable<EmployeeInviteResponse> {
return this.httpClient
.patch<{ data: EmployeeInviteResponse }>(`${this._apiBaseUrl}/invite`, {
emails: [email],
})
.patch<{ data: EmployeeInviteResponse }>(`${this._apiBaseUrl}/invite`, { emails })
.pipe(
take(1),
map(({ data }) => data),

View File

@@ -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;
};
}

View File

@@ -62,6 +62,8 @@ router.render = (req, res) => {
invitedUsers: req.body.emails,
assignedUsers: [],
existingUsersInCurrentOrg: [],
failedInvite: [],
alreadyInvitedUsers: [],
},
});
}