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,6 +1,7 @@
|
|||||||
<msfa-layout>
|
<msfa-layout>
|
||||||
<section class="employee-invite">
|
|
||||||
<digi-typography>
|
<digi-typography>
|
||||||
|
<section class="employee-invite">
|
||||||
|
<header class="employee-invite__header">
|
||||||
<h1>Skapa personalkonto</h1>
|
<h1>Skapa personalkonto</h1>
|
||||||
<p>Så här skapar du ett personalkonto:</p>
|
<p>Så här skapar du ett personalkonto:</p>
|
||||||
<ol>
|
<ol>
|
||||||
@@ -14,50 +15,65 @@
|
|||||||
personalen ska ha. Nu kan personalen logga in och arbeta.
|
personalen ska ha. Nu kan personalen logga in och arbeta.
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</digi-typography>
|
</header>
|
||||||
<form [formGroup]="form" (ngSubmit)="submitForm()">
|
<form [formGroup]="formGroup" (ngSubmit)="submitForm()">
|
||||||
<div class="employee-invite__block">
|
<div class="employee-invite__description">
|
||||||
<digi-typography>
|
|
||||||
<h2>Skicka en inbjudningslänk</h2>
|
<h2>Skicka en inbjudningslänk</h2>
|
||||||
<p>
|
<p>
|
||||||
Skicka en inbjudningslänk till personalen du vill lägga till som systemanvändare. Ange personalens
|
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.
|
e-postadress nedan och tryck på skicka inbjudningslänk.
|
||||||
</p>
|
</p>
|
||||||
</digi-typography>
|
</div>
|
||||||
<div class="employee-invite__input-section">
|
<digi-ng-form-textarea
|
||||||
<digi-ng-form-input
|
formControlName="emails"
|
||||||
afId="employee-invite-email"
|
afSize="m"
|
||||||
class="employee-invite__input"
|
afDescription="Fältet accepterar kommaseparera mailadresserna för att skicka inbjudan till flera e-postadresser i taget."
|
||||||
formControlName="email"
|
|
||||||
afLabel="E-postadress"
|
|
||||||
afType="email"
|
|
||||||
[afRequired]="true"
|
[afRequired]="true"
|
||||||
[afInvalidMessage]="emailControl.errors?.message || 'Ogiltig e-postadress'"
|
|
||||||
[afDisableValidStyle]="true"
|
[afDisableValidStyle]="true"
|
||||||
[afInvalid]="emailControl.invalid && emailControl.dirty"
|
[afInvalidMessage]="emailsControl.errors?.message || 'Ogiltig e-postadress'"
|
||||||
></digi-ng-form-input>
|
[afInvalid]="emailsControl.invalid && emailsControl.dirty"
|
||||||
<digi-button af-size="m" af-type="submit" class="employee-invite__invitation-btn"
|
afLabel="E-postadresser"
|
||||||
>Skicka inbjudningslänk</digi-button
|
></digi-ng-form-textarea>
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<digi-notification-alert
|
<digi-notification-alert
|
||||||
*ngIf="(lastInvite$ | async) as lastInvite"
|
*ngIf="(lastInvites$ | async) && !emailsControl.dirty"
|
||||||
af-variation="success"
|
class="employee-invite__alert"
|
||||||
af-heading="Allt gick bra"
|
[afVariation]="alertTexts.variation"
|
||||||
|
[afHeading]="alertTexts.heading"
|
||||||
af-heading-level="h3"
|
af-heading-level="h3"
|
||||||
af-closeable="true"
|
|
||||||
(click)="onCloseAlert()"
|
|
||||||
>
|
>
|
||||||
<p *ngIf="lastInvitedNewUsers?.length">Inbjudan har skickats till {{lastInvitedNewUsers.join(', ')}}.</p>
|
<ng-container *ngIf="alertTexts.variation === 'warning'">
|
||||||
<p *ngIf="lastInvitedExistingUsers?.length">
|
<p>
|
||||||
Följande personal är redan registrerat: {{lastInvitedExistingUsers.join(', ')}}.
|
Inbjudan skickades endast till vissa mottagare. Skicka inbjudningar igen till de e-postadresser där
|
||||||
|
inbjudan misslyckades.
|
||||||
</p>
|
</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>
|
</digi-notification-alert>
|
||||||
|
|
||||||
<footer class="employee-invite__footer">
|
<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>
|
<msfa-back-link [route]="['/administration/personal']">Tillbaka till personallistan</msfa-back-link>
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
</digi-typography>
|
||||||
</msfa-layout>
|
</msfa-layout>
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
@import 'variables/gutters';
|
@import 'variables/gutters';
|
||||||
|
|
||||||
.employee-invite {
|
.employee-invite {
|
||||||
&__block {
|
|
||||||
max-width: var(--digi--typography--text--max-width);
|
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;
|
margin-bottom: $digi--layout--gutter--xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input-section {
|
&__alert {
|
||||||
display: flex;
|
h4 {
|
||||||
margin-top: $digi--layout--gutter--xl;
|
margin-bottom: 0;
|
||||||
|
margin-top: $digi--layout--gutter;
|
||||||
}
|
}
|
||||||
|
p {
|
||||||
&__input {
|
margin: 0;
|
||||||
display: block;
|
|
||||||
min-width: 240px;
|
|
||||||
margin-bottom: var(--digi--layout--gutter);
|
|
||||||
}
|
}
|
||||||
&__invitation-btn {
|
|
||||||
margin-top: 31px;
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
margin-top: $digi--layout--gutter--xl;
|
margin-top: $digi--layout--gutter--xl;
|
||||||
display: flex;
|
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 { 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 { EmployeeInviteResponse } from '@msfa-models/api/employee-invite.response.model';
|
||||||
import { EmployeeService } from '@msfa-services/api/employee.service';
|
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 { RequiredValidator } from '@msfa-utils/validators/required.validator';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
@@ -12,48 +13,84 @@ import { BehaviorSubject, Observable } from 'rxjs';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class EmployeeInviteComponent {
|
export class EmployeeInviteComponent {
|
||||||
form: FormGroup = new FormGroup({
|
formGroup: FormGroup = new FormGroup({
|
||||||
email: new FormControl('', [
|
emails: new FormControl('', [RequiredValidator('E-postadresser'), CommaSeparatedEmailValidator()]),
|
||||||
RequiredValidator('E-postadress'),
|
|
||||||
Validators.pattern('^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$'),
|
|
||||||
]),
|
|
||||||
});
|
});
|
||||||
private _lastInvite$ = new BehaviorSubject<EmployeeInviteResponse>(null);
|
private _lastInvites$ = new BehaviorSubject<EmployeeInviteResponse>(null);
|
||||||
lastInvite$: Observable<EmployeeInviteResponse> = this._lastInvite$.asObservable();
|
lastInvites$: Observable<EmployeeInviteResponse> = this._lastInvites$.asObservable();
|
||||||
|
|
||||||
constructor(private employeeService: EmployeeService) {}
|
constructor(private employeeService: EmployeeService) {}
|
||||||
|
|
||||||
get emailControl(): AbstractControl {
|
get emailsControl(): AbstractControl {
|
||||||
return this.form.get('email');
|
return this.formGroup.get('emails');
|
||||||
}
|
}
|
||||||
|
|
||||||
get lastInvite(): EmployeeInviteResponse {
|
get lastInvites(): EmployeeInviteResponse {
|
||||||
return this._lastInvite$.getValue();
|
return this._lastInvites$.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
get emailsControlValueAsArray(): string[] {
|
||||||
|
return (this.emailsControl.value as string)
|
||||||
|
.replaceAll(' ', '')
|
||||||
|
.split(',')
|
||||||
|
.filter(email => !!email);
|
||||||
}
|
}
|
||||||
|
|
||||||
get lastInvitedNewUsers(): string[] {
|
get lastInvitedNewUsers(): string[] {
|
||||||
const invitedUsers = this.lastInvite?.invitedUsers || [];
|
const invitedUsers = this.lastInvites?.invitedUsers || [];
|
||||||
const assignedUsers = this.lastInvite?.assignedUsers || [];
|
const assignedUsers = this.lastInvites?.assignedUsers || [];
|
||||||
return [...invitedUsers, ...assignedUsers.map(assigned => assigned.email)];
|
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[] {
|
get alertTexts(): { variation: string; heading: string } {
|
||||||
const existingUsersInCurrentOrg = this.lastInvite?.existingUsersInCurrentOrg || [];
|
if (this.lastInvitedFailedInvites.length) {
|
||||||
return existingUsersInCurrentOrg.map(existing => existing.email);
|
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 {
|
submitForm(): void {
|
||||||
this._lastInvite$.next(null);
|
this._lastInvites$.next(null);
|
||||||
if (this.form.invalid) {
|
if (this.formGroup.invalid) {
|
||||||
this.emailControl.markAsDirty();
|
this.emailsControl.markAsDirty();
|
||||||
this.emailControl.markAsTouched();
|
this.emailsControl.markAsTouched();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = this.employeeService.postEmployeeInvitation(this.emailControl.value).subscribe({
|
const post = this.employeeService.postEmployeeInvitation(this.emailsControlValueAsArray).subscribe({
|
||||||
next: data => {
|
next: data => {
|
||||||
this._lastInvite$.next(data);
|
this._lastInvites$.next(data);
|
||||||
this.form.reset();
|
this.formGroup.reset();
|
||||||
},
|
},
|
||||||
complete: () => {
|
complete: () => {
|
||||||
post.unsubscribe();
|
post.unsubscribe();
|
||||||
@@ -62,6 +99,6 @@ export class EmployeeInviteComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onCloseAlert(): void {
|
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 { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
@@ -16,7 +16,7 @@ import { EmployeeInviteComponent } from './employee-invite.component';
|
|||||||
LayoutModule,
|
LayoutModule,
|
||||||
BackLinkModule,
|
BackLinkModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
DigiNgFormInputModule,
|
DigiNgFormTextareaModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class EmployeeInviteModule {}
|
export class EmployeeInviteModule {}
|
||||||
|
|||||||
@@ -15,4 +15,6 @@ export interface EmployeeInviteResponse {
|
|||||||
assignedUsers: ExistingUser[];
|
assignedUsers: ExistingUser[];
|
||||||
invitedUsers: string[];
|
invitedUsers: string[];
|
||||||
existingUsersInCurrentOrg: ExistingUser[];
|
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
|
return this.httpClient
|
||||||
.patch<{ data: EmployeeInviteResponse }>(`${this._apiBaseUrl}/invite`, {
|
.patch<{ data: EmployeeInviteResponse }>(`${this._apiBaseUrl}/invite`, { emails })
|
||||||
emails: [email],
|
|
||||||
})
|
|
||||||
.pipe(
|
.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
map(({ data }) => data),
|
map(({ data }) => data),
|
||||||
|
|||||||
@@ -1,14 +1,37 @@
|
|||||||
import { AbstractControl, ValidatorFn } from '@angular/forms';
|
import { AbstractControl, ValidatorFn } from '@angular/forms';
|
||||||
import { ValidationError } from '@msfa-models/validation-error.model';
|
import { ValidationError } from '@msfa-models/validation-error.model';
|
||||||
|
|
||||||
export function EmailValidator(label = 'Fältet'): ValidatorFn {
|
const EMAIL_REGEX = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/;
|
||||||
const emailRegex = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/;
|
|
||||||
|
|
||||||
|
export function EmailValidator(label = 'Fältet'): ValidatorFn {
|
||||||
return (control: AbstractControl): ValidationError => {
|
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 { type: 'invalid', message: `Ogiltig ${label}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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,
|
invitedUsers: req.body.emails,
|
||||||
assignedUsers: [],
|
assignedUsers: [],
|
||||||
existingUsersInCurrentOrg: [],
|
existingUsersInCurrentOrg: [],
|
||||||
|
failedInvite: [],
|
||||||
|
alreadyInvitedUsers: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user