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..bc6d772a9a 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; @@ -10,7 +11,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -68,4 +75,14 @@ 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 users to create", 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/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/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..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 @@ -52,4 +52,10 @@ 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..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,4 +39,8 @@ 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))); + } } diff --git a/frontend/cypress/e2e/teammanagement.cy.ts b/frontend/cypress/e2e/teammanagement.cy.ts index 5f6951a78d..627e6c95da 100644 --- a/frontend/cypress/e2e/teammanagement.cy.ts +++ b/frontend/cypress/e2e/teammanagement.cy.ts @@ -141,6 +141,50 @@ 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.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'); + 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 registriert'); + cy.contains(firstNameClaudia); + cy.contains(firstNameStefan); + }); + }); + it('Navigate to Bobs profile and add him to BBT and LoremIpsum', () => { cy.intercept('PUT', '**/updateaddteammembership/*').as('updateEsha'); @@ -202,7 +246,7 @@ describe('Team management tests', () => { cy.getByTestId('edit-okr-champion-checkbox').click(); cy.getByTestId('edit-okr-champion-readonly').contains('OKR Champion:'); cy.getByTestId('edit-okr-champion-readonly').contains('Ja'); - cy.contains('Der User wurde erfolgreich aktualisiert.'); + cy.contains('Der Member wurde erfolgreich aktualisiert.'); // reset okr champion to false cy.getByTestId('edit-okr-champion-edit').click(); @@ -362,3 +406,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/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 d9cd32b815..f22fa27bc9 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 @@ -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/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/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/shared/types/model/NewUserForm.ts b/frontend/src/app/shared/types/model/NewUserForm.ts new file mode 100644 index 0000000000..800df9f04a --- /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 new file mode 100644 index 0000000000..0ebeead56d --- /dev/null +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.html @@ -0,0 +1,41 @@ +
+
+ + Members registrieren + +
+
+ +
+
+ @for (userFormGroup of form.controls; 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..52e589add3 --- /dev/null +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.spec.ts @@ -0,0 +1,106 @@ +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, MatFormFieldModule], + providers: [ + { provide: UserService, useValue: userServiceMock }, + { provide: DialogRef, useValue: dialogRefMock }, + { provide: UniqueEmailValidator, useValue: uniqueMailValidatorMock }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(InviteUserDialogComponent); + component = fixture.componentInstance; + + userServiceMock.createUsers.mockReset(); + dialogRefMock.close.mockReset(); + + userServiceMock.getUsers.mockReturnValue(of([])); + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('addUser should add a user to the existing users', () => { + component.addUser(); + expect(component.form.controls.length).toBe(2); + }); + + it('removeUser should remove given user from users array', () => { + 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(1); + + expect(component.form.controls[0].value).toStrictEqual(user1); + expect(component.form.controls[1].value).toStrictEqual(user3); + }); + + it('registerUsers should call createUsers and close dialog if form is valid', fakeAsync(() => { + userServiceMock.createUsers.mockReturnValue(of([testUser])); + + component.form.controls[0].setValue(user1); + + component.registerUsers(); + tick(); + + expect(userServiceMock.createUsers).toBeCalledTimes(1); + 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/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..36b8d1ac15 --- /dev/null +++ b/frontend/src/app/team-management/invite-user-dialog/invite-user-dialog.component.ts @@ -0,0 +1,72 @@ +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 { takeUntilDestroyed } from '@angular/core/rxjs-interop'; + +@Component({ + selector: 'app-invite-user-dialog', + templateUrl: './invite-user-dialog.component.html', + styleUrl: './invite-user-dialog.component.scss', +}) +export class InviteUserDialogComponent { + 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()]); + this.form.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(() => this.uniqueMailValidator.setAddedMails(this.extractAddedMails())); + } + + registerUsers() { + this.triedToSubmit = true; + if (!this.form.valid) { + return; + } + this.userService.createUsers(this.extractFormValue()).subscribe(() => this.dialogRef.close()); + } + + private extractFormValue(): NewUser[] { + return this.form.value as NewUser[]; + } + + addUser() { + this.form.push(this.createUserFormGroup()); + } + + removeUser(index: number) { + this.form.removeAt(index); + } + + private createUserFormGroup() { + 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), + ]), + }); + } + + private extractAddedMails() { + if (!this.form) { + return []; + } + return this.extractFormValue() + .map((u) => u.email) + .filter((mail) => !!mail); + } +} 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..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 @@ -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) { @@ -127,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/member-list/member-list.component.html b/frontend/src/app/team-management/member-list/member-list.component.html index 43909f8af8..24d6ff39ff 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 +