diff --git a/comixed-frontend/src/app/backend-status/actions/load-task-audit-log.actions.ts b/comixed-frontend/src/app/backend-status/actions/load-task-audit-log.actions.ts new file mode 100644 index 000000000..b2e739eb4 --- /dev/null +++ b/comixed-frontend/src/app/backend-status/actions/load-task-audit-log.actions.ts @@ -0,0 +1,43 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { createAction, props } from '@ngrx/store'; +import { TaskAuditLogEntry } from 'app/backend-status/models/task-audit-log-entry'; + +/** + * Loads all task audit log entries after the specified date. + */ +export const loadTaskAuditLogEntries = createAction( + '[LoadTaskAuditLog] Load task audit log entries', + props<{ since: number }>() +); + +/** + * Receives additional task audit log entries. + */ +export const taskAuditLogEntriesLoaded = createAction( + '[LoadTaskAuditLog] Task audit log entries loaded', + props<{ entries: TaskAuditLogEntry[]; latest: number }>() +); + +/** + * Failed to get task audit log entries. + */ +export const loadTaskAuditLogFailed = createAction( + '[LoadTaskAuditLog] Failed to load the task audit log entries' +); diff --git a/comixed-frontend/src/app/backend-status/actions/task-audit-log.actions.ts b/comixed-frontend/src/app/backend-status/actions/task-audit-log.actions.ts deleted file mode 100644 index 20361a367..000000000 --- a/comixed-frontend/src/app/backend-status/actions/task-audit-log.actions.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * ComiXed - A digital comic book library management application. - * Copyright (C) 2020, The ComiXed Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - */ - -import { Action } from '@ngrx/store'; -import { TaskAuditLogEntry } from 'app/backend-status/models/task-audit-log-entry'; - -export enum TaskAuditLogActionTypes { - GetEntries = '[TASK AUDIT LOG] Get task audit log entries', - ReceivedEntries = '[TASK AUDIT LOG] Task audit log entries received', - GetEntriesFailed = '[TASK AUDIT LOG] Failed to get task audit log entries' -} - -export class GetTaskAuditLogEntries implements Action { - readonly type = TaskAuditLogActionTypes.GetEntries; - - constructor( - public payload: { - cutoff: number; - } - ) {} -} - -export class ReceivedTaskAuditLogEntries implements Action { - readonly type = TaskAuditLogActionTypes.ReceivedEntries; - - constructor(public payload: { entries: TaskAuditLogEntry[] }) {} -} - -export class GetTaskAuditLogEntriesFailed implements Action { - readonly type = TaskAuditLogActionTypes.GetEntriesFailed; - - constructor() {} -} - -export type TaskAuditLogActions = - | GetTaskAuditLogEntries - | ReceivedTaskAuditLogEntries - | GetTaskAuditLogEntriesFailed; diff --git a/comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.spec.ts b/comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.spec.ts deleted file mode 100644 index 5a4b2e595..000000000 --- a/comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.spec.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * ComiXed - A digital comic book library management application. - * Copyright (C) 2020, The ComiXed Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - */ - -import { TaskAuditLogAdaptor } from './task-audit-log.adaptor'; -import { - TASK_AUDIT_LOG_ENTRY_1, - TASK_AUDIT_LOG_ENTRY_2, - TASK_AUDIT_LOG_ENTRY_3, - TASK_AUDIT_LOG_ENTRY_4, - TASK_AUDIT_LOG_ENTRY_5 -} from 'app/backend-status/models/task-audit-log-entry.fixtures'; -import { TestBed } from '@angular/core/testing'; -import { Store, StoreModule } from '@ngrx/store'; -import { AppState } from 'app/backend-status'; -import { - GetTaskAuditLogEntries, - GetTaskAuditLogEntriesFailed, - ReceivedTaskAuditLogEntries -} from 'app/backend-status/actions/task-audit-log.actions'; -import { LoggerModule } from '@angular-ru/logger'; -import { - reducer, - TASK_AUDIT_LOG_FEATURE_KEY -} from 'app/backend-status/reducers/task-audit-log.reducer'; -import { EffectsModule } from '@ngrx/effects'; -import { TaskAuditLogEffects } from 'app/backend-status/effects/task-audit-log.effects'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { MessageService } from 'primeng/api'; -import { TranslateModule } from '@ngx-translate/core'; - -describe('TaskAuditLogAdaptor', () => { - const LAST_ENTRY_DATE = new Date().getTime(); - const LOG_ENTRIES = [ - TASK_AUDIT_LOG_ENTRY_1, - TASK_AUDIT_LOG_ENTRY_2, - TASK_AUDIT_LOG_ENTRY_3, - TASK_AUDIT_LOG_ENTRY_4, - TASK_AUDIT_LOG_ENTRY_5 - ]; - - let taskAuditLogAdaptor: TaskAuditLogAdaptor; - let store: Store; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ - HttpClientTestingModule, - LoggerModule.forRoot(), - TranslateModule.forRoot(), - StoreModule.forRoot({}), - StoreModule.forFeature(TASK_AUDIT_LOG_FEATURE_KEY, reducer), - EffectsModule.forRoot([]), - EffectsModule.forFeature([TaskAuditLogEffects]) - ], - providers: [TaskAuditLogAdaptor, MessageService] - }); - - taskAuditLogAdaptor = TestBed.get(TaskAuditLogAdaptor); - store = TestBed.get(Store); - spyOn(store, 'dispatch').and.callThrough(); - }); - - it('should create an instance', () => { - expect(taskAuditLogAdaptor).toBeTruthy(); - }); - - describe('getting the list of task audit log entries', () => { - let lastEntryDate: number; - - beforeEach(() => { - lastEntryDate = taskAuditLogAdaptor.lastEntryDate; - taskAuditLogAdaptor.getEntries(); - }); - - it('fires an action', () => { - expect(store.dispatch).toHaveBeenCalledWith( - new GetTaskAuditLogEntries({ cutoff: lastEntryDate }) - ); - }); - - it('provides updates on fetching', () => { - taskAuditLogAdaptor.fetchingEntries$.subscribe(response => - expect(response).toBeTruthy() - ); - }); - - describe('success', () => { - beforeEach(() => { - store.dispatch( - new ReceivedTaskAuditLogEntries({ entries: LOG_ENTRIES }) - ); - }); - - it('provides updates on fetching', () => { - taskAuditLogAdaptor.fetchingEntries$.subscribe(response => - expect(response).toBeFalsy() - ); - }); - - it('provides updates on the log entries', () => { - taskAuditLogAdaptor.entries$.subscribe(response => - expect(response).toEqual(LOG_ENTRIES) - ); - }); - }); - - describe('failure', () => { - beforeEach(() => { - store.dispatch(new GetTaskAuditLogEntriesFailed()); - }); - - it('provides updates on fetching', () => { - taskAuditLogAdaptor.fetchingEntries$.subscribe(response => - expect(response).toBeFalsy() - ); - }); - }); - }); -}); diff --git a/comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.ts b/comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.ts deleted file mode 100644 index 8b427d93c..000000000 --- a/comixed-frontend/src/app/backend-status/adaptors/task-audit-log.adaptor.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * ComiXed - A digital comic book library management application. - * Copyright (C) 2020, The ComiXed Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - */ - -import {Injectable} from '@angular/core'; -import {LoggerService} from '@angular-ru/logger'; -import {Store} from '@ngrx/store'; -import {AppState} from 'app/backend-status'; -import {BehaviorSubject, Observable} from 'rxjs'; -import {TaskAuditLogEntry} from 'app/backend-status/models/task-audit-log-entry'; -import {GetTaskAuditLogEntries} from 'app/backend-status/actions/task-audit-log.actions'; -import {TASK_AUDIT_LOG_FEATURE_KEY, TaskAuditLogState} from 'app/backend-status/reducers/task-audit-log.reducer'; -import {filter} from 'rxjs/operators'; -import * as _ from 'lodash'; - -@Injectable({ - providedIn: 'root' -}) -export class TaskAuditLogAdaptor { - private _lastEntryDate$ = new BehaviorSubject(0); - private _fetchingEntries$ = new BehaviorSubject(false); - private _entries$ = new BehaviorSubject([]); - - constructor(private logger: LoggerService, private store: Store) { - this.store - .select(TASK_AUDIT_LOG_FEATURE_KEY) - .pipe(filter(state => !!state)) - .subscribe((state: TaskAuditLogState) => { - this.logger.debug('task audit log state changed:', state); - if (!_.isEqual(this._lastEntryDate$.getValue(), state.lastEntryDate)) { - this._lastEntryDate$.next(state.lastEntryDate); - } - if (this._fetchingEntries$.getValue() !== state.fetchingEntries) { - this._fetchingEntries$.next(state.fetchingEntries); - } - if (!_.isEqual(this._entries$.getValue(), state.entries)) { - this._entries$.next(state.entries); - } - }); - } - - getEntries(): void { - this.logger.debug( - 'getting task audit log entries:', - this._lastEntryDate$.getValue() - ); - this.store.dispatch( - new GetTaskAuditLogEntries({cutoff: this._lastEntryDate$.getValue()}) - ); - } - - get lastEntryDate(): number { - return this._lastEntryDate$.getValue(); - } - - get fetchingEntries$(): Observable { - return this._fetchingEntries$.asObservable(); - } - - get entries$(): Observable { - return this._entries$.asObservable(); - } -} diff --git a/comixed-frontend/src/app/backend-status/backend-status.module.ts b/comixed-frontend/src/app/backend-status/backend-status.module.ts index d90161c7b..cc64d44a2 100644 --- a/comixed-frontend/src/app/backend-status/backend-status.module.ts +++ b/comixed-frontend/src/app/backend-status/backend-status.module.ts @@ -24,20 +24,20 @@ import { BackendStatusRoutingModule } from 'app/backend-status/backend-status-ro import { BuildDetailsAdaptor } from 'app/backend-status/adaptors/build-details.adaptor'; import { StoreModule } from '@ngrx/store'; import * as fromBuildDetails from './reducers/build-details.reducer'; -import * as fromTaskAuditLog from './reducers/task-audit-log.reducer'; import * as fromClearTaskAuditLog from './reducers/clear-task-audit-log.reducer'; +import * as fromLoadTaskAuditLog from './reducers/load-task-audit-log.reducer'; import { EffectsModule } from '@ngrx/effects'; import { BuildDetailsEffects } from 'app/backend-status/effects/build-details.effects'; import { TranslateModule } from '@ngx-translate/core'; -import { TaskAuditLogEffects } from 'app/backend-status/effects/task-audit-log.effects'; import { TaskAuditLogPageComponent } from './pages/task-audit-log-page/task-audit-log-page.component'; -import { TaskAuditLogAdaptor } from 'app/backend-status/adaptors/task-audit-log.adaptor'; import { TableModule } from 'primeng/table'; import { ScrollPanelModule } from 'primeng/primeng'; import { CoreModule } from 'app/core/core.module'; import { CLEAR_TASK_AUDIT_LOG_FEATURE_KEY } from 'app/backend-status/reducers/clear-task-audit-log.reducer'; import { ClearTaskAuditLogEffects } from 'app/backend-status/effects/clear-task-audit-log.effects'; import { ToolbarModule, TooltipModule } from 'primeng/primeng'; +import { LOAD_TASK_AUDIT_LOG_FEATURE_KEY } from 'app/backend-status/reducers/load-task-audit-log.reducer'; +import { LoadTaskAuditLogEffects } from 'app/backend-status/effects/load-task-audit-log.effects'; @NgModule({ declarations: [BuildDetailsPageComponent, TaskAuditLogPageComponent], @@ -51,8 +51,8 @@ import { ToolbarModule, TooltipModule } from 'primeng/primeng'; fromBuildDetails.reducer ), StoreModule.forFeature( - fromTaskAuditLog.TASK_AUDIT_LOG_FEATURE_KEY, - fromTaskAuditLog.reducer + LOAD_TASK_AUDIT_LOG_FEATURE_KEY, + fromLoadTaskAuditLog.reducer ), StoreModule.forFeature( CLEAR_TASK_AUDIT_LOG_FEATURE_KEY, @@ -60,7 +60,7 @@ import { ToolbarModule, TooltipModule } from 'primeng/primeng'; ), EffectsModule.forFeature([ BuildDetailsEffects, - TaskAuditLogEffects, + LoadTaskAuditLogEffects, ClearTaskAuditLogEffects ]), TableModule, @@ -69,6 +69,6 @@ import { ToolbarModule, TooltipModule } from 'primeng/primeng'; TooltipModule ], exports: [CommonModule, CoreModule], - providers: [BuildDetailsService, BuildDetailsAdaptor, TaskAuditLogAdaptor] + providers: [BuildDetailsService, BuildDetailsAdaptor] }) export class BackendStatusModule {} diff --git a/comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.spec.ts b/comixed-frontend/src/app/backend-status/effects/load-task-audit-log.effects.spec.ts similarity index 51% rename from comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.spec.ts rename to comixed-frontend/src/app/backend-status/effects/load-task-audit-log.effects.spec.ts index 44ea8e8e2..423c8fc5c 100644 --- a/comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.spec.ts +++ b/comixed-frontend/src/app/backend-status/effects/load-task-audit-log.effects.spec.ts @@ -18,9 +18,9 @@ import { TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; -import { Observable, of, throwError } from 'rxjs'; +import { Observable, of } from 'rxjs'; -import { TaskAuditLogEffects } from './task-audit-log.effects'; +import { LoadTaskAuditLogEffects } from './load-task-audit-log.effects'; import { TaskAuditLogService } from 'app/backend-status/services/task-audit-log.service'; import { TASK_AUDIT_LOG_ENTRY_1, @@ -30,18 +30,22 @@ import { TASK_AUDIT_LOG_ENTRY_5 } from 'app/backend-status/backend-status.fixtures'; import { - GetTaskAuditLogEntries, - GetTaskAuditLogEntriesFailed, - ReceivedTaskAuditLogEntries -} from 'app/backend-status/actions/task-audit-log.actions'; + loadTaskAuditLogEntries, + loadTaskAuditLogFailed, + taskAuditLogEntriesLoaded +} from 'app/backend-status/actions/load-task-audit-log.actions'; import { hot } from 'jasmine-marbles'; -import { HttpErrorResponse } from '@angular/common/http'; -import { MessageService } from 'primeng/api'; import { LoggerModule } from '@angular-ru/logger'; +import { AlertService, ApiResponse } from 'app/core'; +import { TaskAuditLogEntry } from 'app/backend-status/models/task-audit-log-entry'; import { TranslateModule } from '@ngx-translate/core'; -import objectContaining = jasmine.objectContaining; +import { CoreModule } from 'app/core/core.module'; +import { HttpErrorResponse } from '@angular/common/http'; +import { LoadTaskAuditLogResponse } from 'app/backend-status/models/net/load-task-audit-log-response'; +import { MessageService } from 'primeng/api'; -describe('TaskAuditLogEffects', () => { +describe('LoadTaskAuditLogEffects', () => { + const LATEST = new Date().getTime(); const LOG_ENTRIES = [ TASK_AUDIT_LOG_ENTRY_1, TASK_AUDIT_LOG_ENTRY_2, @@ -51,15 +55,15 @@ describe('TaskAuditLogEffects', () => { ]; let actions$: Observable; - let effects: TaskAuditLogEffects; + let effects: LoadTaskAuditLogEffects; let taskAuditLogService: jasmine.SpyObj; - let messageService: MessageService; + let alertService: AlertService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [LoggerModule.forRoot(), TranslateModule.forRoot()], + imports: [CoreModule, LoggerModule.forRoot(), TranslateModule.forRoot()], providers: [ - TaskAuditLogEffects, + LoadTaskAuditLogEffects, provideMockActions(() => actions$), { provide: TaskAuditLogService, @@ -73,64 +77,76 @@ describe('TaskAuditLogEffects', () => { ] }); - effects = TestBed.get(TaskAuditLogEffects); + effects = TestBed.get(LoadTaskAuditLogEffects); taskAuditLogService = TestBed.get(TaskAuditLogService); - messageService = TestBed.get(MessageService); - spyOn(messageService, 'add'); + alertService = TestBed.get(AlertService); + spyOn(alertService, 'error'); }); it('should be created', () => { expect(effects).toBeTruthy(); }); - describe('getting the list of log entries', () => { + describe('loading the task audit log entries', () => { it('fires an action on success', () => { - const serviceResponse = LOG_ENTRIES; - const action = new GetTaskAuditLogEntries({ - cutoff: new Date().getTime() + const serviceResponse = { + success: true, + result: { + entries: LOG_ENTRIES, + latest: LATEST + } as LoadTaskAuditLogResponse + } as ApiResponse; + const action = loadTaskAuditLogEntries({ since: 0 }); + const outcome = taskAuditLogEntriesLoaded({ + entries: LOG_ENTRIES, + latest: LATEST }); - const outcome = new ReceivedTaskAuditLogEntries({ entries: LOG_ENTRIES }); actions$ = hot('-a', { a: action }); taskAuditLogService.getLogEntries.and.returnValue(of(serviceResponse)); const expected = hot('-b', { b: outcome }); - expect(effects.getLogEntries$).toBeObservable(expected); + expect(effects.loadTaskAuditLogEntries$).toBeObservable(expected); + }); + + it('fires an action on failure', () => { + const serviceResponse = { + success: false + } as ApiResponse; + const action = loadTaskAuditLogEntries({ since: 0 }); + const outcome = loadTaskAuditLogFailed(); + + actions$ = hot('-a', { a: action }); + taskAuditLogService.getLogEntries.and.returnValue(of(serviceResponse)); + + const expected = hot('-b', { b: outcome }); + expect(effects.loadTaskAuditLogEntries$).toBeObservable(expected); + expect(alertService.error).toHaveBeenCalledWith(jasmine.any(String)); }); it('fires an action on service failure', () => { const serviceResponse = new HttpErrorResponse({}); - const action = new GetTaskAuditLogEntries({ - cutoff: new Date().getTime() - }); - const outcome = new GetTaskAuditLogEntriesFailed(); + const action = loadTaskAuditLogEntries({ since: 0 }); + const outcome = loadTaskAuditLogFailed(); actions$ = hot('-a', { a: action }); - taskAuditLogService.getLogEntries.and.returnValue( - throwError(serviceResponse) - ); + taskAuditLogService.getLogEntries.and.returnValue(of(serviceResponse)); const expected = hot('-b', { b: outcome }); - expect(effects.getLogEntries$).toBeObservable(expected); - expect(messageService.add).toHaveBeenCalledWith( - objectContaining({ severity: 'error' }) - ); + expect(effects.loadTaskAuditLogEntries$).toBeObservable(expected); + expect(alertService.error).toHaveBeenCalledWith(jasmine.any(String)); }); it('fires an action on general failure', () => { - const action = new GetTaskAuditLogEntries({ - cutoff: new Date().getTime() - }); - const outcome = new GetTaskAuditLogEntriesFailed(); + const action = loadTaskAuditLogEntries({ since: 0 }); + const outcome = loadTaskAuditLogFailed(); actions$ = hot('-a', { a: action }); taskAuditLogService.getLogEntries.and.throwError('expected'); const expected = hot('-(b|)', { b: outcome }); - expect(effects.getLogEntries$).toBeObservable(expected); - expect(messageService.add).toHaveBeenCalledWith( - objectContaining({ severity: 'error' }) - ); + expect(effects.loadTaskAuditLogEntries$).toBeObservable(expected); + expect(alertService.error).toHaveBeenCalledWith(jasmine.any(String)); }); }); }); diff --git a/comixed-frontend/src/app/backend-status/effects/load-task-audit-log.effects.ts b/comixed-frontend/src/app/backend-status/effects/load-task-audit-log.effects.ts new file mode 100644 index 000000000..31824f8c4 --- /dev/null +++ b/comixed-frontend/src/app/backend-status/effects/load-task-audit-log.effects.ts @@ -0,0 +1,98 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { LoggerService } from '@angular-ru/logger'; +import { + loadTaskAuditLogEntries, + loadTaskAuditLogFailed, + taskAuditLogEntriesLoaded +} from 'app/backend-status/actions/load-task-audit-log.actions'; +import { catchError, map, switchMap, tap } from 'rxjs/operators'; +import { TaskAuditLogService } from 'app/backend-status/services/task-audit-log.service'; +import { AlertService, ApiResponse } from 'app/core'; +import { TranslateService } from '@ngx-translate/core'; +import { of } from 'rxjs'; +import { LoadTaskAuditLogResponse } from 'app/backend-status/models/net/load-task-audit-log-response'; + +@Injectable() +export class LoadTaskAuditLogEffects { + constructor( + private logger: LoggerService, + private actions$: Actions, + private taskAuditLogService: TaskAuditLogService, + private alertService: AlertService, + private translatesService: TranslateService + ) {} + + loadTaskAuditLogEntries$ = createEffect(() => { + return this.actions$.pipe( + ofType(loadTaskAuditLogEntries), + tap(action => + this.logger.debug('effect: load task audit log entries:', action) + ), + switchMap(action => + this.taskAuditLogService.getLogEntries(action.since).pipe( + tap(response => this.logger.debug('received response:', response)), + tap( + (response: ApiResponse) => + !response.success && + this.alertService.error( + this.translatesService.instant( + 'task.list.effects.load.error.detail' + ) + ) + ), + map((response: ApiResponse) => + response.success + ? taskAuditLogEntriesLoaded({ + entries: response.result.entries, + latest: response.result.latest + }) + : loadTaskAuditLogFailed() + ), + catchError(error => { + this.logger.error( + 'service failure getting task audit log entries:', + error + ); + this.alertService.error( + this.translatesService.instant( + 'task.list.effects.load.error.detail' + ) + ); + return of(loadTaskAuditLogFailed()); + }) + ) + ), + catchError(error => { + this.logger.error( + 'service failure getting task audit log entries:', + error + ); + this.alertService.error( + this.translatesService.instant( + 'general-message.error.general-service-failure' + ) + ); + return of(loadTaskAuditLogFailed()); + }) + ); + }); +} diff --git a/comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.ts b/comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.ts deleted file mode 100644 index 4032d107e..000000000 --- a/comixed-frontend/src/app/backend-status/effects/task-audit-log.effects.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * ComiXed - A digital comic book library management application. - * Copyright (C) 2020, The ComiXed Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - */ - -import { Injectable } from '@angular/core'; -import { Actions, Effect, ofType } from '@ngrx/effects'; -import { - GetTaskAuditLogEntriesFailed, - ReceivedTaskAuditLogEntries, - TaskAuditLogActions, - TaskAuditLogActionTypes -} from '../actions/task-audit-log.actions'; -import { Action } from '@ngrx/store'; -import { Observable, of } from 'rxjs'; -import { catchError, map, switchMap, tap } from 'rxjs/operators'; -import { LoggerService } from '@angular-ru/logger'; -import { TaskAuditLogService } from 'app/backend-status/services/task-audit-log.service'; -import { TaskAuditLogEntry } from 'app/backend-status/models/task-audit-log-entry'; -import { MessageService } from 'primeng/api'; -import { TranslateService } from '@ngx-translate/core'; - -@Injectable() -export class TaskAuditLogEffects { - constructor( - private actions$: Actions, - private logger: LoggerService, - private taskAuditLogService: TaskAuditLogService, - private messageService: MessageService, - private translateService: TranslateService - ) {} - - @Effect() - getLogEntries$: Observable = this.actions$.pipe( - ofType(TaskAuditLogActionTypes.GetEntries), - map(action => action.payload), - tap(action => - this.logger.debug('effect: getting task audit log entries:', action) - ), - switchMap(action => - this.taskAuditLogService.getLogEntries(action.cutoff).pipe( - tap(response => this.logger.debug('received response:', response)), - map( - (response: TaskAuditLogEntry[]) => - new ReceivedTaskAuditLogEntries({ entries: response }) - ), - catchError(error => { - this.logger.error( - 'service failure getting task audit log entries:', - error - ); - this.messageService.add({ - severity: 'error', - detail: this.translateService.instant( - 'task-audit-log-effects.get-log-entries.error.detail' - ) - }); - return of(new GetTaskAuditLogEntriesFailed()); - }) - ) - ), - catchError(error => { - this.logger.error( - 'general failure getting task audit log entries:', - error - ); - this.messageService.add({ - severity: 'error', - detail: this.translateService.instant( - 'general-message.error.general-service-failure' - ) - }); - return of(new GetTaskAuditLogEntriesFailed()); - }) - ); -} diff --git a/comixed-frontend/src/app/backend-status/index.ts b/comixed-frontend/src/app/backend-status/index.ts index 054882703..17805354c 100644 --- a/comixed-frontend/src/app/backend-status/index.ts +++ b/comixed-frontend/src/app/backend-status/index.ts @@ -19,13 +19,13 @@ import * as fromRouter from '@ngrx/router-store'; import * as fromBuildDetails from './reducers/build-details.reducer'; import { BuildDetailsState } from './reducers/build-details.reducer'; -import * as fromTaskAuditLog from './reducers/task-audit-log.reducer'; import * as fromClearTaskAuditLog from './reducers/clear-task-audit-log.reducer'; +import * as fromLoadTaskAuditLog from './reducers/load-task-audit-log.reducer'; +import { LoadTaskAuditLogState } from './reducers/load-task-audit-log.reducer'; import { Params } from '@angular/router'; import { ActionReducerMap, MetaReducer } from '@ngrx/store'; import { environment } from '../../environments/environment'; -import { TaskAuditLogState } from 'app/backend-status/reducers/task-audit-log.reducer'; import { ClearTaskAuditLogState } from 'app/backend-status/reducers/clear-task-audit-log.reducer'; interface RouterStateUrl { @@ -37,7 +37,7 @@ interface RouterStateUrl { export interface AppState { router: fromRouter.RouterReducerState; build_details: BuildDetailsState; - task_audit_log_state: TaskAuditLogState; + load_task_audit_log_state: LoadTaskAuditLogState; clear_task_audit_log_state: ClearTaskAuditLogState; } @@ -46,7 +46,7 @@ export type State = AppState; export const reducers: ActionReducerMap = { router: fromRouter.routerReducer, build_details: fromBuildDetails.reducer, - task_audit_log_state: fromTaskAuditLog.reducer, + load_task_audit_log_state: fromLoadTaskAuditLog.reducer, clear_task_audit_log_state: fromClearTaskAuditLog.reducer }; diff --git a/comixed-frontend/src/app/backend-status/models/net/load-task-audit-log-response.ts b/comixed-frontend/src/app/backend-status/models/net/load-task-audit-log-response.ts new file mode 100644 index 000000000..aab49c7e0 --- /dev/null +++ b/comixed-frontend/src/app/backend-status/models/net/load-task-audit-log-response.ts @@ -0,0 +1,24 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { TaskAuditLogEntry } from 'app/backend-status/models/task-audit-log-entry'; + +export interface LoadTaskAuditLogResponse { + entries: TaskAuditLogEntry[]; + latest: number; +} diff --git a/comixed-frontend/src/app/backend-status/pages/task-audit-log-page/task-audit-log-page.component.spec.ts b/comixed-frontend/src/app/backend-status/pages/task-audit-log-page/task-audit-log-page.component.spec.ts index 63fb20742..36837f198 100644 --- a/comixed-frontend/src/app/backend-status/pages/task-audit-log-page/task-audit-log-page.component.spec.ts +++ b/comixed-frontend/src/app/backend-status/pages/task-audit-log-page/task-audit-log-page.component.spec.ts @@ -20,13 +20,12 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TaskAuditLogPageComponent } from './task-audit-log-page.component'; import { TableModule } from 'primeng/table'; -import { TranslateModule } from '@ngx-translate/core'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { LoggerModule } from '@angular-ru/logger'; import { Store, StoreModule } from '@ngrx/store'; -import * as fromTaskAuditLog from 'app/backend-status/reducers/task-audit-log.reducer'; -import { TASK_AUDIT_LOG_FEATURE_KEY } from 'app/backend-status/reducers/task-audit-log.reducer'; +import * as fromLoadTaskAuditLog from 'app/backend-status/reducers/load-task-audit-log.reducer'; +import { LOAD_TASK_AUDIT_LOG_FEATURE_KEY } from 'app/backend-status/reducers/load-task-audit-log.reducer'; import { EffectsModule } from '@ngrx/effects'; -import { TaskAuditLogEffects } from 'app/backend-status/effects/task-audit-log.effects'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { Confirmation, ConfirmationService, MessageService } from 'primeng/api'; import { LibraryModule } from 'app/library/library.module'; @@ -40,12 +39,16 @@ import * as fromClearTaskAuditLog from 'app/backend-status/reducers/build-detail import { ClearTaskAuditLogEffects } from 'app/backend-status/effects/clear-task-audit-log.effects'; import { AppState } from 'app/backend-status'; import { clearTaskAuditLog } from 'app/backend-status/actions/clear-task-audit-log.actions'; +import { Title } from '@angular/platform-browser'; describe('TaskAuditLogPageComponent', () => { let component: TaskAuditLogPageComponent; let fixture: ComponentFixture; let store: Store; let confirmationService: ConfirmationService; + let titleService: Title; + let breadcrumbAdaptor: BreadcrumbAdaptor; + let translateService: TranslateService; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -58,18 +61,15 @@ describe('TaskAuditLogPageComponent', () => { LoggerModule.forRoot(), StoreModule.forRoot({}), StoreModule.forFeature( - TASK_AUDIT_LOG_FEATURE_KEY, - fromTaskAuditLog.reducer + LOAD_TASK_AUDIT_LOG_FEATURE_KEY, + fromLoadTaskAuditLog.reducer ), StoreModule.forFeature( CLEAR_TASK_AUDIT_LOG_FEATURE_KEY, fromClearTaskAuditLog.reducer ), EffectsModule.forRoot([]), - EffectsModule.forFeature([ - TaskAuditLogEffects, - ClearTaskAuditLogEffects - ]), + EffectsModule.forFeature([ClearTaskAuditLogEffects]), TableModule, ScrollPanelModule, ToolbarModule, @@ -85,12 +85,31 @@ describe('TaskAuditLogPageComponent', () => { store = TestBed.get(Store); spyOn(store, 'dispatch').and.callThrough(); confirmationService = TestBed.get(ConfirmationService); + titleService = TestBed.get(Title); + spyOn(titleService, 'setTitle'); + breadcrumbAdaptor = TestBed.get(BreadcrumbAdaptor); + spyOn(breadcrumbAdaptor, 'loadEntries'); + translateService = TestBed.get(TranslateService); })); it('should create', () => { expect(component).toBeTruthy(); }); + describe('when the language changes', () => { + beforeEach(() => { + translateService.use('fr'); + }); + + it('resets the title', () => { + expect(titleService.setTitle).toHaveBeenCalled(); + }); + + it('reloads the breadcrumb trail', () => { + expect(breadcrumbAdaptor.loadEntries).toHaveBeenCalled(); + }); + }); + describe('clearing the task audit log', () => { beforeEach(() => { spyOn(confirmationService, 'confirm').and.callFake( diff --git a/comixed-frontend/src/app/backend-status/pages/task-audit-log-page/task-audit-log-page.component.ts b/comixed-frontend/src/app/backend-status/pages/task-audit-log-page/task-audit-log-page.component.ts index 3aefcbc6f..85bacb53a 100644 --- a/comixed-frontend/src/app/backend-status/pages/task-audit-log-page/task-audit-log-page.component.ts +++ b/comixed-frontend/src/app/backend-status/pages/task-audit-log-page/task-audit-log-page.component.ts @@ -20,7 +20,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; import { TaskAuditLogEntry } from 'app/backend-status/models/task-audit-log-entry'; import { LoggerService } from '@angular-ru/logger'; -import { TaskAuditLogAdaptor } from 'app/backend-status/adaptors/task-audit-log.adaptor'; import { LibraryDisplayAdaptor } from 'app/user/adaptors/library-display.adaptor'; import { BreadcrumbAdaptor } from 'app/adaptors/breadcrumb.adaptor'; import { TranslateService } from '@ngx-translate/core'; @@ -30,6 +29,13 @@ import { Store } from '@ngrx/store'; import { AppState } from 'app/backend-status'; import { clearTaskAuditLog } from 'app/backend-status/actions/clear-task-audit-log.actions'; import { selectClearTaskingAuditLogWorking } from 'app/backend-status/selectors/clear-task-audit-log.selectors'; +import { + selectLoadTaskAuditLogState, + selectTaskAuditLogEntries +} from 'app/backend-status/selectors/load-task-audit-log.selectors'; +import { loadTaskAuditLogEntries } from 'app/backend-status/actions/load-task-audit-log.actions'; +import { LoadTaskAuditLogState } from 'app/backend-status/reducers/load-task-audit-log.reducer'; +import { filter } from 'rxjs/operators'; @Component({ selector: 'app-task-audit-log-page', @@ -37,9 +43,6 @@ import { selectClearTaskingAuditLogWorking } from 'app/backend-status/selectors/ styleUrls: ['./task-audit-log-page.component.scss'] }) export class TaskAuditLogPageComponent implements OnInit, OnDestroy { - fetchingSubscription: Subscription; - fetching = false; - entriesSubscription: Subscription; entries: TaskAuditLogEntry[] = []; rowsSubscription: Subscription; rows = 0; @@ -48,7 +51,6 @@ export class TaskAuditLogPageComponent implements OnInit, OnDestroy { constructor( private logger: LoggerService, - private taskAuditLogAdaptor: TaskAuditLogAdaptor, private libraryDisplayAdaptor: LibraryDisplayAdaptor, private breadcrumbAdaptor: BreadcrumbAdaptor, private translateService: TranslateService, @@ -56,20 +58,22 @@ export class TaskAuditLogPageComponent implements OnInit, OnDestroy { private confirmationService: ConfirmationService, private store: Store ) { + this.store + .select(selectLoadTaskAuditLogState) + .pipe(filter(state => !!state)) + .subscribe((state: LoadTaskAuditLogState) => { + if (!state.loading) { + this.store.dispatch(loadTaskAuditLogEntries({ since: state.latest })); + } + }); + this.store + .select(selectTaskAuditLogEntries) + .pipe(filter(state => !!state)) + .subscribe(entries => (this.entries = entries)); this.store .select(selectClearTaskingAuditLogWorking) + .pipe(filter(state => !!state)) .subscribe(clearing => (this.clearingAuditLog = clearing)); - this.fetchingSubscription = this.taskAuditLogAdaptor.fetchingEntries$.subscribe( - fetching => { - this.fetching = fetching; - if (fetching === false) { - this.taskAuditLogAdaptor.getEntries(); - } - } - ); - this.entriesSubscription = this.taskAuditLogAdaptor.entries$.subscribe( - entries => (this.entries = entries) - ); this.rowsSubscription = this.libraryDisplayAdaptor.rows$.subscribe( rows => (this.rows = rows) ); @@ -84,8 +88,6 @@ export class TaskAuditLogPageComponent implements OnInit, OnDestroy { ngOnInit() {} ngOnDestroy() { - this.fetchingSubscription.unsubscribe(); - this.entriesSubscription.unsubscribe(); this.rowsSubscription.unsubscribe(); } diff --git a/comixed-frontend/src/app/backend-status/reducers/load-task-audit-log.reducer.spec.ts b/comixed-frontend/src/app/backend-status/reducers/load-task-audit-log.reducer.spec.ts new file mode 100644 index 000000000..3fae04682 --- /dev/null +++ b/comixed-frontend/src/app/backend-status/reducers/load-task-audit-log.reducer.spec.ts @@ -0,0 +1,121 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { + initialState, + LoadTaskAuditLogState, + reducer +} from './load-task-audit-log.reducer'; +import { + loadTaskAuditLogEntries, + loadTaskAuditLogFailed, + taskAuditLogEntriesLoaded +} from 'app/backend-status/actions/load-task-audit-log.actions'; +import { + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_2, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 +} from 'app/backend-status/backend-status.fixtures'; + +describe('LoadTaskAuditLog Reducer', () => { + const LOG_ENTRIES = [ + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_2, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 + ]; + const LATEST = new Date().getTime(); + + let state: LoadTaskAuditLogState; + + beforeEach(() => { + state = initialState; + }); + + describe('the initial state', () => { + beforeEach(() => { + state = reducer({ ...state }, {} as any); + }); + + it('clears the loading flag', () => { + expect(state.loading).toBeFalsy(); + }); + + it('has no entries', () => { + expect(state.entries).toEqual([]); + }); + + it('has a default latest date', () => { + expect(state.latest).toEqual(0); + }); + }); + + describe('when loading the entries', () => { + beforeEach(() => { + state = reducer( + { ...state, loading: false }, + loadTaskAuditLogEntries({ since: new Date().getTime() }) + ); + }); + + it('sets the load flag', () => { + expect(state.loading).toBeTruthy(); + }); + }); + + describe('when the entries were loaded', () => { + beforeEach(() => { + state = reducer( + { + ...state, + loading: true, + entries: [] + }, + taskAuditLogEntriesLoaded({ + entries: LOG_ENTRIES, + latest: LATEST + }) + ); + }); + + it('clears the load flag', () => { + expect(state.loading).toBeFalsy(); + }); + + it('updates the list of entries', () => { + expect(state.entries).toEqual(LOG_ENTRIES); + }); + + it('updates the latest date', () => { + expect(state.latest).toEqual(LATEST); + }); + }); + + describe('loading the entries', () => { + beforeEach(() => { + state = reducer({ ...state, loading: true }, loadTaskAuditLogFailed()); + }); + + it('clears the load flag', () => { + expect(state.loading).toBeFalsy(); + }); + }); +}); diff --git a/comixed-frontend/src/app/backend-status/reducers/load-task-audit-log.reducer.ts b/comixed-frontend/src/app/backend-status/reducers/load-task-audit-log.reducer.ts new file mode 100644 index 000000000..0a648ca0a --- /dev/null +++ b/comixed-frontend/src/app/backend-status/reducers/load-task-audit-log.reducer.ts @@ -0,0 +1,62 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { Action, createReducer, on } from '@ngrx/store'; +import { TaskAuditLogEntry } from 'app/backend-status/models/task-audit-log-entry'; +import { + loadTaskAuditLogEntries, + loadTaskAuditLogFailed, + taskAuditLogEntriesLoaded +} from 'app/backend-status/actions/load-task-audit-log.actions'; + +export const LOAD_TASK_AUDIT_LOG_FEATURE_KEY = 'load_task_audit_log_state'; + +export interface LoadTaskAuditLogState { + loading: boolean; + entries: TaskAuditLogEntry[]; + latest: number; +} + +export const initialState: LoadTaskAuditLogState = { + loading: false, + entries: [], + latest: 0 +}; + +const loadTaskAuditLogReducer = createReducer( + initialState, + + on(loadTaskAuditLogEntries, state => ({ ...state, loading: true })), + on(taskAuditLogEntriesLoaded, (state, action) => { + const entries = state.entries.concat(action.entries); + return { + ...state, + loading: false, + entries: entries, + latest: action.latest + }; + }), + on(loadTaskAuditLogFailed, state => ({ ...state, loading: false })) +); + +export function reducer( + state: LoadTaskAuditLogState | undefined, + action: Action +) { + return loadTaskAuditLogReducer(state, action); +} diff --git a/comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.spec.ts b/comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.spec.ts deleted file mode 100644 index fad1b6b71..000000000 --- a/comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * ComiXed - A digital comic book library management application. - * Copyright (C) 2020, The ComiXed Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - */ - -import { - initialState, - reducer, - TaskAuditLogState -} from './task-audit-log.reducer'; -import { - GetTaskAuditLogEntries, - GetTaskAuditLogEntriesFailed, - ReceivedTaskAuditLogEntries -} from 'app/backend-status/actions/task-audit-log.actions'; -import { - TASK_AUDIT_LOG_ENTRY_1, - TASK_AUDIT_LOG_ENTRY_2, - TASK_AUDIT_LOG_ENTRY_3, - TASK_AUDIT_LOG_ENTRY_4, - TASK_AUDIT_LOG_ENTRY_5 -} from 'app/backend-status/models/task-audit-log-entry.fixtures'; - -describe('TaskAuditLog Reducer', () => { - const LOG_ENTRIES = [ - TASK_AUDIT_LOG_ENTRY_1, - TASK_AUDIT_LOG_ENTRY_3, - TASK_AUDIT_LOG_ENTRY_4, - TASK_AUDIT_LOG_ENTRY_5 - ]; - - let state: TaskAuditLogState; - - beforeEach(() => { - state = initialState; - }); - - describe('the initial state', () => { - beforeEach(() => { - state = reducer(state, {} as any); - }); - - it('clears the fetching entries flag', () => { - expect(state.fetchingEntries).toBeFalsy(); - }); - - it('has an empty set of entries', () => { - expect(state.entries).toEqual([]); - }); - - it('has a last entry date of 0', () => { - expect(state.lastEntryDate).toEqual(0); - }); - }); - - describe('fetching a set of entries', () => { - beforeEach(() => { - state = reducer( - { ...state, fetchingEntries: false }, - new GetTaskAuditLogEntries({ cutoff: 0 }) - ); - }); - - it('sets the fetching flag', () => { - expect(state.fetchingEntries).toBeTruthy(); - }); - }); - - describe('receiving a set of entries', () => { - beforeEach(() => { - state = reducer( - { - ...state, - fetchingEntries: true, - entries: [TASK_AUDIT_LOG_ENTRY_2], - lastEntryDate: null - }, - new ReceivedTaskAuditLogEntries({ entries: LOG_ENTRIES }) - ); - }); - - it('clears the fetching flag', () => { - expect(state.fetchingEntries).toBeFalsy(); - }); - - it('merges the set of entries', () => { - expect( - LOG_ENTRIES.every(entry => state.entries.includes(entry)) - ).toBeTruthy(); - }); - - it('updates the last entry date', () => { - expect(state.lastEntryDate).not.toBeNull(); - }); - }); - - describe('failure to receive audit log entries', () => { - beforeEach(() => { - state = reducer( - { ...state, fetchingEntries: true }, - new GetTaskAuditLogEntriesFailed() - ); - }); - - it('clears the fetching flag', () => { - expect(state.fetchingEntries).toBeFalsy(); - }); - }); -}); diff --git a/comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.ts b/comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.ts deleted file mode 100644 index e4316f17f..000000000 --- a/comixed-frontend/src/app/backend-status/reducers/task-audit-log.reducer.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * ComiXed - A digital comic book library management application. - * Copyright (C) 2020, The ComiXed Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - */ - -import { - TaskAuditLogActions, - TaskAuditLogActionTypes -} from '../actions/task-audit-log.actions'; -import { TaskAuditLogEntry } from 'app/backend-status/models/task-audit-log-entry'; - -export const TASK_AUDIT_LOG_FEATURE_KEY = 'task_audit_log_state'; - -export interface TaskAuditLogState { - fetchingEntries: boolean; - entries: TaskAuditLogEntry[]; - lastEntryDate: number; -} - -export const initialState: TaskAuditLogState = { - fetchingEntries: false, - entries: [], - lastEntryDate: 0 -}; - -export function reducer( - state = initialState, - action: TaskAuditLogActions -): TaskAuditLogState { - switch (action.type) { - case TaskAuditLogActionTypes.GetEntries: - return { ...state, fetchingEntries: true }; - - case TaskAuditLogActionTypes.ReceivedEntries: { - const entries = state.entries.concat(action.payload.entries); - let lastEntryDate = state.lastEntryDate; - entries.forEach(entry => { - if (entry.startTime > lastEntryDate) { - lastEntryDate = entry.startTime; - } - }); - return { - ...state, - fetchingEntries: false, - entries: entries, - lastEntryDate: lastEntryDate - }; - } - - case TaskAuditLogActionTypes.GetEntriesFailed: - return { ...state, fetchingEntries: false }; - - default: - return state; - } -} diff --git a/comixed-frontend/src/app/backend-status/selectors/load-task-audit-log.selectors.spec.ts b/comixed-frontend/src/app/backend-status/selectors/load-task-audit-log.selectors.spec.ts new file mode 100644 index 000000000..3bd2ea655 --- /dev/null +++ b/comixed-frontend/src/app/backend-status/selectors/load-task-audit-log.selectors.spec.ts @@ -0,0 +1,81 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import {LOAD_TASK_AUDIT_LOG_FEATURE_KEY, LoadTaskAuditLogState} from '../reducers/load-task-audit-log.reducer'; +import { + selectLoadTaskAuditLogState, + selectTaskAuditLogEntries, + selectTaskAuditLogLatest, + selectTaskAuditLogLoading +} from './load-task-audit-log.selectors'; +import { + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_2, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 +} from 'app/backend-status/backend-status.fixtures'; + +describe('LoadTaskAuditLog Selectors', () => { + const LOG_ENTRIES = [ + TASK_AUDIT_LOG_ENTRY_1, + TASK_AUDIT_LOG_ENTRY_2, + TASK_AUDIT_LOG_ENTRY_3, + TASK_AUDIT_LOG_ENTRY_4, + TASK_AUDIT_LOG_ENTRY_5 + ]; + let state: LoadTaskAuditLogState; + + beforeEach(() => { + state = { + latest: new Date().getTime(), + entries: LOG_ENTRIES, + loading: Math.random() * 100 > 50 + } as LoadTaskAuditLogState; + }); + + it('can select the feature state', () => { + expect( + selectLoadTaskAuditLogState({ [LOAD_TASK_AUDIT_LOG_FEATURE_KEY]: state }) + ).toEqual(state); + }); + + it('can select the loading state', () => { + expect( + selectTaskAuditLogLoading({ + [LOAD_TASK_AUDIT_LOG_FEATURE_KEY]: state + }) + ).toEqual(state.loading); + }); + + it('can select the entries', () => { + expect( + selectTaskAuditLogEntries({ + [LOAD_TASK_AUDIT_LOG_FEATURE_KEY]: state + }) + ).toEqual(state.entries); + }); + + it('can select the latest entry date', () => { + expect( + selectTaskAuditLogLatest({ + [LOAD_TASK_AUDIT_LOG_FEATURE_KEY]: state + }) + ).toEqual(state.latest); + }); +}); diff --git a/comixed-frontend/src/app/backend-status/selectors/load-task-audit-log.selectors.ts b/comixed-frontend/src/app/backend-status/selectors/load-task-audit-log.selectors.ts new file mode 100644 index 000000000..79faece1c --- /dev/null +++ b/comixed-frontend/src/app/backend-status/selectors/load-task-audit-log.selectors.ts @@ -0,0 +1,52 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { createFeatureSelector, createSelector } from '@ngrx/store'; +import * as fromLoadTaskAuditLog from '../reducers/load-task-audit-log.reducer'; +import { LoadTaskAuditLogState } from '../reducers/load-task-audit-log.reducer'; + +/** + * Selects the loading task audit log state. + */ +export const selectLoadTaskAuditLogState = createFeatureSelector< + LoadTaskAuditLogState +>(fromLoadTaskAuditLog.LOAD_TASK_AUDIT_LOG_FEATURE_KEY); + +/** + * Selects the loading task audit log loading flag. + */ +export const selectTaskAuditLogLoading = createSelector( + selectLoadTaskAuditLogState, + (state: LoadTaskAuditLogState) => state.loading +); + +/** + * Selects the task audit log entries. + */ +export const selectTaskAuditLogEntries = createSelector( + selectLoadTaskAuditLogState, + (state: LoadTaskAuditLogState) => state.entries +); + +/** + * Selects the task audit log latest date. + */ +export const selectTaskAuditLogLatest = createSelector( + selectLoadTaskAuditLogState, + (state: LoadTaskAuditLogState) => state.latest +); diff --git a/comixed-frontend/src/assets/i18n/en/backend-status.json b/comixed-frontend/src/assets/i18n/en/backend-status.json index a128fad0a..25c1e711c 100644 --- a/comixed-frontend/src/assets/i18n/en/backend-status.json +++ b/comixed-frontend/src/assets/i18n/en/backend-status.json @@ -41,6 +41,15 @@ } }, "task": { + "list": { + "effects": { + "load": { + "error": { + "detail": "Failed to load the log entries..." + } + } + } + }, "clear-audit-log": { "tooltip": "Clear the task audit log...", "confirm-header": "Clear Task Audit Log", diff --git a/comixed-frontend/src/assets/i18n/es/backend-status.json b/comixed-frontend/src/assets/i18n/es/backend-status.json index 09b058c9c..9bc268b89 100644 --- a/comixed-frontend/src/assets/i18n/es/backend-status.json +++ b/comixed-frontend/src/assets/i18n/es/backend-status.json @@ -41,6 +41,15 @@ } }, "task": { + "list": { + "effects": { + "load": { + "error": { + "detail": "Failed to load the log entries..." + } + } + } + }, "clear-audit-log": { "tooltip": "Clear the task audit log...", "confirm-header": "Clear Task Audit Log", diff --git a/comixed-frontend/src/assets/i18n/fr/backend-status.json b/comixed-frontend/src/assets/i18n/fr/backend-status.json index 3cebc2351..e8ea044de 100644 --- a/comixed-frontend/src/assets/i18n/fr/backend-status.json +++ b/comixed-frontend/src/assets/i18n/fr/backend-status.json @@ -41,6 +41,15 @@ } }, "task": { + "list": { + "effects": { + "load": { + "error": { + "detail": "Failed to load the log entries..." + } + } + } + }, "clear-audit-log": { "tooltip": "Clear the task audit log...", "confirm-header": "Clear Task Audit Log", diff --git a/comixed-frontend/src/assets/i18n/pt/backend-status.json b/comixed-frontend/src/assets/i18n/pt/backend-status.json index a128fad0a..25c1e711c 100644 --- a/comixed-frontend/src/assets/i18n/pt/backend-status.json +++ b/comixed-frontend/src/assets/i18n/pt/backend-status.json @@ -41,6 +41,15 @@ } }, "task": { + "list": { + "effects": { + "load": { + "error": { + "detail": "Failed to load the log entries..." + } + } + } + }, "clear-audit-log": { "tooltip": "Clear the task audit log...", "confirm-header": "Clear Task Audit Log",