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:
@@ -1,63 +1,79 @@
|
||||
<msfa-layout>
|
||||
<section class="employee-invite">
|
||||
<digi-typography>
|
||||
<h1>Skapa personalkonto</h1>
|
||||
<p>Så här skapar du ett personalkonto:</p>
|
||||
<ol>
|
||||
<li>Skicka en inbjudningslänk till personalens e-postadress.</li>
|
||||
<li>Personalen öppnar inbjudningslänken via sin e-post och skapar ett personalkonto med sitt Bank-ID.</li>
|
||||
<li>
|
||||
När kontot är skapat ser du det i personallistan. Det nya personalkontot saknar fortfarande behörigheter.
|
||||
</li>
|
||||
<li>
|
||||
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.
|
||||
</li>
|
||||
</ol>
|
||||
</digi-typography>
|
||||
<form [formGroup]="form" (ngSubmit)="submitForm()">
|
||||
<div class="employee-invite__block">
|
||||
<digi-typography>
|
||||
<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>
|
||||
<li>Skicka en inbjudningslänk till personalens e-postadress.</li>
|
||||
<li>Personalen öppnar inbjudningslänken via sin e-post och skapar ett personalkonto med sitt Bank-ID.</li>
|
||||
<li>
|
||||
När kontot är skapat ser du det i personallistan. Det nya personalkontot saknar fortfarande behörigheter.
|
||||
</li>
|
||||
<li>
|
||||
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.
|
||||
</li>
|
||||
</ol>
|
||||
</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"
|
||||
[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>
|
||||
<digi-notification-alert
|
||||
*ngIf="(lastInvite$ | async) as lastInvite"
|
||||
af-variation="success"
|
||||
af-heading="Allt gick bra"
|
||||
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(', ')}}.
|
||||
</p>
|
||||
</digi-notification-alert>
|
||||
<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"
|
||||
[afDisableValidStyle]="true"
|
||||
[afInvalidMessage]="emailsControl.errors?.message || 'Ogiltig e-postadress'"
|
||||
[afInvalid]="emailsControl.invalid && emailsControl.dirty"
|
||||
afLabel="E-postadresser"
|
||||
></digi-ng-form-textarea>
|
||||
|
||||
<footer class="employee-invite__footer">
|
||||
<msfa-back-link [route]="['/administration/personal']">Tillbaka till personallistan</msfa-back-link>
|
||||
</footer>
|
||||
</form>
|
||||
</section>
|
||||
<digi-notification-alert
|
||||
*ngIf="(lastInvites$ | async) && !emailsControl.dirty"
|
||||
class="employee-invite__alert"
|
||||
[afVariation]="alertTexts.variation"
|
||||
[afHeading]="alertTexts.heading"
|
||||
af-heading-level="h3"
|
||||
>
|
||||
<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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -15,4 +15,6 @@ export interface EmployeeInviteResponse {
|
||||
assignedUsers: ExistingUser[];
|
||||
invitedUsers: string[];
|
||||
existingUsersInCurrentOrg: ExistingUser[];
|
||||
failedInvite: string[];
|
||||
alreadyInvitedUsers: ExistingUser[];
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ router.render = (req, res) => {
|
||||
invitedUsers: req.body.emails,
|
||||
assignedUsers: [],
|
||||
existingUsersInCurrentOrg: [],
|
||||
failedInvite: [],
|
||||
alreadyInvitedUsers: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user