From 87ca431ab743b0accb93aed73a40498b861fd0ad Mon Sep 17 00:00:00 2001 From: Janik Endtner Date: Fri, 2 Feb 2024 08:31:58 +0100 Subject: [PATCH 01/17] #790 create dialog to invite members --- .../src/app/shared/types/model/NewUser.ts | 5 ++ .../invite-user-dialog.component.html | 47 +++++++++++ .../invite-user-dialog.component.scss | 0 .../invite-user-dialog.component.spec.ts | 54 +++++++++++++ .../invite-user-dialog.component.ts | 31 +++++++ .../member-detail/member-detail.component.ts | 5 +- .../member-list/member-list.component.html | 13 +++ .../member-list/member-list.component.ts | 7 +- .../new-user/new-user.component.html | 81 +++++++++++++++++++ .../new-user/new-user.component.scss | 16 ++++ .../new-user/new-user.component.spec.ts | 44 ++++++++++ .../new-user/new-user.component.ts | 46 +++++++++++ .../team-management/team-management.module.ts | 4 + 13 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/shared/types/model/NewUser.ts create mode 100644 frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html create mode 100644 frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.scss create mode 100644 frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts create mode 100644 frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts create mode 100644 frontend/src/app/team-management/new-user/new-user.component.html create mode 100644 frontend/src/app/team-management/new-user/new-user.component.scss create mode 100644 frontend/src/app/team-management/new-user/new-user.component.spec.ts create mode 100644 frontend/src/app/team-management/new-user/new-user.component.ts diff --git a/frontend/src/app/shared/types/model/NewUser.ts b/frontend/src/app/shared/types/model/NewUser.ts new file mode 100644 index 0000000000..f793306897 --- /dev/null +++ b/frontend/src/app/shared/types/model/NewUser.ts @@ -0,0 +1,5 @@ +export interface NewUser { + firstname: string; + lastname: string; + email: string; +} diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html new file mode 100644 index 0000000000..96ca11fd72 --- /dev/null +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html @@ -0,0 +1,47 @@ +
+
+ + Members einladen + +
+
+ +
+
+ @for (user of users; track $index) { +
+ +
+ } +
+ +
+
+
+
+ +
+
+ + +
+
+
diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.scss b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts new file mode 100644 index 0000000000..7c9dbd7579 --- /dev/null +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts @@ -0,0 +1,54 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { InviteUserDialogComponent } from "./invite-user-dialog.component"; +import { MatDialogModule } from "@angular/material/dialog"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { NewUserComponent } from "../new-user/new-user.component"; +import { PuzzleIconComponent } from "../../shared/custom/puzzle-icon/puzzle-icon.component"; +import { PuzzleIconButtonComponent } from "../../shared/custom/puzzle-icon-button/puzzle-icon-button.component"; + +describe("InviteUserDialogComponent", () => { + let component: InviteUserDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + InviteUserDialogComponent, + NewUserComponent, + PuzzleIconComponent, + PuzzleIconButtonComponent + ], + imports: [ + MatDialogModule, + FormsModule, + ReactiveFormsModule + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(InviteUserDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it('addUser should add a user to the existing users', () => { + component.addUser(); + expect(component.users.length).toBe(2); + }) + + it('removeUser should remove given user from users array', () => { + const user1 = {firstname: 'user1', lastname: '1user', email: 'user1@user.ch'}; + const user2 = {firstname: 'user2', lastname: '2user', email: 'user2@user.ch'}; + const user3 = {firstname: 'user3', lastname: '3user', email: 'user3@user.ch'}; + component.users = [user1, user2, user3]; + + component.removeUser(user2); + + expect(component.users).toStrictEqual([user1, user3]); + }) +}); diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts new file mode 100644 index 0000000000..0d30cb35a0 --- /dev/null +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts @@ -0,0 +1,31 @@ +import { Component, ViewChild } from "@angular/core"; +import { NewUser } from "../../shared/types/model/NewUser"; +import { NgForm } from "@angular/forms"; + +@Component({ + selector: 'app-invite-user-dialog', + templateUrl: './invite-user-dialog.component.html', + styleUrl: './invite-user-dialog.component.scss' +}) +export class InviteUserDialogComponent { + + @ViewChild('form') form!: NgForm; + + private readonly emptyUser = {firstname: '', lastname: '', email: ''}; + + users: NewUser[] = [{...this.emptyUser}]; + + constructor() { + } + + inviteUsers() { + } + + addUser() { + this.users.push({...this.emptyUser}); + } + + removeUser(user: NewUser) { + this.users = this.users.filter(u => u !== user); + } +} diff --git a/frontend/src/app/team-management/member-detail/member-detail.component.ts b/frontend/src/app/team-management/member-detail/member-detail.component.ts index 7cb856e719..d6a2127e68 100644 --- a/frontend/src/app/team-management/member-detail/member-detail.component.ts +++ b/frontend/src/app/team-management/member-detail/member-detail.component.ts @@ -91,7 +91,10 @@ export class MemberDetailComponent implements OnInit, OnDestroy { filter((confirm) => confirm), mergeMap(() => this.teamService.removeUserFromTeam(user.id, userTeam.team)), ) - .subscribe(() => this.loadUser(user.id)); + .subscribe(() => { + this.loadUser(user.id); + this.userService.reloadUsers(); + }); } updateTeamMembership(isAdmin: boolean, userTeam: UserTeam, user: User) { diff --git a/frontend/src/app/team-management/member-list/member-list.component.html b/frontend/src/app/team-management/member-list/member-list.component.html index c7dc7901bb..468b248bb4 100644 --- a/frontend/src/app/team-management/member-list/member-list.component.html +++ b/frontend/src/app/team-management/member-list/member-list.component.html @@ -31,6 +31,19 @@

Alle Teams

Member hinzufügen + diff --git a/frontend/src/app/team-management/new-user/new-user.component.scss b/frontend/src/app/team-management/new-user/new-user.component.scss new file mode 100644 index 0000000000..5c5d371ae6 --- /dev/null +++ b/frontend/src/app/team-management/new-user/new-user.component.scss @@ -0,0 +1,16 @@ +:host { + width: 100%; + > div { + gap: .5rem; + } +} +.invite-col { + flex: 1 0 0; + + label { + margin-bottom: .75rem; + } +} +.delete { + display: flex; +} diff --git a/frontend/src/app/team-management/new-user/new-user.component.spec.ts b/frontend/src/app/team-management/new-user/new-user.component.spec.ts new file mode 100644 index 0000000000..ee2250502a --- /dev/null +++ b/frontend/src/app/team-management/new-user/new-user.component.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { NewUserComponent } from "./new-user.component"; +import { FormsModule, NgForm, ReactiveFormsModule } from "@angular/forms"; +import { SharedModule } from "../../shared/shared.module"; +import { PuzzleIconButtonComponent } from "../../shared/custom/puzzle-icon-button/puzzle-icon-button.component"; +import { PuzzleIconComponent } from "../../shared/custom/puzzle-icon/puzzle-icon.component"; +import { CommonModule } from "@angular/common"; + +describe("NewUserComponent", () => { + let component: NewUserComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + NewUserComponent, + PuzzleIconButtonComponent, + PuzzleIconComponent + ], + imports: [ + FormsModule, + ReactiveFormsModule, + SharedModule, + CommonModule + ], + providers: [ + NgForm + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(NewUserComponent); + component = fixture.componentInstance; + + component.user = {firstname: '', lastname: '', email: ''}; + + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/team-management/new-user/new-user.component.ts b/frontend/src/app/team-management/new-user/new-user.component.ts new file mode 100644 index 0000000000..4c7c41009f --- /dev/null +++ b/frontend/src/app/team-management/new-user/new-user.component.ts @@ -0,0 +1,46 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, + ViewChild +} from "@angular/core"; +import { NewUser } from "../../shared/types/model/NewUser"; +import { ControlContainer, NgForm, NgModel } from "@angular/forms"; + +@Component({ + selector: 'app-new-user', + templateUrl: './new-user.component.html', + styleUrl: './new-user.component.scss', + viewProviders: [{provide: ControlContainer, useExisting: NgForm}], + changeDetection: ChangeDetectionStrategy.Default +}) +export class NewUserComponent implements AfterViewInit{ + + randNr = Math.round(Math.random()*10000) + + @Input({required: true}) + index!: number; + + @Input({required: true}) + user!: NewUser + + @Output() + removeUser: EventEmitter = new EventEmitter(); + + @ViewChild('firstInput') firstInput: any; + + ngAfterViewInit(): void { + this.firstInput.nativeElement.focus(); + } + + showError(firstName: NgModel) { + return firstName.invalid && (firstName.dirty || firstName.touched) + } + + remove() { + this.removeUser.emit(); + } +} diff --git a/frontend/src/app/team-management/team-management.module.ts b/frontend/src/app/team-management/team-management.module.ts index 196019e9da..03a57af44e 100644 --- a/frontend/src/app/team-management/team-management.module.ts +++ b/frontend/src/app/team-management/team-management.module.ts @@ -38,6 +38,8 @@ import { ShowEditRoleComponent } from './show-edit-role/show-edit-role.component import { MatCheckboxModule } from '@angular/material/checkbox'; import { EditOkrChampionComponent } from './edit-okr-champion/edit-okr-champion.component'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { NewUserComponent } from './new-user/new-user.component'; +import { InviteUserDialogComponent } from './invite-user-dialog/invite-user-dialog.component'; @NgModule({ declarations: [ @@ -60,6 +62,8 @@ import { MatTooltipModule } from '@angular/material/tooltip'; PuzzleIconButtonComponent, ShowEditRoleComponent, EditOkrChampionComponent, + NewUserComponent, + InviteUserDialogComponent, ], imports: [ CommonModule, From 13cbb67e741dcd34c4d75a166dd405a4d9ba0c68 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 2 Feb 2024 07:36:11 +0000 Subject: [PATCH 02/17] [FM] Automated formating frontend --- .../invite-user-dialog.component.html | 29 ++++-------- .../invite-user-dialog.component.spec.ts | 44 +++++++------------ .../invite-user-dialog.component.ts | 23 +++++----- .../new-user/new-user.component.html | 10 ++++- .../new-user/new-user.component.scss | 4 +- .../new-user/new-user.component.spec.ts | 40 ++++++----------- .../new-user/new-user.component.ts | 25 +++++------ 7 files changed, 72 insertions(+), 103 deletions(-) diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html index 96ca11fd72..6278006215 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html @@ -1,8 +1,8 @@
- - Members einladen - + + Members einladen +
@@ -14,16 +14,11 @@ }
-
@@ -32,13 +27,7 @@
- diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts index 7c9dbd7579..394b706aaf 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts @@ -1,54 +1,44 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { InviteUserDialogComponent } from "./invite-user-dialog.component"; -import { MatDialogModule } from "@angular/material/dialog"; -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { NewUserComponent } from "../new-user/new-user.component"; -import { PuzzleIconComponent } from "../../shared/custom/puzzle-icon/puzzle-icon.component"; -import { PuzzleIconButtonComponent } from "../../shared/custom/puzzle-icon-button/puzzle-icon-button.component"; +import { InviteUserDialogComponent } from './invite-user-dialog.component'; +import { MatDialogModule } from '@angular/material/dialog'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { NewUserComponent } from '../new-user/new-user.component'; +import { PuzzleIconComponent } from '../../shared/custom/puzzle-icon/puzzle-icon.component'; +import { PuzzleIconButtonComponent } from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; -describe("InviteUserDialogComponent", () => { +describe('InviteUserDialogComponent', () => { let component: InviteUserDialogComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - InviteUserDialogComponent, - NewUserComponent, - PuzzleIconComponent, - PuzzleIconButtonComponent - ], - imports: [ - MatDialogModule, - FormsModule, - ReactiveFormsModule - ] - }) - .compileComponents(); + declarations: [InviteUserDialogComponent, NewUserComponent, PuzzleIconComponent, PuzzleIconButtonComponent], + imports: [MatDialogModule, FormsModule, ReactiveFormsModule], + }).compileComponents(); fixture = TestBed.createComponent(InviteUserDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it("should create", () => { + it('should create', () => { expect(component).toBeTruthy(); }); it('addUser should add a user to the existing users', () => { component.addUser(); expect(component.users.length).toBe(2); - }) + }); it('removeUser should remove given user from users array', () => { - const user1 = {firstname: 'user1', lastname: '1user', email: 'user1@user.ch'}; - const user2 = {firstname: 'user2', lastname: '2user', email: 'user2@user.ch'}; - const user3 = {firstname: 'user3', lastname: '3user', email: 'user3@user.ch'}; + const user1 = { firstname: 'user1', lastname: '1user', email: 'user1@user.ch' }; + const user2 = { firstname: 'user2', lastname: '2user', email: 'user2@user.ch' }; + const user3 = { firstname: 'user3', lastname: '3user', email: 'user3@user.ch' }; component.users = [user1, user2, user3]; component.removeUser(user2); expect(component.users).toStrictEqual([user1, user3]); - }) + }); }); diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts index 0d30cb35a0..a292c22e0f 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts @@ -1,31 +1,28 @@ -import { Component, ViewChild } from "@angular/core"; -import { NewUser } from "../../shared/types/model/NewUser"; -import { NgForm } from "@angular/forms"; +import { Component, ViewChild } from '@angular/core'; +import { NewUser } from '../../shared/types/model/NewUser'; +import { NgForm } from '@angular/forms'; @Component({ selector: 'app-invite-user-dialog', templateUrl: './invite-user-dialog.component.html', - styleUrl: './invite-user-dialog.component.scss' + styleUrl: './invite-user-dialog.component.scss', }) export class InviteUserDialogComponent { - @ViewChild('form') form!: NgForm; - private readonly emptyUser = {firstname: '', lastname: '', email: ''}; + private readonly emptyUser = { firstname: '', lastname: '', email: '' }; - users: NewUser[] = [{...this.emptyUser}]; + users: NewUser[] = [{ ...this.emptyUser }]; - constructor() { - } + constructor() {} - inviteUsers() { - } + inviteUsers() {} addUser() { - this.users.push({...this.emptyUser}); + this.users.push({ ...this.emptyUser }); } removeUser(user: NewUser) { - this.users = this.users.filter(u => u !== user); + this.users = this.users.filter((u) => u !== user); } } diff --git a/frontend/src/app/team-management/new-user/new-user.component.html b/frontend/src/app/team-management/new-user/new-user.component.html index daa4a4cf49..613bfc28a4 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.html +++ b/frontend/src/app/team-management/new-user/new-user.component.html @@ -13,7 +13,8 @@ #firstName="ngModel" [(ngModel)]="user.firstname" [id]="'firstName-col_' + index" - [name]="'firstName-col_' + index" /> + [name]="'firstName-col_' + index" + /> @if (showError(firstName)) { @if (firstName.invalid) { @@ -75,7 +76,12 @@ @if (index === 0) { } - +
diff --git a/frontend/src/app/team-management/new-user/new-user.component.scss b/frontend/src/app/team-management/new-user/new-user.component.scss index 5c5d371ae6..4d6c765c77 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.scss +++ b/frontend/src/app/team-management/new-user/new-user.component.scss @@ -1,14 +1,14 @@ :host { width: 100%; > div { - gap: .5rem; + gap: 0.5rem; } } .invite-col { flex: 1 0 0; label { - margin-bottom: .75rem; + margin-bottom: 0.75rem; } } .delete { diff --git a/frontend/src/app/team-management/new-user/new-user.component.spec.ts b/frontend/src/app/team-management/new-user/new-user.component.spec.ts index ee2250502a..e4b6eb2734 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.spec.ts +++ b/frontend/src/app/team-management/new-user/new-user.component.spec.ts @@ -1,44 +1,32 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NewUserComponent } from "./new-user.component"; -import { FormsModule, NgForm, ReactiveFormsModule } from "@angular/forms"; -import { SharedModule } from "../../shared/shared.module"; -import { PuzzleIconButtonComponent } from "../../shared/custom/puzzle-icon-button/puzzle-icon-button.component"; -import { PuzzleIconComponent } from "../../shared/custom/puzzle-icon/puzzle-icon.component"; -import { CommonModule } from "@angular/common"; +import { NewUserComponent } from './new-user.component'; +import { FormsModule, NgForm, ReactiveFormsModule } from '@angular/forms'; +import { SharedModule } from '../../shared/shared.module'; +import { PuzzleIconButtonComponent } from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; +import { PuzzleIconComponent } from '../../shared/custom/puzzle-icon/puzzle-icon.component'; +import { CommonModule } from '@angular/common'; -describe("NewUserComponent", () => { +describe('NewUserComponent', () => { let component: NewUserComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - NewUserComponent, - PuzzleIconButtonComponent, - PuzzleIconComponent - ], - imports: [ - FormsModule, - ReactiveFormsModule, - SharedModule, - CommonModule - ], - providers: [ - NgForm - ] - }) - .compileComponents(); + declarations: [NewUserComponent, PuzzleIconButtonComponent, PuzzleIconComponent], + imports: [FormsModule, ReactiveFormsModule, SharedModule, CommonModule], + providers: [NgForm], + }).compileComponents(); fixture = TestBed.createComponent(NewUserComponent); component = fixture.componentInstance; - component.user = {firstname: '', lastname: '', email: ''}; + component.user = { firstname: '', lastname: '', email: '' }; fixture.detectChanges(); }); - it("should create", () => { + it('should create', () => { expect(component).toBeTruthy(); }); }); diff --git a/frontend/src/app/team-management/new-user/new-user.component.ts b/frontend/src/app/team-management/new-user/new-user.component.ts index 4c7c41009f..613b2412ed 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.ts +++ b/frontend/src/app/team-management/new-user/new-user.component.ts @@ -5,27 +5,26 @@ import { EventEmitter, Input, Output, - ViewChild -} from "@angular/core"; -import { NewUser } from "../../shared/types/model/NewUser"; -import { ControlContainer, NgForm, NgModel } from "@angular/forms"; + ViewChild, +} from '@angular/core'; +import { NewUser } from '../../shared/types/model/NewUser'; +import { ControlContainer, NgForm, NgModel } from '@angular/forms'; @Component({ selector: 'app-new-user', templateUrl: './new-user.component.html', styleUrl: './new-user.component.scss', - viewProviders: [{provide: ControlContainer, useExisting: NgForm}], - changeDetection: ChangeDetectionStrategy.Default + viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], + changeDetection: ChangeDetectionStrategy.Default, }) -export class NewUserComponent implements AfterViewInit{ +export class NewUserComponent implements AfterViewInit { + randNr = Math.round(Math.random() * 10000); - randNr = Math.round(Math.random()*10000) - - @Input({required: true}) + @Input({ required: true }) index!: number; - @Input({required: true}) - user!: NewUser + @Input({ required: true }) + user!: NewUser; @Output() removeUser: EventEmitter = new EventEmitter(); @@ -37,7 +36,7 @@ export class NewUserComponent implements AfterViewInit{ } showError(firstName: NgModel) { - return firstName.invalid && (firstName.dirty || firstName.touched) + return firstName.invalid && (firstName.dirty || firstName.touched); } remove() { From 4e03df36282bd036975f83c5a9a3e233825376e6 Mon Sep 17 00:00:00 2001 From: Janik Endtner Date: Fri, 2 Feb 2024 08:36:20 +0100 Subject: [PATCH 03/17] #790 format --- .../invite-user-dialog.component.html | 29 ++++-------- .../invite-user-dialog.component.spec.ts | 44 +++++++------------ .../invite-user-dialog.component.ts | 23 +++++----- .../new-user/new-user.component.html | 10 ++++- .../new-user/new-user.component.scss | 4 +- .../new-user/new-user.component.spec.ts | 40 ++++++----------- .../new-user/new-user.component.ts | 25 +++++------ 7 files changed, 72 insertions(+), 103 deletions(-) diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html index 96ca11fd72..6278006215 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html @@ -1,8 +1,8 @@
- - Members einladen - + + Members einladen +
@@ -14,16 +14,11 @@ }
-
@@ -32,13 +27,7 @@
- diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts index 7c9dbd7579..394b706aaf 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts @@ -1,54 +1,44 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { InviteUserDialogComponent } from "./invite-user-dialog.component"; -import { MatDialogModule } from "@angular/material/dialog"; -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { NewUserComponent } from "../new-user/new-user.component"; -import { PuzzleIconComponent } from "../../shared/custom/puzzle-icon/puzzle-icon.component"; -import { PuzzleIconButtonComponent } from "../../shared/custom/puzzle-icon-button/puzzle-icon-button.component"; +import { InviteUserDialogComponent } from './invite-user-dialog.component'; +import { MatDialogModule } from '@angular/material/dialog'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { NewUserComponent } from '../new-user/new-user.component'; +import { PuzzleIconComponent } from '../../shared/custom/puzzle-icon/puzzle-icon.component'; +import { PuzzleIconButtonComponent } from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; -describe("InviteUserDialogComponent", () => { +describe('InviteUserDialogComponent', () => { let component: InviteUserDialogComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - InviteUserDialogComponent, - NewUserComponent, - PuzzleIconComponent, - PuzzleIconButtonComponent - ], - imports: [ - MatDialogModule, - FormsModule, - ReactiveFormsModule - ] - }) - .compileComponents(); + declarations: [InviteUserDialogComponent, NewUserComponent, PuzzleIconComponent, PuzzleIconButtonComponent], + imports: [MatDialogModule, FormsModule, ReactiveFormsModule], + }).compileComponents(); fixture = TestBed.createComponent(InviteUserDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); - it("should create", () => { + it('should create', () => { expect(component).toBeTruthy(); }); it('addUser should add a user to the existing users', () => { component.addUser(); expect(component.users.length).toBe(2); - }) + }); it('removeUser should remove given user from users array', () => { - const user1 = {firstname: 'user1', lastname: '1user', email: 'user1@user.ch'}; - const user2 = {firstname: 'user2', lastname: '2user', email: 'user2@user.ch'}; - const user3 = {firstname: 'user3', lastname: '3user', email: 'user3@user.ch'}; + const user1 = { firstname: 'user1', lastname: '1user', email: 'user1@user.ch' }; + const user2 = { firstname: 'user2', lastname: '2user', email: 'user2@user.ch' }; + const user3 = { firstname: 'user3', lastname: '3user', email: 'user3@user.ch' }; component.users = [user1, user2, user3]; component.removeUser(user2); expect(component.users).toStrictEqual([user1, user3]); - }) + }); }); diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts index 0d30cb35a0..a292c22e0f 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts @@ -1,31 +1,28 @@ -import { Component, ViewChild } from "@angular/core"; -import { NewUser } from "../../shared/types/model/NewUser"; -import { NgForm } from "@angular/forms"; +import { Component, ViewChild } from '@angular/core'; +import { NewUser } from '../../shared/types/model/NewUser'; +import { NgForm } from '@angular/forms'; @Component({ selector: 'app-invite-user-dialog', templateUrl: './invite-user-dialog.component.html', - styleUrl: './invite-user-dialog.component.scss' + styleUrl: './invite-user-dialog.component.scss', }) export class InviteUserDialogComponent { - @ViewChild('form') form!: NgForm; - private readonly emptyUser = {firstname: '', lastname: '', email: ''}; + private readonly emptyUser = { firstname: '', lastname: '', email: '' }; - users: NewUser[] = [{...this.emptyUser}]; + users: NewUser[] = [{ ...this.emptyUser }]; - constructor() { - } + constructor() {} - inviteUsers() { - } + inviteUsers() {} addUser() { - this.users.push({...this.emptyUser}); + this.users.push({ ...this.emptyUser }); } removeUser(user: NewUser) { - this.users = this.users.filter(u => u !== user); + this.users = this.users.filter((u) => u !== user); } } diff --git a/frontend/src/app/team-management/new-user/new-user.component.html b/frontend/src/app/team-management/new-user/new-user.component.html index daa4a4cf49..613bfc28a4 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.html +++ b/frontend/src/app/team-management/new-user/new-user.component.html @@ -13,7 +13,8 @@ #firstName="ngModel" [(ngModel)]="user.firstname" [id]="'firstName-col_' + index" - [name]="'firstName-col_' + index" /> + [name]="'firstName-col_' + index" + /> @if (showError(firstName)) { @if (firstName.invalid) { @@ -75,7 +76,12 @@ @if (index === 0) { } - +
diff --git a/frontend/src/app/team-management/new-user/new-user.component.scss b/frontend/src/app/team-management/new-user/new-user.component.scss index 5c5d371ae6..4d6c765c77 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.scss +++ b/frontend/src/app/team-management/new-user/new-user.component.scss @@ -1,14 +1,14 @@ :host { width: 100%; > div { - gap: .5rem; + gap: 0.5rem; } } .invite-col { flex: 1 0 0; label { - margin-bottom: .75rem; + margin-bottom: 0.75rem; } } .delete { diff --git a/frontend/src/app/team-management/new-user/new-user.component.spec.ts b/frontend/src/app/team-management/new-user/new-user.component.spec.ts index ee2250502a..e4b6eb2734 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.spec.ts +++ b/frontend/src/app/team-management/new-user/new-user.component.spec.ts @@ -1,44 +1,32 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NewUserComponent } from "./new-user.component"; -import { FormsModule, NgForm, ReactiveFormsModule } from "@angular/forms"; -import { SharedModule } from "../../shared/shared.module"; -import { PuzzleIconButtonComponent } from "../../shared/custom/puzzle-icon-button/puzzle-icon-button.component"; -import { PuzzleIconComponent } from "../../shared/custom/puzzle-icon/puzzle-icon.component"; -import { CommonModule } from "@angular/common"; +import { NewUserComponent } from './new-user.component'; +import { FormsModule, NgForm, ReactiveFormsModule } from '@angular/forms'; +import { SharedModule } from '../../shared/shared.module'; +import { PuzzleIconButtonComponent } from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; +import { PuzzleIconComponent } from '../../shared/custom/puzzle-icon/puzzle-icon.component'; +import { CommonModule } from '@angular/common'; -describe("NewUserComponent", () => { +describe('NewUserComponent', () => { let component: NewUserComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - NewUserComponent, - PuzzleIconButtonComponent, - PuzzleIconComponent - ], - imports: [ - FormsModule, - ReactiveFormsModule, - SharedModule, - CommonModule - ], - providers: [ - NgForm - ] - }) - .compileComponents(); + declarations: [NewUserComponent, PuzzleIconButtonComponent, PuzzleIconComponent], + imports: [FormsModule, ReactiveFormsModule, SharedModule, CommonModule], + providers: [NgForm], + }).compileComponents(); fixture = TestBed.createComponent(NewUserComponent); component = fixture.componentInstance; - component.user = {firstname: '', lastname: '', email: ''}; + component.user = { firstname: '', lastname: '', email: '' }; fixture.detectChanges(); }); - it("should create", () => { + it('should create', () => { expect(component).toBeTruthy(); }); }); diff --git a/frontend/src/app/team-management/new-user/new-user.component.ts b/frontend/src/app/team-management/new-user/new-user.component.ts index 4c7c41009f..613b2412ed 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.ts +++ b/frontend/src/app/team-management/new-user/new-user.component.ts @@ -5,27 +5,26 @@ import { EventEmitter, Input, Output, - ViewChild -} from "@angular/core"; -import { NewUser } from "../../shared/types/model/NewUser"; -import { ControlContainer, NgForm, NgModel } from "@angular/forms"; + ViewChild, +} from '@angular/core'; +import { NewUser } from '../../shared/types/model/NewUser'; +import { ControlContainer, NgForm, NgModel } from '@angular/forms'; @Component({ selector: 'app-new-user', templateUrl: './new-user.component.html', styleUrl: './new-user.component.scss', - viewProviders: [{provide: ControlContainer, useExisting: NgForm}], - changeDetection: ChangeDetectionStrategy.Default + viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], + changeDetection: ChangeDetectionStrategy.Default, }) -export class NewUserComponent implements AfterViewInit{ +export class NewUserComponent implements AfterViewInit { + randNr = Math.round(Math.random() * 10000); - randNr = Math.round(Math.random()*10000) - - @Input({required: true}) + @Input({ required: true }) index!: number; - @Input({required: true}) - user!: NewUser + @Input({ required: true }) + user!: NewUser; @Output() removeUser: EventEmitter = new EventEmitter(); @@ -37,7 +36,7 @@ export class NewUserComponent implements AfterViewInit{ } showError(firstName: NgModel) { - return firstName.invalid && (firstName.dirty || firstName.touched) + return firstName.invalid && (firstName.dirty || firstName.touched); } remove() { From 6927b65bf56adc90f8baf9600fc4f2851dfba599 Mon Sep 17 00:00:00 2001 From: Janik Endtner Date: Fri, 2 Feb 2024 10:49:41 +0100 Subject: [PATCH 04/17] #790 create backend methods to create users --- .../puzzle/okr/controller/UserController.java | 12 +++++++++ .../java/ch/puzzle/okr/mapper/UserMapper.java | 18 +++++++++++++ .../UserAuthorizationService.java | 10 +++++++- .../service/business/UserBusinessService.java | 8 ++++++ .../persistence/UserPersistenceService.java | 3 +++ .../UserAuthorizationServiceTest.java | 25 +++++++++++++++++++ 6 files changed, 75 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/ch/puzzle/okr/controller/UserController.java b/backend/src/main/java/ch/puzzle/okr/controller/UserController.java index 51893f3060..83ddfe3565 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/UserController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/UserController.java @@ -1,5 +1,6 @@ package ch.puzzle.okr.controller; +import ch.puzzle.okr.dto.NewUserDto; import ch.puzzle.okr.dto.UserDto; import ch.puzzle.okr.mapper.UserMapper; import ch.puzzle.okr.service.authorization.AuthorizationService; @@ -68,4 +69,15 @@ public UserDto setOkrChampion( return userMapper.toDto(user); } + @Operation(summary = "Create users", description = "Creates a user entity for every user in the method body") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Returned users", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = UserDto.class)) }), }) + @PostMapping(path = "/createall") + public List createUsers( + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The Team as json to create a new Team.", required = true) @RequestBody List newUserDtoList + ) { + var createdUsers = this.userAuthorizationService.createUsers(userMapper.toUserList(newUserDtoList)); + return userMapper.toDtos(createdUsers); + } + } diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/UserMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/UserMapper.java index ac68b62921..8d2f3c1892 100644 --- a/backend/src/main/java/ch/puzzle/okr/mapper/UserMapper.java +++ b/backend/src/main/java/ch/puzzle/okr/mapper/UserMapper.java @@ -1,10 +1,12 @@ package ch.puzzle.okr.mapper; +import ch.puzzle.okr.dto.NewUserDto; import ch.puzzle.okr.dto.UserDto; import ch.puzzle.okr.dto.UserTeamDto; import ch.puzzle.okr.models.User; import org.springframework.stereotype.Component; +import java.util.List; import java.util.stream.Collectors; @Component @@ -16,6 +18,10 @@ public UserMapper(TeamMapper teamMapper) { this.teamMapper = teamMapper; } + public List toDtos(List userList) { + return userList.stream().map(this::toDto).toList(); + } + public UserDto toDto(User user) { var userTeams = user.getUserTeamList().stream().map( ut -> new UserTeamDto(ut.getId(), user.getVersion(), teamMapper.toDto(ut.getTeam()), ut.isTeamAdmin())) @@ -24,4 +30,16 @@ public UserDto toDto(User user) { return new UserDto(user.getId(), user.getVersion(), user.getFirstname(), user.getLastname(), user.getEmail(), userTeams, user.isOkrChampion()); } + + public List toUserList(List newUserList) { + return newUserList.stream().map(this::toUser).toList(); + } + + public User toUser(NewUserDto newUserDto) { + var user = new User(); + user.setFirstname(newUserDto.firstname()); + user.setLastname(newUserDto.lastname()); + user.setEmail(newUserDto.email()); + return user; + } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java b/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java index 0bfa5bf90d..1a262c7ebb 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java @@ -19,7 +19,7 @@ public class UserAuthorizationService { private final TeamAuthorizationService teamAuthorizationService; public UserAuthorizationService(UserBusinessService userBusinessService, AuthorizationService authorizationService, - TeamAuthorizationService teamAuthorizationService) { + TeamAuthorizationService teamAuthorizationService) { this.userBusinessService = userBusinessService; this.authorizationService = authorizationService; this.teamAuthorizationService = teamAuthorizationService; @@ -52,4 +52,12 @@ public User setIsOkrChampion(long id, boolean isOkrChampion) { OkrResponseStatusException.of(ErrorKey.NOT_AUTHORIZED_TO_WRITE, USER)); return userBusinessService.setIsOkrChampion(user, isOkrChampion); } + + public List createUsers(List userList) { + AuthorizationService.checkRoleWriteAndReadAll( + authorizationService.updateOrAddAuthorizationUser(), + OkrResponseStatusException.of(ErrorKey.NOT_AUTHORIZED_TO_WRITE, USER) + ); + return userBusinessService.createUsers(userList); + } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/UserBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/UserBusinessService.java index 5c749f6386..6155e4fd0d 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/UserBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/UserBusinessService.java @@ -6,11 +6,13 @@ import ch.puzzle.okr.service.CacheService; import ch.puzzle.okr.service.persistence.UserPersistenceService; import ch.puzzle.okr.service.validation.UserValidationService; +import jakarta.transaction.Transactional; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import java.util.List; import java.util.Objects; +import java.util.stream.StreamSupport; @Service public class UserBusinessService { @@ -61,4 +63,10 @@ private void checkAtLeastOneOkrChampionExists(User user) { public User saveUser(User user) { return userPersistenceService.save(user); } + + @Transactional + public List createUsers(List userList) { + var userIter = userPersistenceService.saveAll(userList); + return StreamSupport.stream(userIter.spliterator(), false).toList(); + } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/UserPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/UserPersistenceService.java index 235cf666a3..2d465f4fea 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/UserPersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/UserPersistenceService.java @@ -39,4 +39,7 @@ public User save(User user) { public List findAllOkrChampions() { return getRepository().findByIsOkrChampion(true); } + public Iterable saveAll(List userList) { + return getRepository().saveAll(userList); + } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/UserAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/UserAuthorizationServiceTest.java index 0e57fba9f8..cecbd45db2 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/UserAuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/UserAuthorizationServiceTest.java @@ -81,4 +81,29 @@ void setOkrChampion_shouldThrowErrorIfLoggedInUserIsNotOkrChampion() { assertThrows(OkrResponseStatusException.class, () -> userAuthorizationService.setIsOkrChampion(user.getId(), true)); } + + @Test + void createUsers_shouldCallBusinessService() { + var loggedInUser = defaultUser(1L); + loggedInUser.setOkrChampion(true); + + List users = List.of(user, user2); + when(userBusinessService.createUsers(users)).thenReturn(users); + when(authorizationService.updateOrAddAuthorizationUser()).thenReturn(new AuthorizationUser(loggedInUser)); + + userAuthorizationService.createUsers(users); + + verify(userBusinessService, times(1)).createUsers(users); + } + + @Test + void createUsers_shouldThrowErrorIfLoggedInUserIsNotOkrChampion() { + var loggedInUser = defaultUser(1L); + loggedInUser.setOkrChampion(false); + + when(authorizationService.updateOrAddAuthorizationUser()).thenReturn(new AuthorizationUser(loggedInUser)); + + assertThrows(OkrResponseStatusException.class, + () -> userAuthorizationService.createUsers(List.of(user, user2))); + } } From b31995c51d946be986e867b85853a0c06b26a12a Mon Sep 17 00:00:00 2001 From: Janik Endtner Date: Fri, 2 Feb 2024 10:50:26 +0100 Subject: [PATCH 05/17] #790 call backend when creating users + check if email not exists --- .../java/ch/puzzle/okr/dto/NewUserDto.java | 4 +++ .../src/app/services/user.service.spec.ts | 36 +++++++++++++++---- frontend/src/app/services/user.service.ts | 12 ++++++- .../invite-user-dialog.component.spec.ts | 28 ++++++++++++++- .../invite-user-dialog.component.ts | 11 ++++-- .../member-detail/member-detail.component.ts | 2 -- .../new-user/new-user.component.html | 13 +++++-- .../new-user/new-user.component.ts | 6 ++-- .../new-user/unique-mail.directive.spec.ts | 36 +++++++++++++++++++ .../new-user/unique-mail.directive.ts | 32 +++++++++++++++++ .../team-management/team-management.module.ts | 2 ++ frontend/src/assets/i18n/de.json | 3 +- 12 files changed, 165 insertions(+), 20 deletions(-) create mode 100644 backend/src/main/java/ch/puzzle/okr/dto/NewUserDto.java create mode 100644 frontend/src/app/team-management/new-user/unique-mail.directive.spec.ts create mode 100644 frontend/src/app/team-management/new-user/unique-mail.directive.ts diff --git a/backend/src/main/java/ch/puzzle/okr/dto/NewUserDto.java b/backend/src/main/java/ch/puzzle/okr/dto/NewUserDto.java new file mode 100644 index 0000000000..5874453023 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/NewUserDto.java @@ -0,0 +1,4 @@ +package ch.puzzle.okr.dto; + +public record NewUserDto(String firstname, String lastname, String email) { +} diff --git a/frontend/src/app/services/user.service.spec.ts b/frontend/src/app/services/user.service.spec.ts index c672c89e26..852129736f 100644 --- a/frontend/src/app/services/user.service.spec.ts +++ b/frontend/src/app/services/user.service.spec.ts @@ -1,7 +1,7 @@ -import { getTestBed, TestBed } from '@angular/core/testing'; +import { fakeAsync, getTestBed, TestBed, tick } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { UserService } from './user.service'; -import { users } from '../shared/testData'; +import { testUser, users } from '../shared/testData'; describe('UserService', () => { let service: UserService; @@ -21,11 +21,11 @@ describe('UserService', () => { httpMock.verify(); }); - test('should be created', () => { + it('should be created', () => { expect(service).toBeTruthy(); }); - test('getUsers should only reload users when they are not loaded yet', (done) => { + it('getUsers should only reload users when they are not loaded yet', (done) => { const spy = jest.spyOn(service, 'reloadUsers'); service.getUsers().subscribe(() => { expect(spy).toBeCalledTimes(1); @@ -38,11 +38,11 @@ describe('UserService', () => { }); }); - test('get current user should throw error, when not loaded', () => { + it('get current user should throw error, when not loaded', () => { expect(() => service.getCurrentUser()).toThrowError('user should not be undefined here'); }); - test('init current user should load user', (done) => { + it('init current user should load user', (done) => { expect(() => service.getCurrentUser()).toThrowError('user should not be undefined here'); service.getOrInitCurrentUser().subscribe(() => { expect(service.getCurrentUser()).toBe(users[0]); @@ -51,4 +51,28 @@ describe('UserService', () => { const req = httpMock.expectOne('api/v1/users/current'); req.flush(users[0]); }); + + it('setIsOkrChampion should call put operation, reloadUsers and reloadCurrentUser', fakeAsync(() => { + service.setIsOkrChampion(testUser, true).subscribe(); + const req = httpMock.expectOne(`api/v1/users/${testUser.id}/isokrchampion/true`); + req.flush(users[0]); + + tick(); + + const req2 = httpMock.expectOne(`api/v1/users`); + const req3 = httpMock.expectOne(`api/v1/users/current`); + req2.flush({}); + req3.flush({}); + })); + + it('createUsers should call createAll and reloadUsers', fakeAsync(() => { + service.createUsers(users).subscribe(); + const req = httpMock.expectOne(`api/v1/users/createall`); + req.flush(users); + + tick(); + + const req2 = httpMock.expectOne(`api/v1/users`); + req2.flush({}); + })); }); diff --git a/frontend/src/app/services/user.service.ts b/frontend/src/app/services/user.service.ts index c632de9e0e..8900999470 100644 --- a/frontend/src/app/services/user.service.ts +++ b/frontend/src/app/services/user.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, of, tap } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { User } from '../shared/types/model/User'; +import { NewUser } from '../shared/types/model/NewUser'; @Injectable({ providedIn: 'root', @@ -50,6 +51,15 @@ export class UserService { } setIsOkrChampion(user: User, isOkrChampion: boolean) { - return this.httpClient.put(`${this.API_URL}/${user.id}/isokrchampion/${isOkrChampion}`, {}); + return this.httpClient.put(`${this.API_URL}/${user.id}/isokrchampion/${isOkrChampion}`, {}).pipe( + tap(() => { + this.reloadUsers(); + this.reloadCurrentUser().subscribe(); + }), + ); + } + + createUsers(userList: NewUser[]) { + return this.httpClient.post(`${this.API_URL}/createall`, userList).pipe(tap(() => this.reloadUsers())); } } diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts index 394b706aaf..ee15263da7 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { InviteUserDialogComponent } from './invite-user-dialog.component'; import { MatDialogModule } from '@angular/material/dialog'; @@ -6,15 +6,31 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { NewUserComponent } from '../new-user/new-user.component'; import { PuzzleIconComponent } from '../../shared/custom/puzzle-icon/puzzle-icon.component'; import { PuzzleIconButtonComponent } from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; +import { UserService } from '../../services/user.service'; +import { testUser } from '../../shared/testData'; +import { DialogRef } from '@angular/cdk/dialog'; +import { of } from 'rxjs'; describe('InviteUserDialogComponent', () => { let component: InviteUserDialogComponent; let fixture: ComponentFixture; + const userServiceMock = { + createUsers: jest.fn(), + }; + + const dialogRefMock = { + close: jest.fn(), + }; + beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [InviteUserDialogComponent, NewUserComponent, PuzzleIconComponent, PuzzleIconButtonComponent], imports: [MatDialogModule, FormsModule, ReactiveFormsModule], + providers: [ + { provide: UserService, useValue: userServiceMock }, + { provide: DialogRef, useValue: dialogRefMock }, + ], }).compileComponents(); fixture = TestBed.createComponent(InviteUserDialogComponent); @@ -41,4 +57,14 @@ describe('InviteUserDialogComponent', () => { expect(component.users).toStrictEqual([user1, user3]); }); + + it('inviteUsers should call createUsers and close dialog', fakeAsync(() => { + userServiceMock.createUsers.mockReturnValue(of([testUser])); + component.inviteUsers(); + tick(); + + expect(userServiceMock.createUsers).toBeCalledTimes(1); + expect(userServiceMock.createUsers).toBeCalledWith(component.users); + expect(dialogRefMock.close).toBeCalledTimes(1); + })); }); diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts index a292c22e0f..87a2a722b1 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts @@ -1,6 +1,8 @@ import { Component, ViewChild } from '@angular/core'; import { NewUser } from '../../shared/types/model/NewUser'; import { NgForm } from '@angular/forms'; +import { UserService } from '../../services/user.service'; +import { DialogRef } from '@angular/cdk/dialog'; @Component({ selector: 'app-invite-user-dialog', @@ -14,9 +16,14 @@ export class InviteUserDialogComponent { users: NewUser[] = [{ ...this.emptyUser }]; - constructor() {} + constructor( + private readonly userService: UserService, + private readonly dialogRef: DialogRef, + ) {} - inviteUsers() {} + inviteUsers() { + this.userService.createUsers(this.users).subscribe(() => this.dialogRef.close()); + } addUser() { this.users.push({ ...this.emptyUser }); diff --git a/frontend/src/app/team-management/member-detail/member-detail.component.ts b/frontend/src/app/team-management/member-detail/member-detail.component.ts index d6a2127e68..b840c238fa 100644 --- a/frontend/src/app/team-management/member-detail/member-detail.component.ts +++ b/frontend/src/app/team-management/member-detail/member-detail.component.ts @@ -130,9 +130,7 @@ export class MemberDetailComponent implements OnInit, OnDestroy { isOkrChampionChange(okrChampion: boolean, user: User) { this.userService.setIsOkrChampion(user, okrChampion).subscribe(() => { this.loadUser(user.id); - this.userService.reloadUsers(); this.teamService.reloadTeams(); - this.userService.reloadCurrentUser().subscribe(); }); } } diff --git a/frontend/src/app/team-management/new-user/new-user.component.html b/frontend/src/app/team-management/new-user/new-user.component.html index 613bfc28a4..7b2c8a0cbc 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.html +++ b/frontend/src/app/team-management/new-user/new-user.component.html @@ -18,7 +18,7 @@ @if (showError(firstName)) { @if (firstName.invalid) { - Ungültige Angabe + Angabe benötigt } } @@ -41,7 +41,7 @@ @if (showError(lastName)) { @if (lastName.invalid) { - Ungültige Angabe + Angabe benötigt } } @@ -60,12 +60,19 @@ class="value-field" #email="ngModel" email + appUniqueEmail [(ngModel)]="user.email" /> @if (showError(email)) { @if (email.invalid) { - Ungültige Angabe + @if (email.errors?.["required"]) { + Angabe benötigt + } @else if (email.errors?.["notUniqueMail"]) { + E-Mail existiert bereits + } @else if (email.errors?.["email"]) { + E-Mail ungültig + } } } diff --git a/frontend/src/app/team-management/new-user/new-user.component.ts b/frontend/src/app/team-management/new-user/new-user.component.ts index 613b2412ed..c6494edd91 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.ts +++ b/frontend/src/app/team-management/new-user/new-user.component.ts @@ -18,8 +18,6 @@ import { ControlContainer, NgForm, NgModel } from '@angular/forms'; changeDetection: ChangeDetectionStrategy.Default, }) export class NewUserComponent implements AfterViewInit { - randNr = Math.round(Math.random() * 10000); - @Input({ required: true }) index!: number; @@ -35,8 +33,8 @@ export class NewUserComponent implements AfterViewInit { this.firstInput.nativeElement.focus(); } - showError(firstName: NgModel) { - return firstName.invalid && (firstName.dirty || firstName.touched); + showError(control: NgModel) { + return control.invalid && (control.dirty || control.touched); } remove() { diff --git a/frontend/src/app/team-management/new-user/unique-mail.directive.spec.ts b/frontend/src/app/team-management/new-user/unique-mail.directive.spec.ts new file mode 100644 index 0000000000..aa4d8068da --- /dev/null +++ b/frontend/src/app/team-management/new-user/unique-mail.directive.spec.ts @@ -0,0 +1,36 @@ +import { UniqueEmailValidatorDirective } from './unique-mail.directive'; +import { users } from '../../shared/testData'; +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { AbstractControl } from '@angular/forms'; + +describe('UniqueMailDirective', () => { + const userServiceMock = { + getUsers: jest.fn(), + } as any; + + beforeEach(() => { + userServiceMock.getUsers.mockReturnValue(of(users)); + }); + + it('should create an instance', () => { + TestBed.runInInjectionContext(() => { + const directive = new UniqueEmailValidatorDirective(userServiceMock); + expect(directive).toBeTruthy(); + }); + }); + + it('should return validationError if user exists, otherwise null', () => { + TestBed.runInInjectionContext(() => { + const directive = new UniqueEmailValidatorDirective(userServiceMock); + + let control = { value: users[0].email } as AbstractControl; + expect(directive.validate(control)).toStrictEqual({ notUniqueMail: { value: users[0].email } }); + + control = { value: 'notexistinguser@test.com' } as AbstractControl; + expect(directive.validate(control)).toStrictEqual(null); + + expect(directive).toBeTruthy(); + }); + }); +}); diff --git a/frontend/src/app/team-management/new-user/unique-mail.directive.ts b/frontend/src/app/team-management/new-user/unique-mail.directive.ts new file mode 100644 index 0000000000..cec125ab34 --- /dev/null +++ b/frontend/src/app/team-management/new-user/unique-mail.directive.ts @@ -0,0 +1,32 @@ +import { Directive } from '@angular/core'; +import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms'; +import { UserService } from '../../services/user.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +@Directive({ + selector: '[appUniqueEmail]', + providers: [ + { + provide: NG_VALIDATORS, + useExisting: UniqueEmailValidatorDirective, + multi: true, + }, + ], +}) +export class UniqueEmailValidatorDirective implements Validator { + private existingUserMails: string[] = []; + + constructor(private readonly userService: UserService) { + this.userService + .getUsers() + .pipe(takeUntilDestroyed()) + .subscribe((users) => { + this.existingUserMails = users.map((u) => u.email); + }); + } + + validate(control: AbstractControl): ValidationErrors | null { + const existingUser = this.existingUserMails.includes(control.value); + return existingUser ? { notUniqueMail: { value: control.value } } : null; + } +} diff --git a/frontend/src/app/team-management/team-management.module.ts b/frontend/src/app/team-management/team-management.module.ts index 03a57af44e..4308be31aa 100644 --- a/frontend/src/app/team-management/team-management.module.ts +++ b/frontend/src/app/team-management/team-management.module.ts @@ -40,6 +40,7 @@ import { EditOkrChampionComponent } from './edit-okr-champion/edit-okr-champion. import { MatTooltipModule } from '@angular/material/tooltip'; import { NewUserComponent } from './new-user/new-user.component'; import { InviteUserDialogComponent } from './invite-user-dialog/invite-user-dialog.component'; +import { UniqueEmailValidatorDirective } from './new-user/unique-mail.directive'; @NgModule({ declarations: [ @@ -64,6 +65,7 @@ import { InviteUserDialogComponent } from './invite-user-dialog/invite-user-dial EditOkrChampionComponent, NewUserComponent, InviteUserDialogComponent, + UniqueEmailValidatorDirective, ], imports: [ CommonModule, diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index d0219e84a2..8dee2ef4f2 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -81,7 +81,8 @@ "PUT": "Das Check-in wurde erfolgreich aktualisiert." }, "USERS": { - "PUT": "Der User wurde erfolgreich aktualisiert." + "PUT": "Der Member wurde erfolgreich aktualisiert.", + "POST": "Die Members wurden erfolgreich eingeladen" } }, "DIALOG_ERRORS": { From e4117901d5c85dde267aec13c7b29873e3307f80 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 2 Feb 2024 09:53:16 +0000 Subject: [PATCH 06/17] [FM] Automated formating backend --- .../java/ch/puzzle/okr/controller/UserController.java | 3 +-- .../service/authorization/UserAuthorizationService.java | 8 +++----- .../okr/service/persistence/UserPersistenceService.java | 1 + 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/controller/UserController.java b/backend/src/main/java/ch/puzzle/okr/controller/UserController.java index 83ddfe3565..3b2d8a7382 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/UserController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/UserController.java @@ -74,8 +74,7 @@ public UserDto setOkrChampion( @Content(mediaType = "application/json", schema = @Schema(implementation = UserDto.class)) }), }) @PostMapping(path = "/createall") public List createUsers( - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The Team as json to create a new Team.", required = true) @RequestBody List newUserDtoList - ) { + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "The Team as json to create a new Team.", required = true) @RequestBody List newUserDtoList) { var createdUsers = this.userAuthorizationService.createUsers(userMapper.toUserList(newUserDtoList)); return userMapper.toDtos(createdUsers); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java b/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java index 1a262c7ebb..2cd0a29259 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/authorization/UserAuthorizationService.java @@ -19,7 +19,7 @@ public class UserAuthorizationService { private final TeamAuthorizationService teamAuthorizationService; public UserAuthorizationService(UserBusinessService userBusinessService, AuthorizationService authorizationService, - TeamAuthorizationService teamAuthorizationService) { + TeamAuthorizationService teamAuthorizationService) { this.userBusinessService = userBusinessService; this.authorizationService = authorizationService; this.teamAuthorizationService = teamAuthorizationService; @@ -54,10 +54,8 @@ public User setIsOkrChampion(long id, boolean isOkrChampion) { } public List createUsers(List userList) { - AuthorizationService.checkRoleWriteAndReadAll( - authorizationService.updateOrAddAuthorizationUser(), - OkrResponseStatusException.of(ErrorKey.NOT_AUTHORIZED_TO_WRITE, USER) - ); + AuthorizationService.checkRoleWriteAndReadAll(authorizationService.updateOrAddAuthorizationUser(), + OkrResponseStatusException.of(ErrorKey.NOT_AUTHORIZED_TO_WRITE, USER)); return userBusinessService.createUsers(userList); } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/UserPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/UserPersistenceService.java index 2d465f4fea..6d5befc699 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/UserPersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/UserPersistenceService.java @@ -39,6 +39,7 @@ public User save(User user) { public List findAllOkrChampions() { return getRepository().findByIsOkrChampion(true); } + public Iterable saveAll(List userList) { return getRepository().saveAll(userList); } From 74a46a3f493167d938c5496bdbc3c3bd329fd9c1 Mon Sep 17 00:00:00 2001 From: Janik Endtner Date: Fri, 2 Feb 2024 11:42:26 +0100 Subject: [PATCH 07/17] #790 add cypress tests --- frontend/cypress/e2e/teammanagement.cy.ts | 50 +++++++++++++++++++ .../invite-user-dialog.component.html | 9 +++- .../member-list/member-list.component.html | 6 +-- .../member-list/member-list.component.ts | 4 +- 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/frontend/cypress/e2e/teammanagement.cy.ts b/frontend/cypress/e2e/teammanagement.cy.ts index d43544ab68..03fc299eda 100644 --- a/frontend/cypress/e2e/teammanagement.cy.ts +++ b/frontend/cypress/e2e/teammanagement.cy.ts @@ -141,6 +141,48 @@ describe('Team management tests', () => { }); }); + describe('invite members', () => { + it('invite two members', () => { + const mailUserClaudia = uniqueSuffix('claudia.meier@test') + '.ch'; + const mailUserStefan = uniqueSuffix('stefan.schmidt@test') + '.ch'; + const firstNameClaudia = uniqueSuffix('Claudia'); + const firstNameStefan = uniqueSuffix('Stefan'); + + cy.getByTestId('invite-member').click(); + fillOutNewUser(firstNameClaudia, 'Meier', mailUserClaudia); + cy.tabForward(); + cy.tabForward(); + cy.realPress('Enter'); + fillOutNewUser(firstNameStefan, 'Schmidt', mailUserStefan); + cy.tabForward(); + cy.tabForward(); + cy.realPress('Enter'); + + // test error messages + fillOutNewUser('Robin', '', 'papierer'); + cy.contains('Angabe benötigt'); + cy.contains('E-Mail ungültig'); + cy.realType('@puzzle.ch'); + cy.contains('E-Mail ungültig').should('not.exist'); + cy.contains('E-Mail existiert bereits'); + cy.tabBackward(); + cy.realType('Papirer'); + cy.contains('Angabe benötigt').should('not.exist'); + + // delete last entry + cy.tabForward(); + cy.tabForward(); + cy.realPress('Enter'); + cy.contains('papiererr@puzzle.ch').should('not.exist'); + + // save + cy.getByTestId('invite').click(); + cy.contains('Die Members wurden erfolgreich eingeladen'); + cy.contains(firstNameClaudia); + cy.contains(firstNameStefan); + }); + }); + it('Navigate to Bobs profile and add him to BBT and LoremIpsum', () => { cy.intercept('PUT', '**/updateaddteammembership/*').as('updateEsha'); @@ -362,3 +404,11 @@ function navigateToUser(userName: string) { cy.get('td').contains(userName).click(); cy.wait('@getUser'); } + +function fillOutNewUser(firstname: string, lastname: string, email: string) { + cy.realType(firstname, { delay: 1 }); + cy.tabForward(); + cy.realType(lastname, { delay: 1 }); + cy.tabForward(); + cy.realType(email, { delay: 1 }); +} diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html index 6278006215..48edaca8cb 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html @@ -27,7 +27,14 @@
- diff --git a/frontend/src/app/team-management/member-list/member-list.component.html b/frontend/src/app/team-management/member-list/member-list.component.html index 468b248bb4..efbe51cc88 100644 --- a/frontend/src/app/team-management/member-list/member-list.component.html +++ b/frontend/src/app/team-management/member-list/member-list.component.html @@ -32,9 +32,9 @@

Alle Teams

diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index 8dee2ef4f2..64b7e4f567 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -82,7 +82,7 @@ }, "USERS": { "PUT": "Der Member wurde erfolgreich aktualisiert.", - "POST": "Die Members wurden erfolgreich eingeladen" + "POST": "Die Members wurden erfolgreich registriert" } }, "DIALOG_ERRORS": { From e281ca8fe2ad8d6942766c932bd85743c45cf4d0 Mon Sep 17 00:00:00 2001 From: e560704 Date: Fri, 12 Apr 2024 08:37:19 +0200 Subject: [PATCH 11/17] #790 change form to reactiveForm for better validation + add emails from current form to unique-mail validator --- .../src/app/shared/types/model/NewUserForm.ts | 5 ++ .../invite-user-dialog.component.html | 9 +- .../invite-user-dialog.component.ts | 62 +++++++++---- .../new-user/new-user.component.html | 68 ++++++-------- .../new-user/new-user.component.ts | 25 ++++-- .../new-user/unique-mail.directive.ts | 32 ------- .../new-user/unique-mail.validator.ts | 28 ++++++ .../team-management/team-management.module.ts | 88 +++++++++---------- 8 files changed, 171 insertions(+), 146 deletions(-) create mode 100644 frontend/src/app/shared/types/model/NewUserForm.ts delete mode 100644 frontend/src/app/team-management/new-user/unique-mail.directive.ts create mode 100644 frontend/src/app/team-management/new-user/unique-mail.validator.ts diff --git a/frontend/src/app/shared/types/model/NewUserForm.ts b/frontend/src/app/shared/types/model/NewUserForm.ts new file mode 100644 index 0000000000..c28c2fc626 --- /dev/null +++ b/frontend/src/app/shared/types/model/NewUserForm.ts @@ -0,0 +1,5 @@ +export interface NewUserForm { + firstname: T; + email: T; + lastname: T +} diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html index 70d276f948..5d2d0faf67 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html @@ -7,10 +7,10 @@
-
- @for (user of users; track $index) { + + @for (userFormGroup of form.controls; track $index) {
- +
}
@@ -30,10 +30,9 @@ diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts index 87a2a722b1..8fd16646c7 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts @@ -1,8 +1,10 @@ -import { Component, ViewChild } from '@angular/core'; -import { NewUser } from '../../shared/types/model/NewUser'; -import { NgForm } from '@angular/forms'; -import { UserService } from '../../services/user.service'; -import { DialogRef } from '@angular/cdk/dialog'; +import {Component} from '@angular/core'; +import {NewUser} from '../../shared/types/model/NewUser'; +import {FormArray, FormControl, FormGroup, NonNullableFormBuilder, Validators,} from '@angular/forms'; +import {UserService} from '../../services/user.service'; +import {DialogRef} from '@angular/cdk/dialog'; +import {NewUserForm} from "../../shared/types/model/NewUserForm"; +import {UniqueEmailValidator} from "../new-user/unique-mail.validator"; @Component({ selector: 'app-invite-user-dialog', @@ -10,26 +12,56 @@ import { DialogRef } from '@angular/cdk/dialog'; styleUrl: './invite-user-dialog.component.scss', }) export class InviteUserDialogComponent { - @ViewChild('form') form!: NgForm; - private readonly emptyUser = { firstname: '', lastname: '', email: '' }; - - users: NewUser[] = [{ ...this.emptyUser }]; + form: FormArray>>; + triedToSubmit = false; constructor( private readonly userService: UserService, private readonly dialogRef: DialogRef, - ) {} + private readonly formBuilder: NonNullableFormBuilder, + private readonly uniqueMailValidator: UniqueEmailValidator + ) { + this.form = this.formBuilder.array([this.createUserFormGroup()]); + } + + registerUsers() { + this.triedToSubmit = true; + if (!this.form.valid) { + return; + } + this.userService.createUsers(this.extractFormValue()).subscribe(() => this.dialogRef.close()); + } - inviteUsers() { - this.userService.createUsers(this.users).subscribe(() => this.dialogRef.close()); + private extractFormValue(): NewUser[] { + return this.form.value as NewUser[]; } addUser() { - this.users.push({ ...this.emptyUser }); + this.form.push(this.createUserFormGroup()); + } + + removeUser(index: number) { + this.form.removeAt(index); + } + + private createUserFormGroup() { + this.uniqueMailValidator.setAddedMails(this.extractAddedMails()); + return this.formBuilder.group({ + firstname: this.formBuilder.control('', [Validators.required, Validators.minLength(1)]), + lastname: this.formBuilder.control('', [Validators.required, Validators.minLength(1)]), + email: this.formBuilder.control('', [ + Validators.required, + Validators.minLength(1), + this.uniqueMailValidator.validate.bind(this.uniqueMailValidator) + ]), + }) } - removeUser(user: NewUser) { - this.users = this.users.filter((u) => u !== user); + private extractAddedMails() { + if (!this.form) { + return []; + } + return this.extractFormValue().map(u => u.email); } } diff --git a/frontend/src/app/team-management/new-user/new-user.component.html b/frontend/src/app/team-management/new-user/new-user.component.html index 53289d1228..0d2ba69ea6 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.html +++ b/frontend/src/app/team-management/new-user/new-user.component.html @@ -1,81 +1,63 @@ -
+
@if (index === 0) { - + } - @if (showError(firstName)) { - - @if (firstName.invalid) { - Angabe benötigt - } - - } + + @if (firstname.errors?.['required'] && triedToSubmit) { + Angabe benötigt + } +
@if (index === 0) { - + } - @if (showError(lastName)) { - - @if (lastName.invalid) { - Angabe benötigt - } - - } + + @if (lastname.errors?.['required'] && triedToSubmit) { + Angabe benötigt + } +
diff --git a/frontend/src/app/team-management/new-user/new-user.component.ts b/frontend/src/app/team-management/new-user/new-user.component.ts index c6494edd91..832897569c 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.ts +++ b/frontend/src/app/team-management/new-user/new-user.component.ts @@ -7,8 +7,8 @@ import { Output, ViewChild, } from '@angular/core'; -import { NewUser } from '../../shared/types/model/NewUser'; -import { ControlContainer, NgForm, NgModel } from '@angular/forms'; +import {ControlContainer, FormControl, FormGroup, NgForm} from '@angular/forms'; +import {NewUserForm} from "../../shared/types/model/NewUserForm"; @Component({ selector: 'app-new-user', @@ -22,7 +22,10 @@ export class NewUserComponent implements AfterViewInit { index!: number; @Input({ required: true }) - user!: NewUser; + userFormGroup!: FormGroup>; + + @Input({ required: true }) + triedToSubmit!: boolean; @Output() removeUser: EventEmitter = new EventEmitter(); @@ -33,11 +36,19 @@ export class NewUserComponent implements AfterViewInit { this.firstInput.nativeElement.focus(); } - showError(control: NgModel) { - return control.invalid && (control.dirty || control.touched); - } - remove() { this.removeUser.emit(); } + + get firstname() { + return this.userFormGroup.controls.firstname; + } + + get lastname() { + return this.userFormGroup.controls.lastname; + } + + get email() { + return this.userFormGroup.controls.email; + } } diff --git a/frontend/src/app/team-management/new-user/unique-mail.directive.ts b/frontend/src/app/team-management/new-user/unique-mail.directive.ts deleted file mode 100644 index cec125ab34..0000000000 --- a/frontend/src/app/team-management/new-user/unique-mail.directive.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Directive } from '@angular/core'; -import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms'; -import { UserService } from '../../services/user.service'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; - -@Directive({ - selector: '[appUniqueEmail]', - providers: [ - { - provide: NG_VALIDATORS, - useExisting: UniqueEmailValidatorDirective, - multi: true, - }, - ], -}) -export class UniqueEmailValidatorDirective implements Validator { - private existingUserMails: string[] = []; - - constructor(private readonly userService: UserService) { - this.userService - .getUsers() - .pipe(takeUntilDestroyed()) - .subscribe((users) => { - this.existingUserMails = users.map((u) => u.email); - }); - } - - validate(control: AbstractControl): ValidationErrors | null { - const existingUser = this.existingUserMails.includes(control.value); - return existingUser ? { notUniqueMail: { value: control.value } } : null; - } -} diff --git a/frontend/src/app/team-management/new-user/unique-mail.validator.ts b/frontend/src/app/team-management/new-user/unique-mail.validator.ts new file mode 100644 index 0000000000..150444ef00 --- /dev/null +++ b/frontend/src/app/team-management/new-user/unique-mail.validator.ts @@ -0,0 +1,28 @@ +import {Injectable} from '@angular/core'; +import {AbstractControl, ValidationErrors, Validator} from '@angular/forms'; +import {UserService} from '../../services/user.service'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; + +@Injectable({ providedIn: 'root' }) +export class UniqueEmailValidator implements Validator { + private existingUserMails: string[] = []; // mails exsiting already in backend + private addedMails: string[] = []; // mails added in form + + constructor(private readonly userService: UserService) { + this.userService + .getUsers() + .pipe(takeUntilDestroyed()) + .subscribe((users) => { + this.existingUserMails = users.map((u) => u.email); + }); + } + + validate(control: AbstractControl, ): ValidationErrors | null { + const existingUser = this.existingUserMails.concat(this.addedMails).includes(control.value); + return existingUser ? { notUniqueMail: { value: control.value } } : null; + } + + setAddedMails(mails: string[]) { + this.addedMails = mails; + } +} diff --git a/frontend/src/app/team-management/team-management.module.ts b/frontend/src/app/team-management/team-management.module.ts index 4308be31aa..b7e7508a7b 100644 --- a/frontend/src/app/team-management/team-management.module.ts +++ b/frontend/src/app/team-management/team-management.module.ts @@ -1,46 +1,47 @@ -import { NgModule } from '@angular/core'; -import { CommonModule, NgOptimizedImage } from '@angular/common'; -import { TeamManagementComponent } from './team-management.component'; -import { TeamManagementRoutingModule } from './team-management-routing.module'; -import { AddEditTeamDialog } from './add-edit-team-dialog/add-edit-team-dialog.component'; -import { SharedModule } from '../shared/shared.module'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { TeamManagementBannerComponent } from './team-management-banner/team-management-banner.component'; -import { TeamListComponent } from './team-list/team-list.component'; -import { MemberListComponent } from './member-list/member-list.component'; -import { MatListModule } from '@angular/material/list'; -import { MatTableModule } from '@angular/material/table'; -import { RolesPipe } from './roles.pipe'; -import { TeamsPipe } from './teams.pipe'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatButtonModule } from '@angular/material/button'; -import { MatMenuModule } from '@angular/material/menu'; -import { SearchTeamManagementComponent } from './search-team-management/search-team-management.component'; -import { AddMemberToTeamDialogComponent } from './add-member-to-team-dialog/add-member-to-team-dialog.component'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; -import { MemberDetailComponent } from './member-detail/member-detail.component'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { TeamRoleDropdownComponent } from './team-role-dropdown/team-role-dropdown.component'; -import { MatSelectModule } from '@angular/material/select'; -import { TranslateModule } from '@ngx-translate/core'; -import { AddUserTeamComponent } from './add-user-team/add-user-team.component'; -import { A11yModule } from '@angular/cdk/a11y'; -import { TeamManagementMobileFilterComponent } from './team-management-mobile-filter/team-management-mobile-filter.component'; -import { MemberListTableComponent } from './member-list/member-list-table/member-list-table.component'; -import { MemberListMobileComponent } from './member-list/member-list-mobile/member-list-mobile.component'; -import { OkrChampionPipe } from './okr-champion.pipe'; -import { PuzzleIconComponent } from '../shared/custom/puzzle-icon/puzzle-icon.component'; -import { PuzzleIconButtonComponent } from '../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; -import { ShowEditRoleComponent } from './show-edit-role/show-edit-role.component'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { EditOkrChampionComponent } from './edit-okr-champion/edit-okr-champion.component'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { NewUserComponent } from './new-user/new-user.component'; -import { InviteUserDialogComponent } from './invite-user-dialog/invite-user-dialog.component'; -import { UniqueEmailValidatorDirective } from './new-user/unique-mail.directive'; +import {NgModule} from '@angular/core'; +import {CommonModule, NgOptimizedImage} from '@angular/common'; +import {TeamManagementComponent} from './team-management.component'; +import {TeamManagementRoutingModule} from './team-management-routing.module'; +import {AddEditTeamDialog} from './add-edit-team-dialog/add-edit-team-dialog.component'; +import {SharedModule} from '../shared/shared.module'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {TeamManagementBannerComponent} from './team-management-banner/team-management-banner.component'; +import {TeamListComponent} from './team-list/team-list.component'; +import {MemberListComponent} from './member-list/member-list.component'; +import {MatListModule} from '@angular/material/list'; +import {MatTableModule} from '@angular/material/table'; +import {RolesPipe} from './roles.pipe'; +import {TeamsPipe} from './teams.pipe'; +import {MatIconModule} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; +import {MatButtonModule} from '@angular/material/button'; +import {MatMenuModule} from '@angular/material/menu'; +import {SearchTeamManagementComponent} from './search-team-management/search-team-management.component'; +import {AddMemberToTeamDialogComponent} from './add-member-to-team-dialog/add-member-to-team-dialog.component'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {MemberDetailComponent} from './member-detail/member-detail.component'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {TeamRoleDropdownComponent} from './team-role-dropdown/team-role-dropdown.component'; +import {MatSelectModule} from '@angular/material/select'; +import {TranslateModule} from '@ngx-translate/core'; +import {AddUserTeamComponent} from './add-user-team/add-user-team.component'; +import {A11yModule} from '@angular/cdk/a11y'; +import { + TeamManagementMobileFilterComponent +} from './team-management-mobile-filter/team-management-mobile-filter.component'; +import {MemberListTableComponent} from './member-list/member-list-table/member-list-table.component'; +import {MemberListMobileComponent} from './member-list/member-list-mobile/member-list-mobile.component'; +import {OkrChampionPipe} from './okr-champion.pipe'; +import {PuzzleIconComponent} from '../shared/custom/puzzle-icon/puzzle-icon.component'; +import {PuzzleIconButtonComponent} from '../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; +import {ShowEditRoleComponent} from './show-edit-role/show-edit-role.component'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {EditOkrChampionComponent} from './edit-okr-champion/edit-okr-champion.component'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {NewUserComponent} from './new-user/new-user.component'; +import {InviteUserDialogComponent} from './invite-user-dialog/invite-user-dialog.component'; @NgModule({ declarations: [ @@ -65,7 +66,6 @@ import { UniqueEmailValidatorDirective } from './new-user/unique-mail.directive' EditOkrChampionComponent, NewUserComponent, InviteUserDialogComponent, - UniqueEmailValidatorDirective, ], imports: [ CommonModule, From 57fcf1accb1c590f4cd03d86fd5dbb84f438fbf4 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 12 Apr 2024 09:53:14 +0000 Subject: [PATCH 12/17] [FM] Automated formating frontend --- .../src/app/shared/types/model/NewUserForm.ts | 2 +- .../invite-user-dialog.component.html | 15 ++-- .../invite-user-dialog.component.ts | 23 +++-- .../new-user/new-user.component.html | 18 ++-- .../new-user/new-user.component.ts | 4 +- .../new-user/unique-mail.validator.ts | 10 +-- .../team-management/team-management.module.ts | 86 +++++++++---------- 7 files changed, 77 insertions(+), 81 deletions(-) diff --git a/frontend/src/app/shared/types/model/NewUserForm.ts b/frontend/src/app/shared/types/model/NewUserForm.ts index c28c2fc626..800df9f04a 100644 --- a/frontend/src/app/shared/types/model/NewUserForm.ts +++ b/frontend/src/app/shared/types/model/NewUserForm.ts @@ -1,5 +1,5 @@ export interface NewUserForm { firstname: T; email: T; - lastname: T + lastname: T; } diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html index 5d2d0faf67..0ebeead56d 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html @@ -10,7 +10,12 @@ @for (userFormGroup of form.controls; track $index) {
- +
}
@@ -27,13 +32,7 @@
- diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts index 8fd16646c7..0d50297c80 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts @@ -1,10 +1,10 @@ -import {Component} from '@angular/core'; -import {NewUser} from '../../shared/types/model/NewUser'; -import {FormArray, FormControl, FormGroup, NonNullableFormBuilder, Validators,} from '@angular/forms'; -import {UserService} from '../../services/user.service'; -import {DialogRef} from '@angular/cdk/dialog'; -import {NewUserForm} from "../../shared/types/model/NewUserForm"; -import {UniqueEmailValidator} from "../new-user/unique-mail.validator"; +import { Component } from '@angular/core'; +import { NewUser } from '../../shared/types/model/NewUser'; +import { FormArray, FormControl, FormGroup, NonNullableFormBuilder, Validators } from '@angular/forms'; +import { UserService } from '../../services/user.service'; +import { DialogRef } from '@angular/cdk/dialog'; +import { NewUserForm } from '../../shared/types/model/NewUserForm'; +import { UniqueEmailValidator } from '../new-user/unique-mail.validator'; @Component({ selector: 'app-invite-user-dialog', @@ -12,7 +12,6 @@ import {UniqueEmailValidator} from "../new-user/unique-mail.validator"; styleUrl: './invite-user-dialog.component.scss', }) export class InviteUserDialogComponent { - form: FormArray>>; triedToSubmit = false; @@ -20,7 +19,7 @@ export class InviteUserDialogComponent { private readonly userService: UserService, private readonly dialogRef: DialogRef, private readonly formBuilder: NonNullableFormBuilder, - private readonly uniqueMailValidator: UniqueEmailValidator + private readonly uniqueMailValidator: UniqueEmailValidator, ) { this.form = this.formBuilder.array([this.createUserFormGroup()]); } @@ -53,15 +52,15 @@ export class InviteUserDialogComponent { email: this.formBuilder.control('', [ Validators.required, Validators.minLength(1), - this.uniqueMailValidator.validate.bind(this.uniqueMailValidator) + this.uniqueMailValidator.validate.bind(this.uniqueMailValidator), ]), - }) + }); } private extractAddedMails() { if (!this.form) { return []; } - return this.extractFormValue().map(u => u.email); + return this.extractFormValue().map((u) => u.email); } } diff --git a/frontend/src/app/team-management/new-user/new-user.component.html b/frontend/src/app/team-management/new-user/new-user.component.html index 0d2ba69ea6..3262a86f22 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.html +++ b/frontend/src/app/team-management/new-user/new-user.component.html @@ -12,7 +12,7 @@ [name]="'firstName-col_' + index" /> - @if (firstname.errors?.['required'] && triedToSubmit) { + @if (firstname.errors?.["required"] && triedToSubmit) { Angabe benötigt } @@ -31,7 +31,7 @@ formControlName="lastname" /> - @if (lastname.errors?.['required'] && triedToSubmit) { + @if (lastname.errors?.["required"] && triedToSubmit) { Angabe benötigt } @@ -50,13 +50,13 @@ formControlName="email" /> - @if (email.errors?.["required"] && triedToSubmit) { - Angabe benötigt - } @else if (email.errors?.["notUniqueMail"]) { - E-Mail existiert bereits - } @else if (email.errors?.["email"]) { - E-Mail ungültig - } + @if (email.errors?.["required"] && triedToSubmit) { + Angabe benötigt + } @else if (email.errors?.["notUniqueMail"]) { + E-Mail existiert bereits + } @else if (email.errors?.["email"]) { + E-Mail ungültig + }
diff --git a/frontend/src/app/team-management/new-user/new-user.component.ts b/frontend/src/app/team-management/new-user/new-user.component.ts index 832897569c..2ef11f8b3c 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.ts +++ b/frontend/src/app/team-management/new-user/new-user.component.ts @@ -7,8 +7,8 @@ import { Output, ViewChild, } from '@angular/core'; -import {ControlContainer, FormControl, FormGroup, NgForm} from '@angular/forms'; -import {NewUserForm} from "../../shared/types/model/NewUserForm"; +import { ControlContainer, FormControl, FormGroup, NgForm } from '@angular/forms'; +import { NewUserForm } from '../../shared/types/model/NewUserForm'; @Component({ selector: 'app-new-user', diff --git a/frontend/src/app/team-management/new-user/unique-mail.validator.ts b/frontend/src/app/team-management/new-user/unique-mail.validator.ts index 150444ef00..99d66782d8 100644 --- a/frontend/src/app/team-management/new-user/unique-mail.validator.ts +++ b/frontend/src/app/team-management/new-user/unique-mail.validator.ts @@ -1,7 +1,7 @@ -import {Injectable} from '@angular/core'; -import {AbstractControl, ValidationErrors, Validator} from '@angular/forms'; -import {UserService} from '../../services/user.service'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import { Injectable } from '@angular/core'; +import { AbstractControl, ValidationErrors, Validator } from '@angular/forms'; +import { UserService } from '../../services/user.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Injectable({ providedIn: 'root' }) export class UniqueEmailValidator implements Validator { @@ -17,7 +17,7 @@ export class UniqueEmailValidator implements Validator { }); } - validate(control: AbstractControl, ): ValidationErrors | null { + validate(control: AbstractControl): ValidationErrors | null { const existingUser = this.existingUserMails.concat(this.addedMails).includes(control.value); return existingUser ? { notUniqueMail: { value: control.value } } : null; } diff --git a/frontend/src/app/team-management/team-management.module.ts b/frontend/src/app/team-management/team-management.module.ts index b7e7508a7b..03a57af44e 100644 --- a/frontend/src/app/team-management/team-management.module.ts +++ b/frontend/src/app/team-management/team-management.module.ts @@ -1,47 +1,45 @@ -import {NgModule} from '@angular/core'; -import {CommonModule, NgOptimizedImage} from '@angular/common'; -import {TeamManagementComponent} from './team-management.component'; -import {TeamManagementRoutingModule} from './team-management-routing.module'; -import {AddEditTeamDialog} from './add-edit-team-dialog/add-edit-team-dialog.component'; -import {SharedModule} from '../shared/shared.module'; -import {MatDialogModule} from '@angular/material/dialog'; -import {MatFormFieldModule} from '@angular/material/form-field'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {TeamManagementBannerComponent} from './team-management-banner/team-management-banner.component'; -import {TeamListComponent} from './team-list/team-list.component'; -import {MemberListComponent} from './member-list/member-list.component'; -import {MatListModule} from '@angular/material/list'; -import {MatTableModule} from '@angular/material/table'; -import {RolesPipe} from './roles.pipe'; -import {TeamsPipe} from './teams.pipe'; -import {MatIconModule} from '@angular/material/icon'; -import {MatInputModule} from '@angular/material/input'; -import {MatButtonModule} from '@angular/material/button'; -import {MatMenuModule} from '@angular/material/menu'; -import {SearchTeamManagementComponent} from './search-team-management/search-team-management.component'; -import {AddMemberToTeamDialogComponent} from './add-member-to-team-dialog/add-member-to-team-dialog.component'; -import {MatAutocompleteModule} from '@angular/material/autocomplete'; -import {MemberDetailComponent} from './member-detail/member-detail.component'; -import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; -import {TeamRoleDropdownComponent} from './team-role-dropdown/team-role-dropdown.component'; -import {MatSelectModule} from '@angular/material/select'; -import {TranslateModule} from '@ngx-translate/core'; -import {AddUserTeamComponent} from './add-user-team/add-user-team.component'; -import {A11yModule} from '@angular/cdk/a11y'; -import { - TeamManagementMobileFilterComponent -} from './team-management-mobile-filter/team-management-mobile-filter.component'; -import {MemberListTableComponent} from './member-list/member-list-table/member-list-table.component'; -import {MemberListMobileComponent} from './member-list/member-list-mobile/member-list-mobile.component'; -import {OkrChampionPipe} from './okr-champion.pipe'; -import {PuzzleIconComponent} from '../shared/custom/puzzle-icon/puzzle-icon.component'; -import {PuzzleIconButtonComponent} from '../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; -import {ShowEditRoleComponent} from './show-edit-role/show-edit-role.component'; -import {MatCheckboxModule} from '@angular/material/checkbox'; -import {EditOkrChampionComponent} from './edit-okr-champion/edit-okr-champion.component'; -import {MatTooltipModule} from '@angular/material/tooltip'; -import {NewUserComponent} from './new-user/new-user.component'; -import {InviteUserDialogComponent} from './invite-user-dialog/invite-user-dialog.component'; +import { NgModule } from '@angular/core'; +import { CommonModule, NgOptimizedImage } from '@angular/common'; +import { TeamManagementComponent } from './team-management.component'; +import { TeamManagementRoutingModule } from './team-management-routing.module'; +import { AddEditTeamDialog } from './add-edit-team-dialog/add-edit-team-dialog.component'; +import { SharedModule } from '../shared/shared.module'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { TeamManagementBannerComponent } from './team-management-banner/team-management-banner.component'; +import { TeamListComponent } from './team-list/team-list.component'; +import { MemberListComponent } from './member-list/member-list.component'; +import { MatListModule } from '@angular/material/list'; +import { MatTableModule } from '@angular/material/table'; +import { RolesPipe } from './roles.pipe'; +import { TeamsPipe } from './teams.pipe'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatButtonModule } from '@angular/material/button'; +import { MatMenuModule } from '@angular/material/menu'; +import { SearchTeamManagementComponent } from './search-team-management/search-team-management.component'; +import { AddMemberToTeamDialogComponent } from './add-member-to-team-dialog/add-member-to-team-dialog.component'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MemberDetailComponent } from './member-detail/member-detail.component'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { TeamRoleDropdownComponent } from './team-role-dropdown/team-role-dropdown.component'; +import { MatSelectModule } from '@angular/material/select'; +import { TranslateModule } from '@ngx-translate/core'; +import { AddUserTeamComponent } from './add-user-team/add-user-team.component'; +import { A11yModule } from '@angular/cdk/a11y'; +import { TeamManagementMobileFilterComponent } from './team-management-mobile-filter/team-management-mobile-filter.component'; +import { MemberListTableComponent } from './member-list/member-list-table/member-list-table.component'; +import { MemberListMobileComponent } from './member-list/member-list-mobile/member-list-mobile.component'; +import { OkrChampionPipe } from './okr-champion.pipe'; +import { PuzzleIconComponent } from '../shared/custom/puzzle-icon/puzzle-icon.component'; +import { PuzzleIconButtonComponent } from '../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; +import { ShowEditRoleComponent } from './show-edit-role/show-edit-role.component'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { EditOkrChampionComponent } from './edit-okr-champion/edit-okr-champion.component'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { NewUserComponent } from './new-user/new-user.component'; +import { InviteUserDialogComponent } from './invite-user-dialog/invite-user-dialog.component'; @NgModule({ declarations: [ From b42d8083ba949ca66b18a1c66685a2172e12c4d3 Mon Sep 17 00:00:00 2001 From: e560704 Date: Fri, 12 Apr 2024 13:14:44 +0200 Subject: [PATCH 13/17] #790 fix frontend unit tests --- .../application-top-bar.component.spec.ts | 34 ++++---- .../invite-user-dialog.component.spec.ts | 85 +++++++++++++------ .../new-user/new-user.component.html | 1 + .../new-user/new-user.component.spec.ts | 21 +++-- .../new-user/unique-mail.directive.spec.ts | 20 +++-- 5 files changed, 104 insertions(+), 57 deletions(-) diff --git a/frontend/src/app/components/application-top-bar/application-top-bar.component.spec.ts b/frontend/src/app/components/application-top-bar/application-top-bar.component.spec.ts index b2ac7ec07c..cf1d9b8c0b 100644 --- a/frontend/src/app/components/application-top-bar/application-top-bar.component.spec.ts +++ b/frontend/src/app/components/application-top-bar/application-top-bar.component.spec.ts @@ -1,20 +1,20 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; -import { ApplicationTopBarComponent } from './application-top-bar.component'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { DateTimeProvider, OAuthLogger, OAuthService, UrlHelperService } from 'angular-oauth2-oidc'; -import { HttpClient, HttpHandler } from '@angular/common/http'; -import { MatMenuModule } from '@angular/material/menu'; -import { HarnessLoader } from '@angular/cdk/testing'; -import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; -import { MatMenuHarness } from '@angular/material/menu/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { MatDialog, MatDialogModule } from '@angular/material/dialog'; -import { NavigationEnd, Router } from '@angular/router'; -import { of } from 'rxjs'; -import { testUser } from '../../shared/testData'; -import { UserService } from '../../services/user.service'; -import { ConfigService } from '../../config.service'; +import {ApplicationTopBarComponent} from './application-top-bar.component'; +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {DateTimeProvider, OAuthLogger, OAuthService, UrlHelperService} from 'angular-oauth2-oidc'; +import {HttpClient, HttpHandler} from '@angular/common/http'; +import {MatMenuModule} from '@angular/material/menu'; +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; +import {MatMenuHarness} from '@angular/material/menu/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {MatDialog, MatDialogModule} from '@angular/material/dialog'; +import {NavigationEnd, Router} from '@angular/router'; +import {of} from 'rxjs'; +import {testUser} from '../../shared/testData'; +import {UserService} from '../../services/user.service'; +import {ConfigService} from '../../config.service'; const oAuthMock = { getIdentityClaims: jest.fn(), @@ -39,7 +39,7 @@ const configServiceMock = { config$: of({}), }; -describe('ApplicationHeaderComponent', () => { +describe('ApplicationTopBarComponent', () => { let component: ApplicationTopBarComponent; let fixture: ComponentFixture; let loader: HarnessLoader; diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts index ee15263da7..f86498c500 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts @@ -1,40 +1,62 @@ -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; - -import { InviteUserDialogComponent } from './invite-user-dialog.component'; -import { MatDialogModule } from '@angular/material/dialog'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { NewUserComponent } from '../new-user/new-user.component'; -import { PuzzleIconComponent } from '../../shared/custom/puzzle-icon/puzzle-icon.component'; -import { PuzzleIconButtonComponent } from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; -import { UserService } from '../../services/user.service'; -import { testUser } from '../../shared/testData'; -import { DialogRef } from '@angular/cdk/dialog'; -import { of } from 'rxjs'; +import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; + +import {InviteUserDialogComponent} from './invite-user-dialog.component'; +import {MatDialogModule} from '@angular/material/dialog'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {NewUserComponent} from '../new-user/new-user.component'; +import {PuzzleIconComponent} from '../../shared/custom/puzzle-icon/puzzle-icon.component'; +import {PuzzleIconButtonComponent} from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; +import {UserService} from '../../services/user.service'; +import {testUser} from '../../shared/testData'; +import {DialogRef} from '@angular/cdk/dialog'; +import {of} from 'rxjs'; +import {UniqueEmailValidator} from "../new-user/unique-mail.validator"; +import {MatFormFieldModule} from "@angular/material/form-field"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; describe('InviteUserDialogComponent', () => { let component: InviteUserDialogComponent; let fixture: ComponentFixture; + const user1 = { firstname: 'user1', lastname: '1user', email: 'user1@user.ch' }; + const user2 = { firstname: 'user2', lastname: '2user', email: 'user2@user.ch' }; + const user3 = { firstname: 'user3', lastname: '3user', email: 'user3@user.ch' }; + const userServiceMock = { createUsers: jest.fn(), + getUsers: jest.fn() }; const dialogRefMock = { close: jest.fn(), }; + const uniqueMailValidatorMock = { + setAddedMails: jest.fn(), + validate: () => null + }; + beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [InviteUserDialogComponent, NewUserComponent, PuzzleIconComponent, PuzzleIconButtonComponent], - imports: [MatDialogModule, FormsModule, ReactiveFormsModule], + imports: [MatDialogModule, FormsModule, ReactiveFormsModule, MatFormFieldModule], providers: [ { provide: UserService, useValue: userServiceMock }, { provide: DialogRef, useValue: dialogRefMock }, + { provide: UniqueEmailValidator, useValue: uniqueMailValidatorMock }, ], - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .compileComponents(); fixture = TestBed.createComponent(InviteUserDialogComponent); component = fixture.componentInstance; + + userServiceMock.createUsers.mockReset(); + dialogRefMock.close.mockReset(); + + userServiceMock.getUsers.mockReturnValue(of([])); + fixture.detectChanges(); }); @@ -44,27 +66,42 @@ describe('InviteUserDialogComponent', () => { it('addUser should add a user to the existing users', () => { component.addUser(); - expect(component.users.length).toBe(2); + expect(component.form.controls.length).toBe(2); }); it('removeUser should remove given user from users array', () => { - const user1 = { firstname: 'user1', lastname: '1user', email: 'user1@user.ch' }; - const user2 = { firstname: 'user2', lastname: '2user', email: 'user2@user.ch' }; - const user3 = { firstname: 'user3', lastname: '3user', email: 'user3@user.ch' }; - component.users = [user1, user2, user3]; + component.addUser(); + component.addUser(); + component.addUser(); + + component.form.controls[0].setValue(user1); + component.form.controls[1].setValue(user2); + component.form.controls[2].setValue(user3); - component.removeUser(user2); + component.removeUser(1); - expect(component.users).toStrictEqual([user1, user3]); + expect(component.form.controls[0].value).toStrictEqual(user1); + expect(component.form.controls[1].value).toStrictEqual(user3); }); - it('inviteUsers should call createUsers and close dialog', fakeAsync(() => { + it('registerUsers should call createUsers and close dialog if form is valid', fakeAsync(() => { userServiceMock.createUsers.mockReturnValue(of([testUser])); - component.inviteUsers(); + + component.form.controls[0].setValue(user1); + + component.registerUsers(); tick(); expect(userServiceMock.createUsers).toBeCalledTimes(1); - expect(userServiceMock.createUsers).toBeCalledWith(component.users); + expect(userServiceMock.createUsers).toBeCalledWith(component.form.value); expect(dialogRefMock.close).toBeCalledTimes(1); })); + + it('registerUsers should not call createUsers form is not valid', fakeAsync(() => { + component.registerUsers(); + tick(); + + expect(userServiceMock.createUsers).toBeCalledTimes(0); + expect(dialogRefMock.close).toBeCalledTimes(0); + })); }); diff --git a/frontend/src/app/team-management/new-user/new-user.component.html b/frontend/src/app/team-management/new-user/new-user.component.html index 0d2ba69ea6..023567898e 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.html +++ b/frontend/src/app/team-management/new-user/new-user.component.html @@ -8,6 +8,7 @@ type="text" class="value-field" formControlName="firstname" + #firstInput [id]="'firstName-col_' + index" [name]="'firstName-col_' + index" /> diff --git a/frontend/src/app/team-management/new-user/new-user.component.spec.ts b/frontend/src/app/team-management/new-user/new-user.component.spec.ts index e4b6eb2734..249911e8e2 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.spec.ts +++ b/frontend/src/app/team-management/new-user/new-user.component.spec.ts @@ -1,11 +1,12 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; -import { NewUserComponent } from './new-user.component'; -import { FormsModule, NgForm, ReactiveFormsModule } from '@angular/forms'; -import { SharedModule } from '../../shared/shared.module'; -import { PuzzleIconButtonComponent } from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; -import { PuzzleIconComponent } from '../../shared/custom/puzzle-icon/puzzle-icon.component'; -import { CommonModule } from '@angular/common'; +import {NewUserComponent} from './new-user.component'; +import {FormControl, FormGroup, FormsModule, NgForm, ReactiveFormsModule} from '@angular/forms'; +import {SharedModule} from '../../shared/shared.module'; +import {PuzzleIconButtonComponent} from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; +import {PuzzleIconComponent} from '../../shared/custom/puzzle-icon/puzzle-icon.component'; +import {CommonModule} from '@angular/common'; +import {NewUserForm} from "../../shared/types/model/NewUserForm"; describe('NewUserComponent', () => { let component: NewUserComponent; @@ -21,7 +22,11 @@ describe('NewUserComponent', () => { fixture = TestBed.createComponent(NewUserComponent); component = fixture.componentInstance; - component.user = { firstname: '', lastname: '', email: '' }; + component.userFormGroup = new FormGroup>({ + firstname: new FormControl('user1'), + lastname: new FormControl('user'), + email: new FormControl('test@test.ch'), + }); fixture.detectChanges(); }); diff --git a/frontend/src/app/team-management/new-user/unique-mail.directive.spec.ts b/frontend/src/app/team-management/new-user/unique-mail.directive.spec.ts index aa4d8068da..cb85d69a82 100644 --- a/frontend/src/app/team-management/new-user/unique-mail.directive.spec.ts +++ b/frontend/src/app/team-management/new-user/unique-mail.directive.spec.ts @@ -1,8 +1,8 @@ -import { UniqueEmailValidatorDirective } from './unique-mail.directive'; -import { users } from '../../shared/testData'; -import { TestBed } from '@angular/core/testing'; -import { of } from 'rxjs'; -import { AbstractControl } from '@angular/forms'; +import {UniqueEmailValidator} from './unique-mail.validator'; +import {users} from '../../shared/testData'; +import {TestBed} from '@angular/core/testing'; +import {of} from 'rxjs'; +import {AbstractControl} from '@angular/forms'; describe('UniqueMailDirective', () => { const userServiceMock = { @@ -15,21 +15,25 @@ describe('UniqueMailDirective', () => { it('should create an instance', () => { TestBed.runInInjectionContext(() => { - const directive = new UniqueEmailValidatorDirective(userServiceMock); + const directive = new UniqueEmailValidator(userServiceMock); expect(directive).toBeTruthy(); }); }); it('should return validationError if user exists, otherwise null', () => { TestBed.runInInjectionContext(() => { - const directive = new UniqueEmailValidatorDirective(userServiceMock); + const directive = new UniqueEmailValidator(userServiceMock); let control = { value: users[0].email } as AbstractControl; expect(directive.validate(control)).toStrictEqual({ notUniqueMail: { value: users[0].email } }); - control = { value: 'notexistinguser@test.com' } as AbstractControl; + const notExistingMail = 'notexistinguser@test.com'; + control = { value: notExistingMail} as AbstractControl; expect(directive.validate(control)).toStrictEqual(null); + directive.setAddedMails([notExistingMail]); + expect(directive.validate(control)).toStrictEqual({ notUniqueMail: { value: notExistingMail } }); + expect(directive).toBeTruthy(); }); }); From 15c1024bc9b6b7a3a18750b0ec02845aa4eb470b Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Fri, 12 Apr 2024 11:18:43 +0000 Subject: [PATCH 14/17] [FM] Automated formating frontend --- .../application-top-bar.component.spec.ts | 32 ++++++++-------- .../invite-user-dialog.component.spec.ts | 37 +++++++++---------- .../new-user/new-user.component.spec.ts | 16 ++++---- .../new-user/unique-mail.directive.spec.ts | 12 +++--- 4 files changed, 48 insertions(+), 49 deletions(-) diff --git a/frontend/src/app/components/application-top-bar/application-top-bar.component.spec.ts b/frontend/src/app/components/application-top-bar/application-top-bar.component.spec.ts index cf1d9b8c0b..3bde42ff74 100644 --- a/frontend/src/app/components/application-top-bar/application-top-bar.component.spec.ts +++ b/frontend/src/app/components/application-top-bar/application-top-bar.component.spec.ts @@ -1,20 +1,20 @@ -import {ComponentFixture, TestBed} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; -import {ApplicationTopBarComponent} from './application-top-bar.component'; -import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; -import {DateTimeProvider, OAuthLogger, OAuthService, UrlHelperService} from 'angular-oauth2-oidc'; -import {HttpClient, HttpHandler} from '@angular/common/http'; -import {MatMenuModule} from '@angular/material/menu'; -import {HarnessLoader} from '@angular/cdk/testing'; -import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; -import {MatMenuHarness} from '@angular/material/menu/testing'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {MatDialog, MatDialogModule} from '@angular/material/dialog'; -import {NavigationEnd, Router} from '@angular/router'; -import {of} from 'rxjs'; -import {testUser} from '../../shared/testData'; -import {UserService} from '../../services/user.service'; -import {ConfigService} from '../../config.service'; +import { ApplicationTopBarComponent } from './application-top-bar.component'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { DateTimeProvider, OAuthLogger, OAuthService, UrlHelperService } from 'angular-oauth2-oidc'; +import { HttpClient, HttpHandler } from '@angular/common/http'; +import { MatMenuModule } from '@angular/material/menu'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatMenuHarness } from '@angular/material/menu/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { NavigationEnd, Router } from '@angular/router'; +import { of } from 'rxjs'; +import { testUser } from '../../shared/testData'; +import { UserService } from '../../services/user.service'; +import { ConfigService } from '../../config.service'; const oAuthMock = { getIdentityClaims: jest.fn(), diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts index f86498c500..52e589add3 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts @@ -1,18 +1,18 @@ -import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; - -import {InviteUserDialogComponent} from './invite-user-dialog.component'; -import {MatDialogModule} from '@angular/material/dialog'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; -import {NewUserComponent} from '../new-user/new-user.component'; -import {PuzzleIconComponent} from '../../shared/custom/puzzle-icon/puzzle-icon.component'; -import {PuzzleIconButtonComponent} from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; -import {UserService} from '../../services/user.service'; -import {testUser} from '../../shared/testData'; -import {DialogRef} from '@angular/cdk/dialog'; -import {of} from 'rxjs'; -import {UniqueEmailValidator} from "../new-user/unique-mail.validator"; -import {MatFormFieldModule} from "@angular/material/form-field"; -import {NO_ERRORS_SCHEMA} from "@angular/core"; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; + +import { InviteUserDialogComponent } from './invite-user-dialog.component'; +import { MatDialogModule } from '@angular/material/dialog'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { NewUserComponent } from '../new-user/new-user.component'; +import { PuzzleIconComponent } from '../../shared/custom/puzzle-icon/puzzle-icon.component'; +import { PuzzleIconButtonComponent } from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; +import { UserService } from '../../services/user.service'; +import { testUser } from '../../shared/testData'; +import { DialogRef } from '@angular/cdk/dialog'; +import { of } from 'rxjs'; +import { UniqueEmailValidator } from '../new-user/unique-mail.validator'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; describe('InviteUserDialogComponent', () => { let component: InviteUserDialogComponent; @@ -24,7 +24,7 @@ describe('InviteUserDialogComponent', () => { const userServiceMock = { createUsers: jest.fn(), - getUsers: jest.fn() + getUsers: jest.fn(), }; const dialogRefMock = { @@ -33,7 +33,7 @@ describe('InviteUserDialogComponent', () => { const uniqueMailValidatorMock = { setAddedMails: jest.fn(), - validate: () => null + validate: () => null, }; beforeEach(async () => { @@ -46,8 +46,7 @@ describe('InviteUserDialogComponent', () => { { provide: UniqueEmailValidator, useValue: uniqueMailValidatorMock }, ], schemas: [NO_ERRORS_SCHEMA], - }) - .compileComponents(); + }).compileComponents(); fixture = TestBed.createComponent(InviteUserDialogComponent); component = fixture.componentInstance; diff --git a/frontend/src/app/team-management/new-user/new-user.component.spec.ts b/frontend/src/app/team-management/new-user/new-user.component.spec.ts index 249911e8e2..3934b994ad 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.spec.ts +++ b/frontend/src/app/team-management/new-user/new-user.component.spec.ts @@ -1,12 +1,12 @@ -import {ComponentFixture, TestBed} from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; -import {NewUserComponent} from './new-user.component'; -import {FormControl, FormGroup, FormsModule, NgForm, ReactiveFormsModule} from '@angular/forms'; -import {SharedModule} from '../../shared/shared.module'; -import {PuzzleIconButtonComponent} from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; -import {PuzzleIconComponent} from '../../shared/custom/puzzle-icon/puzzle-icon.component'; -import {CommonModule} from '@angular/common'; -import {NewUserForm} from "../../shared/types/model/NewUserForm"; +import { NewUserComponent } from './new-user.component'; +import { FormControl, FormGroup, FormsModule, NgForm, ReactiveFormsModule } from '@angular/forms'; +import { SharedModule } from '../../shared/shared.module'; +import { PuzzleIconButtonComponent } from '../../shared/custom/puzzle-icon-button/puzzle-icon-button.component'; +import { PuzzleIconComponent } from '../../shared/custom/puzzle-icon/puzzle-icon.component'; +import { CommonModule } from '@angular/common'; +import { NewUserForm } from '../../shared/types/model/NewUserForm'; describe('NewUserComponent', () => { let component: NewUserComponent; diff --git a/frontend/src/app/team-management/new-user/unique-mail.directive.spec.ts b/frontend/src/app/team-management/new-user/unique-mail.directive.spec.ts index cb85d69a82..7e8a654721 100644 --- a/frontend/src/app/team-management/new-user/unique-mail.directive.spec.ts +++ b/frontend/src/app/team-management/new-user/unique-mail.directive.spec.ts @@ -1,8 +1,8 @@ -import {UniqueEmailValidator} from './unique-mail.validator'; -import {users} from '../../shared/testData'; -import {TestBed} from '@angular/core/testing'; -import {of} from 'rxjs'; -import {AbstractControl} from '@angular/forms'; +import { UniqueEmailValidator } from './unique-mail.validator'; +import { users } from '../../shared/testData'; +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { AbstractControl } from '@angular/forms'; describe('UniqueMailDirective', () => { const userServiceMock = { @@ -28,7 +28,7 @@ describe('UniqueMailDirective', () => { expect(directive.validate(control)).toStrictEqual({ notUniqueMail: { value: users[0].email } }); const notExistingMail = 'notexistinguser@test.com'; - control = { value: notExistingMail} as AbstractControl; + control = { value: notExistingMail } as AbstractControl; expect(directive.validate(control)).toStrictEqual(null); directive.setAddedMails([notExistingMail]); From b081bfe4a1683cf5cb81a07a215c588aa13400e6 Mon Sep 17 00:00:00 2001 From: e560704 Date: Fri, 12 Apr 2024 13:48:39 +0200 Subject: [PATCH 15/17] #790 improve email validation unique-mail validator - filter empty strings - update added mails every time form changes added general email validation --- .../invite-user-dialog/invite-user-dialog.component.ts | 10 ++++++++-- .../team-management/new-user/new-user.component.html | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts index 0d50297c80..36b8d1ac15 100644 --- a/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts @@ -5,6 +5,7 @@ import { UserService } from '../../services/user.service'; import { DialogRef } from '@angular/cdk/dialog'; import { NewUserForm } from '../../shared/types/model/NewUserForm'; import { UniqueEmailValidator } from '../new-user/unique-mail.validator'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'app-invite-user-dialog', @@ -22,6 +23,9 @@ export class InviteUserDialogComponent { private readonly uniqueMailValidator: UniqueEmailValidator, ) { this.form = this.formBuilder.array([this.createUserFormGroup()]); + this.form.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(() => this.uniqueMailValidator.setAddedMails(this.extractAddedMails())); } registerUsers() { @@ -45,13 +49,13 @@ export class InviteUserDialogComponent { } private createUserFormGroup() { - this.uniqueMailValidator.setAddedMails(this.extractAddedMails()); return this.formBuilder.group({ firstname: this.formBuilder.control('', [Validators.required, Validators.minLength(1)]), lastname: this.formBuilder.control('', [Validators.required, Validators.minLength(1)]), email: this.formBuilder.control('', [ Validators.required, Validators.minLength(1), + Validators.email, this.uniqueMailValidator.validate.bind(this.uniqueMailValidator), ]), }); @@ -61,6 +65,8 @@ export class InviteUserDialogComponent { if (!this.form) { return []; } - return this.extractFormValue().map((u) => u.email); + return this.extractFormValue() + .map((u) => u.email) + .filter((mail) => !!mail); } } diff --git a/frontend/src/app/team-management/new-user/new-user.component.html b/frontend/src/app/team-management/new-user/new-user.component.html index dab4fd004d..cd53093d0f 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.html +++ b/frontend/src/app/team-management/new-user/new-user.component.html @@ -55,7 +55,7 @@ Angabe benötigt } @else if (email.errors?.["notUniqueMail"]) { E-Mail existiert bereits - } @else if (email.errors?.["email"]) { + } @else if (email.errors?.["email"] && triedToSubmit) { E-Mail ungültig } From b609505319322f2eeb6e4dba412cb324b2c5cee8 Mon Sep 17 00:00:00 2001 From: e560704 Date: Fri, 12 Apr 2024 13:57:55 +0200 Subject: [PATCH 16/17] #790 fix padding in input fields --- .../app/team-management/new-user/new-user.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/team-management/new-user/new-user.component.html b/frontend/src/app/team-management/new-user/new-user.component.html index cd53093d0f..e8574dfc6e 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.html +++ b/frontend/src/app/team-management/new-user/new-user.component.html @@ -6,7 +6,7 @@ } @@ -47,7 +47,7 @@ type="email" [id]="'email-col_' + index" [name]="'email-col_' + index" - class="value-field" + class="value-field dialog-form-field" formControlName="email" /> @@ -62,7 +62,7 @@
-
+
@if (index === 0) { } From 419b424dfa81086c360378f9be6a0d133dcf610c Mon Sep 17 00:00:00 2001 From: Janik Endtner Date: Fri, 17 May 2024 15:54:48 +0200 Subject: [PATCH 17/17] #790 fix test error messages --- frontend/cypress/e2e/teammanagement.cy.ts | 2 ++ .../src/app/team-management/new-user/new-user.component.html | 1 + 2 files changed, 3 insertions(+) diff --git a/frontend/cypress/e2e/teammanagement.cy.ts b/frontend/cypress/e2e/teammanagement.cy.ts index 6a19addd13..627e6c95da 100644 --- a/frontend/cypress/e2e/teammanagement.cy.ts +++ b/frontend/cypress/e2e/teammanagement.cy.ts @@ -160,8 +160,10 @@ describe('Team management tests', () => { // test error messages fillOutNewUser('Robin', '', 'papierer'); + cy.getByTestId('invite').click(); cy.contains('Angabe benötigt'); cy.contains('E-Mail ungültig'); + cy.getByTestId('email-col_2').focus(); cy.realType('@puzzle.ch'); cy.contains('E-Mail ungültig').should('not.exist'); cy.contains('E-Mail existiert bereits'); diff --git a/frontend/src/app/team-management/new-user/new-user.component.html b/frontend/src/app/team-management/new-user/new-user.component.html index e8574dfc6e..c05f8b7764 100644 --- a/frontend/src/app/team-management/new-user/new-user.component.html +++ b/frontend/src/app/team-management/new-user/new-user.component.html @@ -46,6 +46,7 @@