diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/justice-users.service.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/justice-users.service.spec.ts index 18e536815..1bb946845 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/justice-users.service.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/justice-users.service.spec.ts @@ -1,5 +1,6 @@ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { of } from 'rxjs'; +import { TestBed } from '@angular/core/testing'; +import { combineLatest, of } from 'rxjs'; +import { count, delay, switchMap, take } from 'rxjs/operators'; import { AddNewJusticeUserRequest, BHClient, @@ -10,10 +11,12 @@ import { } from './clients/api-client'; import { JusticeUsersService } from './justice-users.service'; +import { Logger } from './logger'; describe('JusticeUsersService', () => { let service: JusticeUsersService; let clientApiSpy: jasmine.SpyObj; + const loggerSpy = jasmine.createSpyObj('Logger', ['error', 'debug', 'warn']); beforeEach(() => { clientApiSpy = jasmine.createSpyObj([ @@ -24,53 +27,115 @@ describe('JusticeUsersService', () => { 'restoreJusticeUser' ]); - TestBed.configureTestingModule({ providers: [{ provide: BHClient, useValue: clientApiSpy }] }); + TestBed.configureTestingModule({ + providers: [ + { provide: BHClient, useValue: clientApiSpy }, + { provide: Logger, useValue: loggerSpy } + ] + }); service = TestBed.inject(JusticeUsersService); }); - describe('retrieveJusticeUserAccounts', () => { - it('should call api when retrieving justice user accounts', (done: DoneFn) => { + describe('`users$` observable', () => { + it('should emit users returned from api', (done: DoneFn) => { const users: JusticeUserResponse[] = [ new JusticeUserResponse({ id: '123', contact_email: 'user1@test.com' }), new JusticeUserResponse({ id: '456', contact_email: 'user2@test.com' }), new JusticeUserResponse({ id: '789', contact_email: 'user3@test.com' }) ]; clientApiSpy.getUserList.and.returnValue(of(users)); - service.retrieveJusticeUserAccounts().subscribe(result => { + service.allUsers$.subscribe(result => { expect(result).toEqual(users); done(); }); }); - it('should not call api when retrieving justice user accounts and users have been already been cached', (done: DoneFn) => { - const users: JusticeUserResponse[] = [ - new JusticeUserResponse({ id: '123', contact_email: 'user1@test.com' }), - new JusticeUserResponse({ id: '456', contact_email: 'user2@test.com' }), - new JusticeUserResponse({ id: '789', contact_email: 'user3@test.com' }) - ]; - service['cache$'] = of(users); - clientApiSpy.getUserList.and.returnValue(of(users)); - service.retrieveJusticeUserAccounts().subscribe(result => { - expect(result).toEqual(users); - expect(clientApiSpy.getUserList).toHaveBeenCalledTimes(0); + it('should not make additional api requests when additional observers subscribe', (done: DoneFn) => { + clientApiSpy.getUserList.and.returnValue(of([])); + + combineLatest([service.allUsers$, service.allUsers$]) + .pipe( + delay(1000), + switchMap(x => service.allUsers$) + ) + .subscribe(() => { + expect(clientApiSpy.getUserList).toHaveBeenCalledTimes(1); + done(); + }); + }); + }); + + describe('search()', () => { + it('should trigger an emission from $users observable each time it is called', (done: DoneFn) => { + // arrange + clientApiSpy.getUserList.and.returnValue(of([])); + + // after calling search() two times, we should see 2 emissions from users$ + service.filteredUsers$.pipe(take(2), count()).subscribe(c => { + // assert + expect(c).toBe(2); done(); }); + + // act + service.search(''); + service.search(''); }); - it('should call api and return user list', (done: DoneFn) => { - const users: JusticeUserResponse[] = [new JusticeUserResponse({ id: '123', contact_email: 'user1@test.com' })]; - const term = 'user1'; - clientApiSpy.getUserList.and.returnValue(of(users)); - service.retrieveJusticeUserAccountsNoCache(term).subscribe(result => { - expect(result).toEqual(users); - expect(clientApiSpy.getUserList).toHaveBeenCalledTimes(1); + it('should apply a filter to the users collection', () => { + // arrange + const users: JusticeUserResponse[] = [ + new JusticeUserResponse({ + id: '123', + contact_email: 'user1@test.com', + first_name: 'Test', + lastname: 'Test', + username: 'Test' + }), + new JusticeUserResponse({ + id: '456', + contact_email: 'user2@test.com', + first_name: 'Another', + lastname: 'Another', + username: 'Another' + }), + new JusticeUserResponse({ + id: '789', + contact_email: 'user3@test.com', + first_name: 'Last', + lastname: 'Last', + username: 'Last' + }) + ]; + + // act + const filteredUsers = service.applyFilter('Test', users); + + // assert + expect(filteredUsers[0].first_name).toBe('Test'); + }); + }); + + describe('refresh()', () => { + it('should trigger another emission from $users observable', (done: DoneFn) => { + // arrange + clientApiSpy.getUserList.and.returnValue(of([])); + + // users$ will emit initially - after calling refresh() two more times, we should see 3 emissions from users$ + service.allUsers$.pipe(take(3), count()).subscribe(c => { + // assert + expect(c).toBe(3); done(); }); + + // act + service.refresh(); + service.refresh(); }); }); describe('addNewJusticeUser', () => { - it('should call the api to save a new user', fakeAsync(() => { + it('should call the api to save a new user & again to get the users list', (done: DoneFn) => { const username = 'john@doe.com'; const firstName = 'john'; const lastName = 'doe'; @@ -88,10 +153,8 @@ describe('JusticeUsersService', () => { }); clientApiSpy.addNewJusticeUser.and.returnValue(of(newUser)); - let result: JusticeUserResponse; + clientApiSpy.getUserList.and.returnValue(of([])); - service.addNewJusticeUser(username, firstName, lastName, telephone, role).subscribe(data => (result = data)); - tick(); const request = new AddNewJusticeUserRequest({ username: username, first_name: firstName, @@ -99,13 +162,20 @@ describe('JusticeUsersService', () => { contact_telephone: telephone, role: role }); - expect(result).toEqual(newUser); - expect(clientApiSpy.addNewJusticeUser).toHaveBeenCalledWith(request); - })); + + combineLatest([service.allUsers$, service.addNewJusticeUser(username, firstName, lastName, telephone, role)]).subscribe( + ([_, userResponse]: [JusticeUserResponse[], JusticeUserResponse]) => { + expect(clientApiSpy.getUserList).toHaveBeenCalledTimes(2); + expect(userResponse).toEqual(newUser); + expect(clientApiSpy.addNewJusticeUser).toHaveBeenCalledWith(request); + done(); + } + ); + }); }); describe('editJusticeUser', () => { - it('should call the api to edit an existing user', fakeAsync(() => { + it('should call the api to edit an existing user & again to get the users list', (done: DoneFn) => { const id = '123'; const username = 'john@doe.com'; const firstName = 'john'; @@ -123,26 +193,36 @@ describe('JusticeUsersService', () => { }); clientApiSpy.editJusticeUser.and.returnValue(of(existingUser)); - let result: JusticeUserResponse; + clientApiSpy.getUserList.and.returnValue(of([])); - service.editJusticeUser(id, username, role).subscribe(data => (result = data)); - tick(); const request = new EditJusticeUserRequest({ id, username, role }); - expect(result).toEqual(existingUser); - expect(clientApiSpy.editJusticeUser).toHaveBeenCalledWith(request); - })); + + combineLatest([service.allUsers$, service.editJusticeUser(id, username, role)]).subscribe( + ([_, result]: [JusticeUserResponse[], JusticeUserResponse]) => { + expect(clientApiSpy.getUserList).toHaveBeenCalledTimes(2); + expect(result).toEqual(existingUser); + expect(clientApiSpy.editJusticeUser).toHaveBeenCalledWith(request); + done(); + } + ); + }); }); describe('deleteJusticeUser', () => { - it('should call the api to delete the user', () => { + it('should call the api to delete the user & again to get the users list', (done: DoneFn) => { clientApiSpy.deleteJusticeUser.and.returnValue(of('')); + clientApiSpy.getUserList.and.returnValue(of([])); + const id = '123'; - service.deleteJusticeUser(id).subscribe(); - expect(clientApiSpy.deleteJusticeUser).toHaveBeenCalledWith(id); + combineLatest([service.allUsers$, service.deleteJusticeUser(id)]).subscribe(() => { + expect(clientApiSpy.getUserList).toHaveBeenCalledTimes(2); + expect(clientApiSpy.deleteJusticeUser).toHaveBeenCalledWith(id); + done(); + }); }); }); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/justice-users.service.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/justice-users.service.ts index a3a541a10..20abfaaed 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/services/justice-users.service.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/services/justice-users.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { shareReplay } from 'rxjs/operators'; +import { BehaviorSubject, throwError } from 'rxjs'; +import { catchError, filter, map, mergeMap, shareReplay, skip, switchMap, tap } from 'rxjs/operators'; import { AddNewJusticeUserRequest, BHClient, @@ -10,28 +10,50 @@ import { RestoreJusticeUserRequest } from './clients/api-client'; import { cleanQuery } from '../common/helpers/api-helper'; +import { Logger } from './logger'; @Injectable({ providedIn: 'root' }) export class JusticeUsersService { - private cache$: Observable; + loggerPrefix = '[JusticeUsersService] -'; + private refresh$: BehaviorSubject = new BehaviorSubject(null); + private searchTerm$: BehaviorSubject = new BehaviorSubject(null); - constructor(private apiClient: BHClient) {} - retrieveJusticeUserAccounts() { - if (!this.cache$) { - this.cache$ = this.requestJusticeUsers(null).pipe(shareReplay(1)); - } + allUsers$ = this.refresh$.pipe( + mergeMap(() => this.getJusticeUsers(null)), + shareReplay(1) + ); + + filteredUsers$ = this.allUsers$.pipe( + switchMap(users => + this.searchTerm$.pipe( + filter(searchTerm => searchTerm !== null), + map(term => this.applyFilter(term, users)) + ) + ) + ); - return this.cache$; + constructor(private apiClient: BHClient, private logger: Logger) {} + + refresh() { + this.refresh$.next(); } - retrieveJusticeUserAccountsNoCache(term: string) { - return this.requestJusticeUsers(term).pipe(shareReplay(1)); + search(searchTerm: string) { + this.searchTerm$.next(searchTerm); } - private requestJusticeUsers(term: string) { - return this.apiClient.getUserList(cleanQuery(term)); + applyFilter(searchTerm: string, users: JusticeUserResponse[]): JusticeUserResponse[] { + if (!searchTerm) { + return users; + } + + return users.filter(user => + [user.first_name, user.lastname, user.contact_email, user.username].some(field => + field.toLowerCase().includes(searchTerm.toLowerCase()) + ) + ); } addNewJusticeUser(username: string, firstName: string, lastName: string, telephone: string, role: JusticeUserRole) { @@ -42,7 +64,7 @@ export class JusticeUsersService { contact_telephone: telephone, role: role }); - return this.apiClient.addNewJusticeUser(request); + return this.apiClient.addNewJusticeUser(request).pipe(tap(() => this.refresh$.next())); } editJusticeUser(id: string, username: string, role: JusticeUserRole) { @@ -51,11 +73,11 @@ export class JusticeUsersService { username, role }); - return this.apiClient.editJusticeUser(request); + return this.apiClient.editJusticeUser(request).pipe(tap(() => this.refresh$.next())); } deleteJusticeUser(id: string) { - return this.apiClient.deleteJusticeUser(id); + return this.apiClient.deleteJusticeUser(id).pipe(tap(() => this.refresh$.next())); } restoreJusticeUser(id: string, username: string) { @@ -63,6 +85,15 @@ export class JusticeUsersService { username, id }); - return this.apiClient.restoreJusticeUser(request); + return this.apiClient.restoreJusticeUser(request).pipe(tap(() => this.refresh$.next())); + } + + private getJusticeUsers(term: string) { + return this.apiClient.getUserList(cleanQuery(term)).pipe( + catchError(error => { + this.logger.error(`${this.loggerPrefix} There was an unexpected error getting justice users`, new Error(error)); + return throwError(error); + }) + ); } } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.html index 009b4d2b2..cd21970a6 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.html +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.html @@ -4,7 +4,7 @@ id="user-list" class="custom" labelForId="users" - [items]="users" + [items]="users$ | async" bindLabel="full_name" bindValue="id" formControlName="selectedUserIds" diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.spec.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.spec.ts index 3d91db040..273d1acfe 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.spec.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.spec.ts @@ -4,8 +4,6 @@ import { HttpClient, HttpHandler } from '@angular/common/http'; import { FormBuilder } from '@angular/forms'; import { MockLogger } from '../../testing/mock-logger'; import { Logger } from '../../../services/logger'; -import { of, throwError } from 'rxjs'; -import { JusticeUserResponse } from '../../../services/clients/api-client'; import { JusticeUsersService } from 'src/app/services/justice-users.service'; describe('JusticeUsersMenuComponent', () => { @@ -15,7 +13,6 @@ describe('JusticeUsersMenuComponent', () => { beforeEach(async () => { justiceUsersServiceSpy = jasmine.createSpyObj('JusticeUsersService', ['retrieveJusticeUserAccounts']); - justiceUsersServiceSpy.retrieveJusticeUserAccounts.and.returnValue(of([new JusticeUserResponse()])); await TestBed.configureTestingModule({ declarations: [JusticeUsersMenuComponent], @@ -54,22 +51,4 @@ describe('JusticeUsersMenuComponent', () => { expect(component.form.controls[component.formGroupName].enabled).toEqual(false); }); }); - - describe('loadItems', () => { - it('should call video hearing service', () => { - const expectedResponse = [new JusticeUserResponse()]; - component.loadItems(); - expect(justiceUsersServiceSpy.retrieveJusticeUserAccounts).toHaveBeenCalled(); - expect(component.users).toEqual(expectedResponse); - }); - - it('should call video hearing service, and catch thrown exception', () => { - justiceUsersServiceSpy.retrieveJusticeUserAccounts.and.returnValue(throwError({ status: 404 })); - - const handleListErrorSpy = spyOn(component, 'handleListError'); - component.loadItems(); - expect(justiceUsersServiceSpy.retrieveJusticeUserAccounts).toHaveBeenCalled(); - expect(handleListErrorSpy).toHaveBeenCalled(); - }); - }); }); diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.ts b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.ts index 920c96c1a..f0683e8ae 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.ts +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/shared/menus/justice-users-menu/justice-users-menu.component.ts @@ -1,20 +1,21 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { JusticeUserResponse } from '../../../services/clients/api-client'; import { FormBuilder } from '@angular/forms'; import { BookingPersistService } from '../../../services/bookings-persist.service'; import { JusticeUsersService } from '../../../services/justice-users.service'; import { Logger } from '../../../services/logger'; import { MenuBase } from '../menu-base'; +import { Observable } from 'rxjs'; @Component({ selector: 'app-justice-users-menu', templateUrl: './justice-users-menu.component.html', styleUrls: ['./justice-users-menu.component.scss'] }) -export class JusticeUsersMenuComponent extends MenuBase { +export class JusticeUsersMenuComponent extends MenuBase implements OnInit { loggerPrefix = '[MenuJusticeUser] -'; formGroupName = 'selectedUserIds'; - users: JusticeUserResponse[]; + users$: Observable; selectedItems: [] | string; formConfiguration = { selectedUserIds: [this.bookingPersistService.selectedUsers || []] @@ -32,14 +33,10 @@ export class JusticeUsersMenuComponent extends MenuBase { super(formBuilder, logger); } - loadItems(): void { - this.justiceUserService.retrieveJusticeUserAccounts().subscribe( - (data: JusticeUserResponse[]) => { - this.users = data; - this.items = data; - this.logger.debug(`${this.loggerPrefix} Updating list of users.`, { users: data.length }); - }, - error => this.handleListError(error, 'users') - ); + ngOnInit(): void { + this.users$ = this.justiceUserService.allUsers$; + super.ngOnInit(); } + + loadItems(): void {} } diff --git a/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/manage-team/manage-team.component.html b/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/manage-team/manage-team.component.html index 40af07696..3f3e2ce36 100644 --- a/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/manage-team/manage-team.component.html +++ b/AdminWebsite/AdminWebsite/ClientApp/src/app/work-allocation/manage-team/manage-team.component.html @@ -11,10 +11,15 @@
- -
+
@@ -24,26 +29,27 @@
-
+ +
- {{ message }} + {{ message$ | async }}
- - - - - - - - - - - - - - + +
UsernameFirst nameLast nameContact telephoneRole
+ + + + + + + + + + + + - - -
UsernameFirst nameLast nameContact telephoneRole
Deleted{{ user.username }} {{ user.first_name }} @@ -90,21 +96,23 @@ >
-