Browse Source

Improve error handler system

pull/17481/head
Masum ULU 2 years ago
parent
commit
0f44dc5586
  1. 32
      npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts
  2. 44
      npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts
  3. 6
      npm/ng-packs/packages/theme-shared/src/lib/models/common.ts
  4. 48
      npm/ng-packs/packages/theme-shared/src/lib/services/create-error-component.service.ts
  5. 29
      npm/ng-packs/packages/theme-shared/src/lib/services/router-error-handler.service.ts
  6. 88
      npm/ng-packs/packages/theme-shared/src/lib/services/status-code-error-handler.service.ts

32
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<KeyboardEvent>(document, 'keyup').pipe(
const keyup$ = fromEvent<KeyboardEvent>(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();
}

44
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;
};
}

6
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<ConfirmationIcons>;
}
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;
}

48
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<HttpErrorWrapperComponent> | 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<HttpErrorWrapperComponent>) {
execute(instance: Partial<HttpErrorWrapperComponent>): void {
const renderer = this.rendererFactory.createRenderer(null, null);
const hostElement = this.getErrorHostElement();
const host = renderer.selectRootElement(hostElement, true);

29
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: <ErrorScreenErrorCodes>404,
};
this.createErrorComponentService.execute(instance);

88
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<Confirmation.Status> {
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<Confirmation.Status> {
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);
}
}

Loading…
Cancel
Save