From 0f44dc558698cd954a3dfeb331017bd1effe362a Mon Sep 17 00:00:00 2001 From: Masum ULU Date: Fri, 25 Aug 2023 09:59:13 +0300 Subject: [PATCH] Improve error handler system --- .../http-error-wrapper.component.ts | 32 ++++--- .../src/lib/handlers/error.handler.ts | 44 +++++----- .../theme-shared/src/lib/models/common.ts | 6 +- .../create-error-component.service.ts | 48 +++++----- .../services/router-error-handler.service.ts | 29 +++--- .../status-code-error-handler.service.ts | 88 ++++++++++--------- 6 files changed, 133 insertions(+), 114 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts index da167d1f82..7761751e8c 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts @@ -1,19 +1,22 @@ -import { LocalizationParam, SubscriptionService } from '@abp/ng.core'; import { - AfterViewInit, ApplicationRef, Component, + Injector, + inject, + OnInit, ComponentFactoryResolver, ElementRef, EmbeddedViewRef, - Injector, - OnDestroy, - OnInit, Type, ViewChild, + AfterViewInit, + OnDestroy, } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; import { fromEvent, Subject } from 'rxjs'; import { debounceTime, filter } from 'rxjs/operators'; +import { LocalizationParam, SubscriptionService } from '@abp/ng.core'; +import { ErrorScreenErrorCodes } from '../../models'; @Component({ selector: 'abp-http-error-wrapper', @@ -21,14 +24,17 @@ import { debounceTime, filter } from 'rxjs/operators'; styleUrls: ['http-error-wrapper.component.scss'], providers: [SubscriptionService], }) -export class HttpErrorWrapperComponent implements AfterViewInit, OnDestroy, OnInit { +export class HttpErrorWrapperComponent implements OnInit, AfterViewInit, OnDestroy { + protected readonly document = inject(DOCUMENT); + protected readonly window = this.document.defaultView; + appRef!: ApplicationRef; cfRes!: ComponentFactoryResolver; injector!: Injector; - status = 0; + status: ErrorScreenErrorCodes = 0; title: LocalizationParam = 'Oops!'; @@ -53,12 +59,12 @@ export class HttpErrorWrapperComponent implements AfterViewInit, OnDestroy, OnIn constructor(private subscription: SubscriptionService) {} - ngOnInit() { + ngOnInit(): void { this.backgroundColor = - window.getComputedStyle(document.body)?.getPropertyValue('background-color') || '#fff'; + this.window.getComputedStyle(this.document.body)?.getPropertyValue('background-color') || '#fff'; } - ngAfterViewInit() { + ngAfterViewInit(): void { if (this.customComponent) { const customComponentRef = this.cfRes .resolveComponentFactory(this.customComponent) @@ -74,18 +80,18 @@ export class HttpErrorWrapperComponent implements AfterViewInit, OnDestroy, OnIn customComponentRef.changeDetectorRef.detectChanges(); } - const keyup$ = fromEvent(document, 'keyup').pipe( + const keyup$ = fromEvent(this.document, 'keyup').pipe( debounceTime(150), filter((key: KeyboardEvent) => key && key.key === 'Escape'), ); this.subscription.addOne(keyup$, () => this.destroy()); } - ngOnDestroy() { + ngOnDestroy(): void { this.destroy(); } - destroy() { + destroy(): void { this.destroy$.next(); this.destroy$.complete(); } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts index 98f4ece491..ffb7407ab0 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts @@ -1,25 +1,30 @@ -import { HttpErrorReporterService } from '@abp/ng.core'; -import { HttpErrorResponse } from '@angular/common/http'; import { inject, Injectable, Injector } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; import { Observable, of, throwError } from 'rxjs'; import { catchError, filter, switchMap } from 'rxjs/operators'; + +import { HttpErrorReporterService } from '@abp/ng.core'; + import { CustomHttpErrorHandlerService } from '../models/common'; import { Confirmation } from '../models/confirmation'; -import { ConfirmationService } from '../services/confirmation.service'; + import { CUSTOM_ERROR_HANDLERS, HTTP_ERROR_HANDLER } from '../tokens/http-error.token'; +import { HTTP_ERROR_CONFIG } from '../tokens/http-error.token'; import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES } from '../constants/default-errors'; + +import { ConfirmationService } from '../services/confirmation.service'; import { RouterErrorHandlerService } from '../services/router-error-handler.service'; -import { HTTP_ERROR_CONFIG } from '../tokens/http-error.token'; @Injectable({ providedIn: 'root' }) export class ErrorHandler { - private httpErrorReporter = inject(HttpErrorReporterService); - private confirmationService = inject(ConfirmationService); - private routerErrorHandlerService = inject(RouterErrorHandlerService); - protected httpErrorConfig = inject(HTTP_ERROR_CONFIG); - private customErrorHandlers = inject(CUSTOM_ERROR_HANDLERS); - private defaultHttpErrorHandler = (_, err: HttpErrorResponse) => throwError(() => err); - private httpErrorHandler = + protected readonly httpErrorReporter = inject(HttpErrorReporterService); + protected readonly confirmationService = inject(ConfirmationService); + protected readonly routerErrorHandlerService = inject(RouterErrorHandlerService); + protected readonly httpErrorConfig = inject(HTTP_ERROR_CONFIG); + protected readonly customErrorHandlers = inject(CUSTOM_ERROR_HANDLERS); + + protected readonly defaultHttpErrorHandler = (_, err: HttpErrorResponse) => throwError(() => err); + protected readonly httpErrorHandler = inject(HTTP_ERROR_HANDLER, { optional: true }) || this.defaultHttpErrorHandler; constructor(protected injector: Injector) { @@ -34,17 +39,14 @@ export class ErrorHandler { protected listenToRestError() { this.httpErrorReporter.reporter$ .pipe(filter(this.filterRestErrors), switchMap(this.executeErrorHandler)) - .subscribe(err => { - this.handleError(err); - }); + .subscribe(err => this.handleError(err)); } - private executeErrorHandler = (error: HttpErrorResponse) => { + protected executeErrorHandler = (error: HttpErrorResponse) => { const errHandler = this.httpErrorHandler(this.injector, error); const isObservable = errHandler instanceof Observable; - const response = isObservable ? errHandler : of(null); - return response.pipe( + return (isObservable ? errHandler : of(null)).pipe( catchError(err => { this.handleError(err); return of(null); @@ -59,16 +61,18 @@ export class ErrorHandler { return (b.priority || 0) - (a.priority || 0); } - private handleError(err: unknown) { + protected handleError(err: unknown) { if (this.customErrorHandlers && this.customErrorHandlers.length) { const canHandleService = this.customErrorHandlers .sort(this.sortHttpErrorHandlers) .find(service => service.canHandle(err)); + if (canHandleService) { canHandleService.execute(); return; } } + this.showError().subscribe(); } @@ -90,10 +94,10 @@ export class ErrorHandler { protected filterRestErrors = ({ status }: HttpErrorResponse): boolean => { if (typeof status !== 'number') return false; - if (!this.httpErrorConfig.skipHandledErrorCodes) { + if (!this.httpErrorConfig || !this.httpErrorConfig.skipHandledErrorCodes) { return true; } - return this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === status) < 0; + return this.httpErrorConfig.skipHandledErrorCodes?.findIndex(code => code === status) < 0; }; } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts b/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts index 6fd9c015d3..82a14641ad 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts @@ -1,5 +1,5 @@ import { HttpErrorResponse } from '@angular/common/http'; -import { Injector, Type } from '@angular/core'; +import { Type } from '@angular/core'; import { Validation } from '@ngx-validate/core'; import { Observable } from 'rxjs'; import { ConfirmationIcons } from '../tokens/confirmation-icons.token'; @@ -10,7 +10,7 @@ export interface RootParams { confirmationIcons?: Partial; } -export type ErrorScreenErrorCodes = 401 | 403 | 404 | 500; +export type ErrorScreenErrorCodes = 0 | 401 | 403 | 404 | 500; export interface HttpErrorConfig { skipHandledErrorCodes?: ErrorScreenErrorCodes[] | number[]; @@ -26,5 +26,5 @@ export type LocaleDirection = 'ltr' | 'rtl'; export interface CustomHttpErrorHandlerService { readonly priority: number; canHandle(error: unknown): boolean; - execute(); + execute(): void; } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/create-error-component.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/create-error-component.service.ts index 6972a93f34..5a6f02aef9 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/services/create-error-component.service.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/create-error-component.service.ts @@ -8,8 +8,9 @@ import { Injector, RendererFactory2, } from '@angular/core'; -import { Subject } from 'rxjs'; +import { DOCUMENT } from '@angular/common'; import { ResolveEnd } from '@angular/router'; +import { Subject } from 'rxjs'; import { filter } from 'rxjs/operators'; import { RouterEvents } from '@abp/ng.core'; import { HTTP_ERROR_CONFIG } from '../tokens/http-error.token'; @@ -18,31 +19,20 @@ import { ErrorScreenErrorCodes } from '../models/common'; @Injectable({ providedIn: 'root' }) export class CreateErrorComponentService { - protected rendererFactory = inject(RendererFactory2); - protected cfRes = inject(ComponentFactoryResolver); - private routerEvents = inject(RouterEvents); - private injector = inject(Injector); - private httpErrorConfig = inject(HTTP_ERROR_CONFIG); + protected readonly document = inject(DOCUMENT); + protected readonly rendererFactory = inject(RendererFactory2); + protected readonly cfRes = inject(ComponentFactoryResolver); + protected readonly routerEvents = inject(RouterEvents); + protected readonly injector = inject(Injector); + protected readonly httpErrorConfig = inject(HTTP_ERROR_CONFIG); componentRef: ComponentRef | null = null; - private getErrorHostElement() { - return document.body; - } - - public canCreateCustomError(status: ErrorScreenErrorCodes) { - return !!( - this.httpErrorConfig?.errorScreen?.component && - this.httpErrorConfig?.errorScreen?.forWhichErrors && - this.httpErrorConfig?.errorScreen?.forWhichErrors.indexOf(status) > -1 - ); - } - constructor() { this.listenToRouterDataResolved(); } - protected listenToRouterDataResolved() { + protected listenToRouterDataResolved(): void { this.routerEvents .getEvents(ResolveEnd) .pipe(filter(() => !!this.componentRef)) @@ -52,11 +42,25 @@ export class CreateErrorComponentService { }); } - private isCloseIconHidden() { - return !!this.httpErrorConfig.errorScreen?.hideCloseIcon; + protected getErrorHostElement(): HTMLElement { + return this.document.body; + } + + protected isCloseIconHidden(): boolean { + return !!this.httpErrorConfig?.errorScreen?.hideCloseIcon; + } + + canCreateCustomError(status: ErrorScreenErrorCodes) { + const { component, forWhichErrors } = this.httpErrorConfig?.errorScreen || {}; + + if (!component || !forWhichErrors) { + return false; + } + + return forWhichErrors.indexOf(status) > -1; } - execute(instance: Partial) { + execute(instance: Partial): void { const renderer = this.rendererFactory.createRenderer(null, null); const hostElement = this.getErrorHostElement(); const host = renderer.selectRootElement(hostElement, true); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/router-error-handler.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/router-error-handler.service.ts index 9235204b99..89eee491e8 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/services/router-error-handler.service.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/router-error-handler.service.ts @@ -1,42 +1,43 @@ import { inject, Injectable } from '@angular/core'; +import { NavigationError } from '@angular/router'; import { filter } from 'rxjs/operators'; import { RouterEvents } from '@abp/ng.core'; -import { NavigationError } from '@angular/router'; import { HTTP_ERROR_CONFIG } from '../tokens/'; import { CreateErrorComponentService } from '../services'; import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES } from '../constants/default-errors'; +import { ErrorScreenErrorCodes } from '../models'; @Injectable({ providedIn: 'root' }) export class RouterErrorHandlerService { - private readonly routerEvents = inject(RouterEvents); - private httpErrorConfig = inject(HTTP_ERROR_CONFIG); - private createErrorComponentService = inject(CreateErrorComponentService); - - listen() { - this.routerEvents - .getNavigationEvents('Error') - .pipe(filter(this.filterRouteErrors)) - .subscribe(() => this.show404Page()); - } + protected readonly routerEvents = inject(RouterEvents); + protected readonly httpErrorConfig = inject(HTTP_ERROR_CONFIG); + protected readonly createErrorComponentService = inject(CreateErrorComponentService); protected filterRouteErrors = (navigationError: NavigationError): boolean => { - if (!this.httpErrorConfig.skipHandledErrorCodes) { + if (!this.httpErrorConfig?.skipHandledErrorCodes) { return true; } - + return ( navigationError.error?.message?.indexOf('Cannot match') > -1 && this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === 404) < 0 ); }; + listen() { + this.routerEvents + .getNavigationEvents('Error') + .pipe(filter(this.filterRouteErrors)) + .subscribe(() => this.show404Page()); + } + show404Page() { const instance = { title: { key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.title, defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title, }, - status: 404, + status: 404, }; this.createErrorComponentService.execute(instance); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/status-code-error-handler.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/status-code-error-handler.service.ts index e566ccb792..dd310afa19 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/services/status-code-error-handler.service.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/status-code-error-handler.service.ts @@ -12,19 +12,57 @@ import { CreateErrorComponentService } from './create-error-component.service'; @Injectable({ providedIn: 'root' }) export class StatusCodeErrorHandlerService implements CustomHttpErrorHandlerService { - private readonly confirmationService = inject(ConfirmationService); - private readonly createErrorComponentService = inject(CreateErrorComponentService); - private readonly authService = inject(AuthService); + protected readonly confirmationService = inject(ConfirmationService); + protected readonly createErrorComponentService = inject(CreateErrorComponentService); + protected readonly authService = inject(AuthService); + + protected readonly handledStatusCodes = [401, 403, 404, 500] as const; + protected status: typeof this.handledStatusCodes[number]; + readonly priority = CUSTOM_HTTP_ERROR_HANDLER_PRIORITY.normal; - private status: typeof this.handledStatusCodes[number]; - private readonly handledStatusCodes = [401, 403, 404, 500] as const; + + protected navigateToLogin(): void { + this.authService.navigateToLogin(); + } + + protected showConfirmation( + message: LocalizationParam, + title: LocalizationParam, + ): Observable { + return this.confirmationService.error(message, title, { + hideCancelBtn: true, + yesText: 'AbpAccount::Close', + }); + } + + protected showPage(): void { + const key = `defaultError${this.status}`; + const shouldRemoveDetail = [401, 404].indexOf(this.status) > -1; + const instance = { + title: { + key: DEFAULT_ERROR_LOCALIZATIONS[key]?.title, + defaultValue: DEFAULT_ERROR_MESSAGES[key]?.title, + }, + details: { + key: DEFAULT_ERROR_LOCALIZATIONS[key]?.details, + defaultValue: DEFAULT_ERROR_MESSAGES[key]?.details, + }, + status: this.status, + }; + + if (shouldRemoveDetail) { + delete instance.details; + } + + this.errorComponentService.execute(instance); + } canHandle({ status }): boolean { - this.status = status; + this.status = status || 0; return this.handledStatusCodes.indexOf(status) > -1; } - execute() { + execute(): void { const key = `defaultError${this.status}`; const title = { key: DEFAULT_ERROR_LOCALIZATIONS[key]?.title, @@ -36,6 +74,7 @@ export class StatusCodeErrorHandlerService implements CustomHttpErrorHandlerServ }; const canCreateCustomError = this.createErrorComponentService.canCreateCustomError(this.status); + switch (this.status) { case 401: case 404: @@ -56,39 +95,4 @@ export class StatusCodeErrorHandlerService implements CustomHttpErrorHandlerServ break; } } - - private navigateToLogin() { - this.authService.navigateToLogin(); - } - - protected showConfirmation( - message: LocalizationParam, - title: LocalizationParam, - ): Observable { - return this.confirmationService.error(message, title, { - hideCancelBtn: true, - yesText: 'AbpAccount::Close', - }); - } - - protected showPage() { - const key = `defaultError${this.status}`; - - const instance = { - title: { - key: DEFAULT_ERROR_LOCALIZATIONS[key]?.title, - defaultValue: DEFAULT_ERROR_MESSAGES[key]?.title, - }, - details: { - key: DEFAULT_ERROR_LOCALIZATIONS[key]?.details, - defaultValue: DEFAULT_ERROR_MESSAGES[key]?.details, - }, - status: this.status, - }; - const shouldRemoveDetail = [401, 404].indexOf(this.status) > -1; - if (shouldRemoveDetail) { - delete instance.details; - } - this.createErrorComponentService.execute(instance); - } }