diff --git a/ui/src/app/account/password/password.component.ts b/ui/src/app/account/password/password.component.ts index a6852c77b..413c351d9 100644 --- a/ui/src/app/account/password/password.component.ts +++ b/ui/src/app/account/password/password.component.ts @@ -31,7 +31,6 @@ export class PasswordComponent implements OnInit { this.account = account this.username = this.accountService.getUsername() this.passwordForUsernameString = $localize`:@@password.title.string:Password for ${this.username} (You)` - console.log('username:', this.username) }) } diff --git a/ui/src/app/app.constants.ts b/ui/src/app/app.constants.ts index e51c8fdc3..e26c7ccd7 100644 --- a/ui/src/app/app.constants.ts +++ b/ui/src/app/app.constants.ts @@ -1,3 +1,5 @@ +import { FormControl } from "@angular/forms"; + export enum EventType { LOG_IN_SUCCESS = 'LOG_IN_SUCCESS', AFFILIATION_CREATED = 'AFFILIATION_CREATED', @@ -16,3 +18,12 @@ export const DATE_FORMAT = 'YYYY-MM-DD' export const DATE_TIME_FORMAT = 'YYYY-MM-DDTHH:mm' export const ITEMS_PER_PAGE = 20 + +export function emailValidator(control: FormControl): { [key: string]: any } | null { + // eslint-disable-next-line + const emailRegexp = /^([^@\s/."'\(\)\[\]\{\}\\/,:;]+\.)*([^@\s\."\(\)\[\]\{\}\\/,:;]|(".+"))+@[^@\s\."'\(\)\[\]\{\}\\/,:;]+(\.[^@\s\."'\(\)\[\]\{\}\\/,:;]{2,})+$/; + if (control.value && !emailRegexp.test(control.value)) { + return { invalidEmail: true }; + } + return null +} \ No newline at end of file diff --git a/ui/src/app/member/model/member.model.ts b/ui/src/app/member/model/member.model.ts index 558306881..a330f5c60 100644 --- a/ui/src/app/member/model/member.model.ts +++ b/ui/src/app/member/model/member.model.ts @@ -11,9 +11,9 @@ export interface IMember { superadminEnabled?: boolean assertionServiceEnabled?: boolean createdBy?: string - createdDate?: Moment + createdDate?: Moment | undefined | null lastModifiedBy?: string - lastModifiedDate?: Moment + lastModifiedDate?: Moment | undefined | null type?: string status?: string defaultLanguage?: string @@ -31,9 +31,9 @@ export class Member implements IMember { public superadminEnabled?: boolean, public assertionServiceEnabled?: boolean, public createdBy?: string, - public createdDate?: Moment, + public createdDate?: Moment | undefined | null, public lastModifiedBy?: string, - public lastModifiedDate?: Moment, + public lastModifiedDate?: Moment | undefined | null, public type?: string, public status?: string, public defaultLaungage?: string diff --git a/ui/src/app/member/service/member.service.ts b/ui/src/app/member/service/member.service.ts index e4a1c25ed..c83654393 100644 --- a/ui/src/app/member/service/member.service.ts +++ b/ui/src/app/member/service/member.service.ts @@ -5,6 +5,7 @@ import { IMember } from '../model/member.model' import * as moment from 'moment' type EntityResponseType = HttpResponse +type EntityArrayResponseType = HttpResponse; @Injectable({ providedIn: 'root' }) export class MemberService { @@ -26,6 +27,12 @@ export class MemberService { ) } + getAllMembers(): Observable { + return this.http + .get(`${this.resourceUrl}/members/list/all`, { observe: 'response' }) + .pipe(map((res: EntityArrayResponseType) => this.convertDateArrayFromServer(res))); + } + getManagedMember(): Observable { return this.managedMember.asObservable() } @@ -41,4 +48,14 @@ export class MemberService { } return res.body } + + protected convertDateArrayFromServer(res: EntityArrayResponseType): EntityArrayResponseType { + if (res.body) { + res.body.forEach((member: IMember) => { + member.createdDate = member.createdDate != null ? moment(member.createdDate) : null; + member.lastModifiedDate = member.lastModifiedDate != null ? moment(member.lastModifiedDate) : null; + }); + } + return res; + } } diff --git a/ui/src/app/user/user-detail.component.spec.ts b/ui/src/app/user/user-detail.component.spec.ts index ec8476b4b..e3c48fe95 100644 --- a/ui/src/app/user/user-detail.component.spec.ts +++ b/ui/src/app/user/user-detail.component.spec.ts @@ -8,7 +8,6 @@ import { UserService } from './service/user.service' import { MemberService } from '../member/service/member.service' import { User } from './model/user.model' import { of } from 'rxjs' -import { Member } from '../member/model/member.model' describe('UserDetailComponent', () => { let component: UserDetailComponent diff --git a/ui/src/app/user/user-update.component.html b/ui/src/app/user/user-update.component.html new file mode 100644 index 000000000..a181ee422 --- /dev/null +++ b/ui/src/app/user/user-update.component.html @@ -0,0 +1,149 @@ +
+
+
+

+ Create or edit a User Settings +

+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ + +
+
+
+ + +
+
+
+
+ + + + + +
+
+
+
diff --git a/ui/src/app/user/user-update.component.scss b/ui/src/app/user/user-update.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/ui/src/app/user/user-update.component.spec.ts b/ui/src/app/user/user-update.component.spec.ts new file mode 100644 index 000000000..41e8af1e3 --- /dev/null +++ b/ui/src/app/user/user-update.component.spec.ts @@ -0,0 +1,145 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { of } from 'rxjs'; +import { UserUpdateComponent } from './user-update.component'; +import { UserService } from './service/user.service'; +import { AccountService } from '../account'; +import { MemberService } from '../member/service/member.service'; +import { AlertService } from '../shared/service/alert.service'; +import { ErrorService } from '../error/service/error.service'; +import { IUser, User } from './model/user.model'; +import { Member } from '../member/model/member.model'; +import { UserValidation } from './model/user-validation.model'; + +describe('UserUpdateComponent', () => { + let component: UserUpdateComponent; + let fixture: ComponentFixture; + let userService: jasmine.SpyObj; + let accountService: jasmine.SpyObj; + let alertService: jasmine.SpyObj; + let memberService: jasmine.SpyObj; + + beforeEach(() => { + const userServiceSpy = jasmine.createSpyObj('UserService', [ + 'validate', + 'update', + 'sendActivate', + 'hasOwner', + 'create', + 'update' + ]); + const accountServiceSpy = jasmine.createSpyObj('AccountService', [ + 'getAccountData', + 'hasAnyAuthority', + 'getSalesforceId' + ]); + const alertServiceSpy = jasmine.createSpyObj('AlertService', ['broadcast']); + const memberServiceSpy = jasmine.createSpyObj('MemberService', [ + 'find', + ]); + + TestBed.configureTestingModule({ + declarations: [UserUpdateComponent], + imports: [ReactiveFormsModule], + providers: [ + FormBuilder, + { provide: ActivatedRoute, useValue: { data: of({ user: {salesforceId: 'test'} as IUser }) } }, + // rewrite in the same way as other services + // eslint-disable-next-line + { provide: Router, useValue: { navigate: () => {} } }, + { provide: UserService, useValue: userServiceSpy }, + { provide: AccountService, useValue: accountServiceSpy }, + { provide: MemberService, useValue: memberServiceSpy }, + { provide: AlertService, useValue: alertServiceSpy }, + { provide: ErrorService, useValue: {} }, + ] + }).compileComponents(); + + fixture = TestBed.createComponent(UserUpdateComponent); + component = fixture.componentInstance; + userService = TestBed.inject(UserService) as jasmine.SpyObj; + accountService = TestBed.inject(AccountService) as jasmine.SpyObj; + alertService = TestBed.inject(AlertService) as jasmine.SpyObj; + memberService = TestBed.inject(MemberService) as jasmine.SpyObj; + + accountService.getAccountData.and.returnValue(of({ + activated: true, + authorities: ['ROLE_USER'], + email: 'email@email.com', + firstName: 'name', + langKey: 'en', + lastName: 'surname', + imageUrl: 'url', + salesforceId: 'sfid', + loggedAs: false, + loginAs: 'sfid', + mainContact: false, + mfaEnabled: false, + })); + userService.hasOwner.and.returnValue(of(true)); + userService.validate.and.returnValue(of(new UserValidation(true, null))); + userService.update.and.returnValue(of({})); + userService.sendActivate.and.returnValue(of(new User())); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should navigate to users list', () => { + const router = TestBed.inject(Router); + const navigateSpy = spyOn(router, 'navigate'); + component.navigateToUsersList(); + expect(navigateSpy).toHaveBeenCalledWith(['/users']); + }); + + it('should disable salesforceId dropdown for non-admin users', fakeAsync(() => { + accountService.hasAnyAuthority.and.returnValue(false); + memberService.find.and.returnValue(of(new Member())) + component.isExistentMember = true; + + tick(); + fixture.detectChanges(); + expect(component.disableSalesForceIdDD()).toBe(true); + })); + + it('should enable salesforceId dropdown for admin users', () => { + accountService.hasAnyAuthority.and.returnValue(true); + expect(component.disableSalesForceIdDD()).toBe(false); + }); + + it('should validate org owners', () => { + component.editForm.patchValue({ salesforceId: '123', mainContact: true }); + component.validateOrgOwners(); + expect(component.hasOwner).toBe(true); + expect(component.editForm.get('salesforceId')?.disabled).toBe(true); + }); + +/* fit('should create new user', fakeAsync(() => { + component.isExistentMember = false; + + component.editForm.patchValue({salesforceId: 'sfid', email: "test@test.com", firstName: "firstName", lastName: "lastName", activated: false, }) + console.log(component.editForm.value); + + userService.create.and.returnValue(of(new User())) + component.save(); + tick(); + expect(userService.validate).toHaveBeenCalled(); + expect(userService.create).toHaveBeenCalled(); + })); */ + + it('should send activation email for existing user', fakeAsync(() => { + component.existentUser = { email: 'test@example.com', activated: false } as IUser; + component.sendActivate(); + tick(); + expect(userService.sendActivate).toHaveBeenCalled(); + })); + + it('should display send activation option for existing user with unactivated email', () => { + component.existentUser = { email: 'test@example.com', activated: false } as IUser; + expect(component.displaySendActivate()).toBe(true); + }); + +}); diff --git a/ui/src/app/user/user-update.component.ts b/ui/src/app/user/user-update.component.ts new file mode 100644 index 000000000..044b3ddcf --- /dev/null +++ b/ui/src/app/user/user-update.component.ts @@ -0,0 +1,339 @@ +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { FormBuilder, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { EMPTY, Observable } from 'rxjs'; +import * as moment from 'moment'; +import { faBan, faCheckCircle, faSave } from '@fortawesome/free-solid-svg-icons'; + + +import { map } from 'rxjs/operators'; +import { AlertService } from '../shared/service/alert.service'; +import { UserService } from './service/user.service'; +import { MemberService } from '../member/service/member.service'; +import { AccountService } from '../account'; +import { IUser, User } from './model/user.model'; +import { IMember } from '../member/model/member.model'; +import { ErrorService } from '../error/service/error.service'; +import { DATE_TIME_FORMAT, emailValidator } from '../app.constants'; + +@Component({ + selector: 'app-user-update', + templateUrl: './user-update.component.html', + styleUrls: ['./user-update.component.scss'] +}) +export class UserUpdateComponent { + + + isSaving = false; + isExistentMember = false; + existentUser: IUser | null = null; + faCheckCircle = faCheckCircle; + faBan = faBan; + faSave = faSave; + showIsAdminCheckbox = false; + currentAccount: any; + validation: any; + + editForm = this.fb.group({ + id: [''], + email: ['', [Validators.required, Validators.email, Validators.maxLength(50), emailValidator]], + firstName: ['', Validators.required], + lastName: ['', Validators.required], + mainContact: [false], + assertionServiceEnabled: [], + salesforceId: ['', Validators.required], + activated: [false], + isAdmin: [false], + createdBy: [''], + createdDate: [''], + lastModifiedBy: [''], + lastModifiedDate: [''] + }); + + memberList = [] as IMember[]; + hasOwner = false; + + constructor( + protected alertService: AlertService, + protected userService: UserService, + protected memberService: MemberService, + protected activatedRoute: ActivatedRoute, + protected router: Router, + protected accountService: AccountService, + protected errorService: ErrorService, + private fb: FormBuilder, + private cdref: ChangeDetectorRef + ) { + this.validation = {}; + } + + ngOnInit() { + this.isSaving = false; + this.isExistentMember = false; + this.existentUser = null; + this.activatedRoute.data.subscribe(({ user }) => { + this.existentUser = user; + }); + this.editForm.disable(); + this.accountService.getAccountData().subscribe(account => { + this.currentAccount = account; + this.getMemberList().subscribe((list: IMember[]) => { + list.forEach((msMember: IMember) => { + this.memberList.push(msMember); + }); + this.editForm.enable(); + if (this.existentUser) { + this.updateForm(this.existentUser); + } + }); + }); + this.cdref.detectChanges(); + this.onChanges(); + } + + onChanges(): void { + this.editForm.get('salesforceId')?.valueChanges.subscribe(val => { + const selectedOrg = this.memberList.find(cm => cm.salesforceId === this.editForm.get(['salesforceId'])?.value); + if (this.hasRoleAdmin()) { + if (selectedOrg) { + this.showIsAdminCheckbox = selectedOrg.superadminEnabled || false; + } else { + this.showIsAdminCheckbox = false; + } + } else { + this.showIsAdminCheckbox = false; + } + }); + } + + updateForm(user: IUser) { + this.editForm.patchValue({ + id: user.id, + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + mainContact: user.mainContact, + salesforceId: user.salesforceId, + activated: user.activated, + isAdmin: user.isAdmin, + createdBy: user.createdBy, + createdDate: user.createdDate != null ? user.createdDate.format(DATE_TIME_FORMAT) : null, + lastModifiedBy: user.lastModifiedBy, + lastModifiedDate: user.lastModifiedDate != null ? user.lastModifiedDate.format(DATE_TIME_FORMAT) : null + }); + + if (user.mainContact) { + this.editForm.get('mainContact')?.disable(); + this.editForm.get('salesforceId')?.disable(); + } + + if (user.salesforceId) { + this.isExistentMember = true; + } + if (user.email) { + this.editForm.get('email')?.disable(); + } + } + + getMemberList(): Observable { + if (this.hasRoleAdmin()) { + return this.memberService.getAllMembers().pipe( + map(res => { + if (res.body) { + return res.body; + } + return [] + }) + ); + } else { + return this.memberService.find(this.currentAccount.salesforceId).pipe( + map(res => { + if (res) { + return [res]; + } + return [] + }) + ); + } + } + + navigateToUsersList() { + this.router.navigate(['/users']); + } + + disableSalesForceIdDD() { + if (this.hasRoleAdmin()) { + return false; + } else if (this.hasRoleOrgOwner() || this.hasRoleConsortiumLead()) { + this.editForm.patchValue({ + salesforceId: this.getSalesForceId() + }); + return true; + } + return this.isExistentMember; + } + + getSalesForceId() { + return this.accountService.getSalesforceId(); + } + + hasRoleAdmin() { + return this.accountService.hasAnyAuthority(['ROLE_ADMIN']); + } + + hasRoleOrgOwner() { + return this.accountService.hasAnyAuthority(['ROLE_ORG_OWNER']); + } + + hasRoleConsortiumLead() { + return this.accountService.hasAnyAuthority(['ROLE_CONSORTIUM_LEAD']); + } + + validateOrgOwners() { + this.isSaving = true; + const sfId = this.editForm.get('salesforceId')?.value + if (sfId) { + this.userService.hasOwner(sfId).subscribe(value => { + this.isSaving = false; + if (!this.editForm.get('mainContact')?.value) { + this.hasOwner = false; + } else { + this.hasOwner = value; + } + }); + + if (this.editForm.get('mainContact')?.value) { + this.editForm.get('salesforceId')?.disable(); + } else { + this.editForm.get('salesforceId')?.enable(); + } + + } + } + + save() { + if (this.editForm.valid) { + this.isSaving = true; + const msUser = this.createFromForm(); + + this.userService.validate(msUser).subscribe(response => { + const data = response; + if (data.valid) { + + if (msUser.id !== undefined) { + if (this.currentAccount.id === msUser.id) { + if (this.currentAccount.mainContact !== msUser.mainContact) { + this.subscribeToUpdateResponseWithOwnershipChange(this.userService.update(msUser)); + } else { + this.subscribeToUpdateResponse(this.userService.update(msUser)); + } + } else if (msUser.mainContact && !this.hasRoleAdmin()) { + this.subscribeToUpdateResponseWithOwnershipChange(this.userService.update(msUser)); + } else { + this.subscribeToUpdateResponse(this.userService.update(msUser)); + } + } else { + if (msUser.mainContact && !this.hasRoleAdmin()) { + this.subscribeToSaveResponseWithOwnershipChange(this.userService.create(msUser)); + } else { + this.subscribeToSaveResponse(this.userService.create(msUser)); + } + } + } else { + this.isSaving = false; + this.validation = data; + } + }); + } + } + + sendActivate() { + if (this.existentUser) { + this.userService.sendActivate(this.existentUser).subscribe(res => { + if (res) { + this.alertService.broadcast('gatewayApp.msUserServiceMSUser.sendActivate.success.string'); + } else { + this.alertService.broadcast('gatewayApp.msUserServiceMSUser.sendActivate.error.string'); + } + this.navigateToUsersList(); + }); + } + } + + displaySendActivate() { + if (this.existentUser && this.existentUser.email && !this.existentUser.activated) { + console.log('this.existentMSUser: ', this.existentUser); + console.log('this.existentMSUser.activated', this.existentUser.activated); + return true; + } + return false; + } + + private createFromForm(): IUser { + + return { + ...new User(), + id: this.editForm.get(['id'])?.value !== '' ? this.editForm.get(['id'])?.value : undefined, + email: this.editForm.get(['email'])?.value, + firstName: this.editForm.get(['firstName'])?.value, + lastName: this.editForm.get(['lastName'])?.value, + mainContact: this.editForm.get(['mainContact'])?.value, + isAdmin: this.editForm.get(['isAdmin']) ? this.editForm.get(['isAdmin'])?.value : false, + salesforceId: this.editForm.get(['salesforceId'])?.value, + createdBy: this.editForm.get(['createdBy'])?.value, + createdDate: + this.editForm.get(['createdDate'])?.value != null ? moment(this.editForm.get(['createdDate'])?.value, DATE_TIME_FORMAT) : undefined, + lastModifiedBy: this.editForm.get(['lastModifiedBy'])?.value, + lastModifiedDate: + this.editForm.get(['lastModifiedDate'])?.value != null + ? moment(this.editForm.get(['lastModifiedDate'])?.value, DATE_TIME_FORMAT) + : undefined + }; + } + + protected subscribeToSaveResponse(result: Observable) { + result.subscribe(() => this.onSaveSuccess(), () => this.onSaveError()); + } + + protected subscribeToUpdateResponse(result: Observable) { + result.subscribe(() => this.onUpdateSuccess(), () => this.onSaveError()); + } + + protected subscribeToSaveResponseWithOwnershipChange(result: Observable) { + result.subscribe(() => this.onSaveSuccessOwnershipChange(), () => this.onSaveError()); + } + + protected subscribeToUpdateResponseWithOwnershipChange(result: Observable) { + result.subscribe(() => this.onUpdateSuccessOwnershipChange(), () => this.onSaveError()); + } + + protected onSaveSuccess() { + this.isSaving = false; + this.navigateToUsersList(); + this.alertService.broadcast('userServiceApp.user.created.string'); + } + + protected onUpdateSuccess() { + this.isSaving = false; + this.navigateToUsersList(); + this.alertService.broadcast('userServiceApp.user.updated.string'); + } + + protected onSaveSuccessOwnershipChange() { + this.isSaving = false; + // TODO: confirm this actually works, previously it was set to SERVER_API_URL + window.location.href = '/'; + this.alertService.broadcast('userServiceApp.user.created.string'); + } + + protected onUpdateSuccessOwnershipChange() { + this.isSaving = false; + window.location.href = '/'; + this.alertService.broadcast('userServiceApp.user.updated.string'); + } + + protected onSaveError() { + this.isSaving = false; + } +} diff --git a/ui/src/app/user/user.module.ts b/ui/src/app/user/user.module.ts index f9635aece..dfef8089a 100644 --- a/ui/src/app/user/user.module.ts +++ b/ui/src/app/user/user.module.ts @@ -4,15 +4,13 @@ import { UsersComponent } from './users.component' import { RouterModule } from '@angular/router' import { routes } from './user.route' import { SharedModule } from '../shared/shared.module' -import { BrowserModule } from '@angular/platform-browser' -import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { FontAwesomeModule } from '@fortawesome/angular-fontawesome' -import { FormsModule } from '@angular/forms' -import { HasAnyAuthorityDirective } from '../shared/directive/has-any-authority.directive'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { UserUpdateComponent } from './user-update.component' import { UserDetailComponent } from './user-detail.component' @NgModule({ - declarations: [UsersComponent, UserDetailComponent], - imports: [CommonModule, SharedModule, RouterModule.forChild(routes), FontAwesomeModule, FormsModule], + declarations: [UsersComponent, UserUpdateComponent, UserDetailComponent], + imports: [CommonModule, SharedModule, RouterModule.forChild(routes), FontAwesomeModule, FormsModule, ReactiveFormsModule], }) export class UserModule {} diff --git a/ui/src/app/user/user.route.ts b/ui/src/app/user/user.route.ts index 000b800ca..c34f78c2b 100644 --- a/ui/src/app/user/user.route.ts +++ b/ui/src/app/user/user.route.ts @@ -2,23 +2,25 @@ import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot, Routes } from ' import { UsersComponent } from './users.component' import { AuthGuard } from '../account/auth.guard' import { UserDetailComponent } from './user-detail.component' -import { EMPTY, Observable, filter, take } from 'rxjs' +import { EMPTY, Observable, filter, of, take } from 'rxjs' import { User } from './model/user.model' import { UserService } from './service/user.service' import { Injectable, inject } from '@angular/core' +import { UserUpdateComponent } from './user-update.component' -export const UserResolver: ResolveFn = ( +export const UserResolver: ResolveFn = ( route: ActivatedRouteSnapshot, state: RouterStateSnapshot, userService: UserService = inject(UserService) -): Observable => { +): Observable => { if (route.paramMap.get('id')) { return userService.find(route.paramMap.get('id')!).pipe( filter((user: User) => !!user), take(1) ) } else { - return EMPTY + + return of(null) } } @@ -35,7 +37,7 @@ export const routes: Routes = [ canActivate: [AuthGuard], }, { - path: ':id/view', + path: 'users/:id/view', component: UserDetailComponent, resolve: { user: UserResolver, @@ -46,4 +48,29 @@ export const routes: Routes = [ }, canActivate: [AuthGuard], }, + { + path: 'users/new', + component: UserUpdateComponent, + resolve: { + user: UserResolver + }, + data: { + authorities: ['ROLE_ADMIN', 'ROLE_ORG_OWNER', 'ROLE_CONSORTIUM_LEAD'], + pageTitle: 'gatewayApp.msUserServiceMSUser.home.title.string' + }, + canActivate: [AuthGuard] + }, + { + path: 'users/:id/edit', + component: UserUpdateComponent, + resolve: { + user: UserResolver + }, + data: { + authorities: ['ROLE_ADMIN', 'ROLE_ORG_OWNER', 'ROLE_CONSORTIUM_LEAD'], + pageTitle: 'gatewayApp.msUserServiceMSUser.home.title.string' + }, + canActivate: [AuthGuard] + } ] + diff --git a/ui/src/app/user/users.component.html b/ui/src/app/user/users.component.html index 084207c66..a535ec3ec 100644 --- a/ui/src/app/user/users.component.html +++ b/ui/src/app/user/users.component.html @@ -5,7 +5,7 @@

Add user @@ -14,7 +14,7 @@

Import users from CSV @@ -179,7 +179,7 @@

@@ -188,7 +188,7 @@