Skip to content
Snippets Groups Projects
Commit 76855f25 authored by OZGCloud's avatar OZGCloud
Browse files

Merge pull request 'OZG-2792 Snackbar anzeigen bei fehlendem...

Merge pull request 'OZG-2792 Snackbar anzeigen bei fehlendem ElasticSearch-Server (HTTP 503)' (#91) from OZG-2792_Suchserver_nicht_erreichbar into master

Reviewed-on: https://git.ozg-sh.de/mgm/goofy/pulls/91
parents 7242f896 79e054cb
No related branches found
No related tags found
No related merge requests found
Showing
with 134 additions and 31 deletions
import { HttpErrorResponse } from '@angular/common/http';
import { ApiError, MessageCode } from '@goofy-client/tech-shared';
import { isNil } from 'lodash-es';
......@@ -8,3 +9,7 @@ export function isApiError(value: any): boolean {
export function isServiceUnavailableMessageCode(error: ApiError): boolean {
return error.issues[0].messageCode == MessageCode.SERVICE_UNAVAILABLE;
}
export function getApiErrorFromHttpErrorResponse(httpErrorResponse: HttpErrorResponse): ApiError {
return httpErrorResponse?.error?.error;
}
\ No newline at end of file
import { HttpErrorResponse } from '@angular/common/http';
import { faker } from '@faker-js/faker';
import { ApiError, Issue, IssueParam } from '../src/lib/tech.model';
......@@ -27,3 +28,11 @@ export function createApiError(): ApiError {
export function createError(): unknown {
return {};
}
export function createHttpErrorResponse(apiError: ApiError = null): HttpErrorResponse {
return <HttpErrorResponse>{
error: {
error: apiError ?? createApiError()
}
};
}
import { HttpErrorResponse } from '@angular/common/http';
import { ApiRootResource } from '@goofy-client/api-root-shared';
import { ApiError } from '@goofy-client/tech-shared';
import { ActionCreator, createAction, props } from '@ngrx/store';
......@@ -24,6 +25,9 @@ export interface ApiRootAction {
export interface ApiErrorAction {
apiError: ApiError
}
export interface HttpErrorAction {
httpErrorResponse: HttpErrorResponse
}
export interface VorgangListAction {
vorgangList: VorgangListResource
......@@ -34,6 +38,7 @@ export const noOperation: TypedActionCreator = createAction('[Vorgang-Routing] N
export const loadVorgangList: VorgangActionCreator<ApiRootAction> = createAction('[Vorgang] Load VorgangList', props<ApiRootAction>());
export const searchVorgaengeBy: VorgangActionCreator<SearchVorgaengeByProps> = createAction('[Vorgang] Search VorgangList', props<SearchVorgaengeByProps>());
export const searchVorgaengeBySuccess: VorgangActionCreator<VorgangListAction> = createAction('[Vorgang] Search VorgangList Success', props<VorgangListAction>());
export const searchVorgaengeByFailure: VorgangActionCreator<HttpErrorAction> = createAction('[Vorgang] Search VorgangList Failure', props<HttpErrorAction>());
export const loadMyVorgaengeList: VorgangActionCreator<ApiRootAction> = createAction('[Vorgang] Load MyVorgaengList', props<ApiRootAction>());
export const loadVorgangListSuccess: VorgangActionCreator<VorgangListAction> = createAction('[Vorgang] Load VorgangList Success', props<VorgangListAction>());
......@@ -44,6 +49,6 @@ export const loadNextPageSuccess: VorgangActionCreator<VorgangListAction> = crea
export const searchForPreview: VorgangActionCreator<StringBasedProps> = createAction('[Vorgang] Search for preview', props<StringBasedProps>());
export const searchForPreviewSuccess: VorgangActionCreator<VorgangListAction> = createAction('[Vorgang] Search for preview Success', props<VorgangListAction>());
export const searchForPreviewFailure: VorgangActionCreator<ApiErrorAction> = createAction('[Vorgang] Search for preview Failure', props<ApiErrorAction>());
export const searchForPreviewFailure: VorgangActionCreator<HttpErrorAction> = createAction('[Vorgang] Search for preview Failure', props<HttpErrorAction>());
export const clearSearchPreviewList: TypedActionCreator = createAction('[Vorgang] Clear Search preview');
\ No newline at end of file
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';
import { ApiRootFacade, ApiRootLinkRel, ApiRootResource } from '@goofy-client/api-root-shared';
import { NavigationFacade } from '@goofy-client/navigation-shared';
import { ApiError, createStateResource } from '@goofy-client/tech-shared';
import { mock } from '@goofy-client/test-utils';
import { SnackBarService } from '@goofy-client/ui';
import { provideMockActions } from '@ngrx/effects/testing';
import { Action } from '@ngrx/store';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { NxModule } from '@nrwl/angular';
import { cold, hot } from 'jest-marbles';
import { createApiRootResource } from 'libs/api-root-shared/test/api-root';
import { createApiError } from 'libs/tech-shared/test/error';
import { createApiError, createHttpErrorResponse } from 'libs/tech-shared/test/error';
import { createVorgangListResource } from 'libs/vorgang-shared/test/vorgang';
import { Observable, of } from 'rxjs';
import { createRouteData } from '../../../../navigation-shared/test/navigation-test-factory';
......@@ -27,6 +29,7 @@ describe('VorgangEffects', () => {
const apiRootFacade = mock(ApiRootFacade);
const vorgangRepository = mock(VorgangRepository);
const navigationFacade = mock(NavigationFacade);
const snackbarService = mock(SnackBarService);
const vorgangList: VorgangListResource = createVorgangListResource();
......@@ -48,6 +51,10 @@ describe('VorgangEffects', () => {
{
provide: NavigationFacade,
useValue: navigationFacade
},
{
provide: SnackBarService,
useValue: snackbarService
}
]
});
......@@ -119,13 +126,12 @@ describe('VorgangEffects', () => {
expect(effects.searchVorgaengeBy$).toBeObservable(expected);
})
it('should dispatch loadVorgangListFailure action', () => {
const apiError: ApiError = createApiError()
const error = { error: { error: apiError } };
it('should dispatch searchVorgaengeByFailure action', () => {
const error: HttpErrorResponse = createHttpErrorResponse();
const errorResponse = cold('-#', {}, error);
vorgangRepository.searchVorgaengeBy = jest.fn(() => errorResponse);
const expected = cold('--b', { b: VorgangActions.loadVorgangListFailure({ apiError }) });
const expected = cold('--b', { b: VorgangActions.searchVorgaengeByFailure({ httpErrorResponse: error }) });
actions = hot('-a', { a: action });
expect(effects.searchVorgaengeBy$).toBeObservable(expected);
......@@ -189,7 +195,7 @@ describe('VorgangEffects', () => {
expect(vorgangRepository.getNextVorgangListPage).toHaveBeenCalledWith(vorgangList);
})
it('should dispatch loadVorgangListSuccess action', () => {
it('should dispatch loadNextPageSuccess action', () => {
vorgangRepository.getNextVorgangListPage.mockReturnValue(of(vorgangList));
actions = hot('-a-|', { a: action });
......@@ -258,15 +264,44 @@ describe('VorgangEffects', () => {
})
it('should dispatch searchForPreviewFailure action', () => {
const apiError: ApiError = createApiError()
const error = { error: { error: apiError } };
const error: HttpErrorResponse = createHttpErrorResponse();
const errorResponse = cold('-#', {}, error);
vorgangRepository.searchVorgaengeBy = jest.fn(() => errorResponse);
const expected = cold('--c', { c: VorgangActions.searchForPreviewFailure({ apiError }) });
const expected = cold('--c', { c: VorgangActions.searchForPreviewFailure({ httpErrorResponse: error }) });
actions = hot('-a', { a: action });
expect(effects.searchForPreview$).toBeObservable(expected);
})
})
describe('search error', () => {
const action = VorgangActions.searchForPreviewFailure({ httpErrorResponse: null });
const error: HttpErrorResponse = createHttpErrorResponse();
it('should trigger showSearchError$', () => {
actions = of(action);
effects.showSearchError = jest.fn();
effects.showSearchError$.subscribe();
expect(effects.showSearchError).toHaveBeenCalled();
})
it('should call snackbarService.showError if HTTP status code is NOT 503 ', () => {
error.error.status = HttpStatusCode.ExpectationFailed;
effects.showSearchError(error);
expect(snackbarService.showError).not.toHaveBeenCalled();
})
it('should call snackbarService.showError if HTTP status code is 503 ', () => {
error.error.status = HttpStatusCode.ServiceUnavailable;
effects.showSearchError(error);
expect(snackbarService.showError).toHaveBeenCalled();
})
})
});
\ No newline at end of file
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ApiRootFacade } from '@goofy-client/api-root-shared';
import { NavigationFacade } from '@goofy-client/navigation-shared';
import { ApiError } from '@goofy-client/tech-shared';
import { getApiErrorFromHttpErrorResponse, isServiceUnavailable } from '@goofy-client/tech-shared';
import { SnackBarService } from '@goofy-client/ui';
import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { of } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';
import { getSearchLinkRel } from '../vorgang-navigation.util';
import { VorgangMessages } from '../vorgang.messages';
import { VorgangRepository } from '../vorgang.repository';
import * as VorgangActions from './vorgang.actions';
import { ApiRootAction, SearchVorgaengeByProps } from './vorgang.actions';
import { ApiRootAction, HttpErrorAction, SearchVorgaengeByProps } from './vorgang.actions';
import * as VorgangSelectors from './vorgang.selectors';
@Injectable()
......@@ -19,14 +22,14 @@ export class VorgangEffects {
static readonly SEARCH_QUERY_PARAM: string = 'search';
static readonly MY_VORGAENGE_URI_SEGMENT: string = 'myVorgaenge';
constructor(private readonly actions$: Actions, private store: Store, private repository: VorgangRepository, private apiRootFacade: ApiRootFacade, private navigationFacade: NavigationFacade) { }
constructor(private readonly actions$: Actions, private store: Store, private repository: VorgangRepository, private apiRootFacade: ApiRootFacade, private navigationFacade: NavigationFacade, private snackbarService: SnackBarService) { }
loadVorgangList$ = createEffect(() =>
this.actions$.pipe(
ofType(VorgangActions.loadVorgangList),
switchMap((action: ApiRootAction) => this.repository.loadVorgangList(action.apiRoot).pipe(
map(loadedVorgangList => VorgangActions.loadVorgangListSuccess({ vorgangList: loadedVorgangList })),
catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: this.getApiErrorFromHttpError(error) })))
catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: getApiErrorFromHttpErrorResponse(error) })))
))
)
)
......@@ -36,7 +39,7 @@ export class VorgangEffects {
ofType(VorgangActions.searchVorgaengeBy),
switchMap((action: SearchVorgaengeByProps) => this.repository.searchVorgaengeBy(action.apiRoot, action.searchString, action.linkRel).pipe(
map(loadedVorgangList => VorgangActions.searchVorgaengeBySuccess({ vorgangList: loadedVorgangList })),
catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: this.getApiErrorFromHttpError(error) })))
catchError(error => of(VorgangActions.searchVorgaengeByFailure({ httpErrorResponse: error })))
))
)
)
......@@ -46,7 +49,7 @@ export class VorgangEffects {
ofType(VorgangActions.loadMyVorgaengeList),
switchMap((action: ApiRootAction) => this.repository.loadMyVorgaengeList(action.apiRoot).pipe(
map(loadedVorgangList => VorgangActions.loadVorgangListSuccess({ vorgangList: loadedVorgangList })),
catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: this.getApiErrorFromHttpError(error) })))
catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: getApiErrorFromHttpErrorResponse(error) })))
))
)
)
......@@ -57,7 +60,7 @@ export class VorgangEffects {
concatLatestFrom(() => this.store.select(VorgangSelectors.vorgangList)),
switchMap(([, vorgangList]) => this.repository.getNextVorgangListPage(vorgangList.resource).pipe(
map(loadedVorgangList => VorgangActions.loadNextPageSuccess({ vorgangList: loadedVorgangList })),
catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: this.getApiErrorFromHttpError(error) })))
catchError(error => of(VorgangActions.loadVorgangListFailure({ apiError: getApiErrorFromHttpErrorResponse(error) })))
))
)
)
......@@ -69,13 +72,25 @@ export class VorgangEffects {
switchMap(([stringBasedProps, apiRoot, currentRouteData]) => {
return this.repository.searchVorgaengeBy(apiRoot.resource, stringBasedProps.string, getSearchLinkRel(currentRouteData), VorgangEffects.SEARCH_PREVIEW_LIST_LIMIT).pipe(
map(loadedVorgangList => VorgangActions.searchForPreviewSuccess({ vorgangList: loadedVorgangList })),
catchError(error => of(VorgangActions.searchForPreviewFailure({ apiError: this.getApiErrorFromHttpError(error) })))
catchError(error => of(VorgangActions.searchForPreviewFailure({ httpErrorResponse: error })))
)
})
)
)
private getApiErrorFromHttpError(error: any): ApiError {
return error.error.error;
showSearchError$ = createEffect(() =>
this.actions$.pipe(
ofType(
VorgangActions.searchForPreviewFailure,
VorgangActions.searchVorgaengeByFailure
),
map((action: HttpErrorAction) => this.showSearchError(action.httpErrorResponse)),
), { dispatch: false }
);
showSearchError(error: HttpErrorResponse): void {
if (isServiceUnavailable(error.error.status)) {
this.snackbarService.showError(VorgangMessages.SEARCH_UNAVAILABLE);
}
}
}
\ No newline at end of file
import { HttpErrorResponse } from '@angular/common/http';
import { UrlSegment } from '@angular/router';
import { ApiRootResource } from '@goofy-client/api-root-shared';
import { RouteData } from '@goofy-client/navigation-shared';
......@@ -7,7 +8,7 @@ import { createApiRootResource } from 'libs/api-root-shared/test/api-root';
import { createRouteData } from 'libs/navigation-shared/test/navigation-test-factory';
import { createVorgangListResource, createVorgangListResourceWithResource, createVorgangResource, createVorgangResources } from 'libs/vorgang-shared/test/vorgang';
import * as NavigationActions from '../../../../navigation-shared/src/lib/+state/navigation.actions';
import { createApiError } from '../../../../tech-shared/test/error';
import { createApiError, createHttpErrorResponse } from '../../../../tech-shared/test/error';
import * as VorgangNavigationUtil from '../vorgang-navigation.util';
import { VorgangListLinkRel } from '../vorgang.linkrel';
import { VorgangListResource, VorgangResource } from '../vorgang.model';
......@@ -183,6 +184,20 @@ describe('Vorgang Reducer', () => {
expect(state.searchPreviewList).toStrictEqual(createEmptyStateResource());
})
})
describe('on "searchVorgaengeByFailure" action', () => {
const apiError: ApiError = createApiError();
const httpErrorResponse: HttpErrorResponse = createHttpErrorResponse(apiError);
const action = VorgangActions.searchVorgaengeByFailure({ httpErrorResponse });
it('should set error', () => {
const state: VorgangState = reducer(initialState, action);
expect(state.vorgangList.error).toStrictEqual(apiError);
})
})
})
describe('searchForPreview', () => {
......@@ -214,7 +229,8 @@ describe('Vorgang Reducer', () => {
describe('on "searchForPreviewFailure" action', () => {
const apiError: ApiError = createApiError();
const action = VorgangActions.searchForPreviewFailure({ apiError });
const httpErrorResponse: HttpErrorResponse = createHttpErrorResponse(apiError);
const action = VorgangActions.searchForPreviewFailure({ httpErrorResponse });
it('should set error', () => {
const state: VorgangState = reducer(initialState, action);
......
import { RouteData } from '@goofy-client/navigation-shared';
import { createEmptyStateResource, createErrorStateResource, createStateResource, EMPTY_ARRAY, StateResource } from '@goofy-client/tech-shared';
import { createEmptyStateResource, createErrorStateResource, createStateResource, EMPTY_ARRAY, getApiErrorFromHttpErrorResponse, StateResource } from '@goofy-client/tech-shared';
import { Action, createReducer, on } from '@ngrx/store';
import * as NavigationActions from '../../../../navigation-shared/src/lib/+state/navigation.actions';
import { getSearchString, isMyVorgaenge, isSearch, isVorgangDetailPage, isVorgangListPage } from '../vorgang-navigation.util';
import { SearchInfo, VorgangListResource, VorgangResource } from '../vorgang.model';
import { getVorgaengeFromList } from '../vorgang.util';
import * as VorgangActions from './vorgang.actions';
import { ApiErrorAction, VorgangListAction } from './vorgang.actions';
import { ApiErrorAction, HttpErrorAction, VorgangListAction } from './vorgang.actions';
export const VORGANG_FEATURE_KEY = 'VorgangState';
......@@ -73,6 +73,11 @@ const vorgangReducer = createReducer(
vorgaenge: getVorgaengeFromList(action.vorgangList),
searchPreviewList: createEmptyStateResource<VorgangListResource>()
})),
on(VorgangActions.searchVorgaengeByFailure, (state: VorgangState, action: HttpErrorAction): VorgangState => ({
...state,
vorgangList: createErrorStateResource(getApiErrorFromHttpErrorResponse(action.httpErrorResponse)),
searchPreviewList: createEmptyStateResource()
})),
on(VorgangActions.searchForPreview, (state: VorgangState): VorgangState => ({
......@@ -83,9 +88,9 @@ const vorgangReducer = createReducer(
...state,
searchPreviewList: createStateResource<VorgangListResource>(action.vorgangList)
})),
on(VorgangActions.searchForPreviewFailure, (state: VorgangState, action: ApiErrorAction): VorgangState => ({
on(VorgangActions.searchForPreviewFailure, (state: VorgangState, action: HttpErrorAction): VorgangState => ({
...state,
searchPreviewList: createErrorStateResource(action.apiError)
searchPreviewList: createErrorStateResource(getApiErrorFromHttpErrorResponse(action.httpErrorResponse))
})),
on(VorgangActions.clearSearchPreviewList, (state: VorgangState): VorgangState => ({
...state,
......
......@@ -17,5 +17,6 @@ export enum VorgangMessages {
BESCHIEDEN = 'Der Vorgang wurde beschieden.',
ZURUECKGESTELLT = 'Der Vorgang wurde zurückgestellt.',
ABGESCHLOSSEN = 'Der Vorgang wurde abgeschlossen.',
WIEDEREROEFFNET = 'Der Vorgang wurde wiedereröffnet.'
WIEDEREROEFFNET = 'Der Vorgang wurde wiedereröffnet.',
SEARCH_UNAVAILABLE = 'Die Suche ist vorübergehen nicht verfügbar. Versuchen Sie es zu einem späteren Zeitpunkt erneut.'
}
......@@ -2,6 +2,6 @@
<goofy-client-spinner diameter="60" [stateResource]="vorgangListPageResource"></goofy-client-spinner>
<goofy-client-empty-list *ngIf="!vorgangListPageResource.loading && vorgangListPageResource.loaded && !(vorgaenge && vorgaenge.length)" data-test-id="empty-list"
<goofy-client-empty-list *ngIf="isEmptySearchResult()" data-test-id="empty-list"
[searchString]="searchString">
</goofy-client-empty-list>
\ No newline at end of file
......@@ -102,7 +102,7 @@ describe('VorgangListComponent', () => {
component.nextPage = <any>mock(EventEmitter);
})
it('should emit "nextPage" if necesarry', () => {
it('should emit "nextPage" if necessary', () => {
component.shouldLoadNextPage = jest.fn().mockReturnValue(true);
component.loadNextPage();
......@@ -110,7 +110,7 @@ describe('VorgangListComponent', () => {
expect(component.nextPage.emit).toHaveBeenCalled();
})
it('should emit "nextPage" if necesarry', () => {
it('should emit "nextPage" if necessary', () => {
component.shouldLoadNextPage = jest.fn().mockReturnValue(false);
component.loadNextPage();
......
......@@ -47,4 +47,16 @@ export class VorgangListComponent {
hasNextPage(): boolean {
return hasLink(this.vorgangListPageResource.resource, VorgangListLinkRel.NEXT)
}
isEmptySearchResult(): boolean {
return this.isValidList() && this.notExistsVorgaenge();
}
private notExistsVorgaenge(): boolean {
return this.vorgaenge.length === 0;
}
private isValidList(): boolean {
return !this.vorgangListPageResource.loading && this.vorgangListPageResource.loaded && !this.vorgangListPageResource.hasOwnProperty('error');
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment