Browse Source

Merge pull request #2119 from abpframework/feat/theme-shared/custom-error

Feat/theme shared/custom error
pull/2128/head
Yasin Aydın 7 years ago
committed by GitHub
parent
commit
fbc8c5723b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      npm/ng-packs/packages/core/src/lib/models/config.ts
  2. 2
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts
  3. 18
      npm/ng-packs/packages/theme-shared/src/lib/abstracts/toaster.ts
  4. 9
      npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts
  5. 2
      npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.ts
  6. 11
      npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.html
  7. 60
      npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.ts
  8. 221
      npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts
  9. 18
      npm/ng-packs/packages/theme-shared/src/lib/models/common.ts
  10. 9
      npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts
  11. 1
      npm/ng-packs/packages/theme-shared/src/lib/models/index.ts
  12. 10
      npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts
  13. 15
      npm/ng-packs/packages/theme-shared/src/lib/tokens/error-pages.token.ts
  14. 24
      templates/app/angular/package.json

2
npm/ng-packs/packages/core/src/lib/models/config.ts

@ -35,4 +35,6 @@ export namespace Config {
key: string;
defaultValue: string;
}
export type LocalizationParam = string | LocalizationWithDefault;
}

2
npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts

@ -163,7 +163,7 @@ export class UsersComponent implements OnInit {
}
save() {
if (!this.form.valid) return;
if (!this.form.valid || this.modalBusy) return;
this.modalBusy = true;
const { roleNames } = this.form.value;

18
npm/ng-packs/packages/theme-shared/src/lib/abstracts/toaster.ts

@ -1,6 +1,7 @@
import { MessageService } from 'primeng/components/common/messageservice';
import { Observable, Subject } from 'rxjs';
import { Toaster } from '../models/toaster';
import { Config } from '@abp/ng.core';
export abstract class AbstractToaster<T = Toaster.Options> {
status$: Subject<Toaster.Status>;
@ -11,23 +12,28 @@ export abstract class AbstractToaster<T = Toaster.Options> {
constructor(protected messageService: MessageService) {}
info(message: string, title: string, options?: T): Observable<Toaster.Status> {
info(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> {
return this.show(message, title, 'info', options);
}
success(message: string, title: string, options?: T): Observable<Toaster.Status> {
success(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> {
return this.show(message, title, 'success', options);
}
warn(message: string, title: string, options?: T): Observable<Toaster.Status> {
warn(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> {
return this.show(message, title, 'warn', options);
}
error(message: string, title: string, options?: T): Observable<Toaster.Status> {
error(message: Config.LocalizationParam, title: Config.LocalizationParam, options?: T): Observable<Toaster.Status> {
return this.show(message, title, 'error', options);
}
protected show(message: string, title: string, severity: Toaster.Severity, options?: T): Observable<Toaster.Status> {
protected show(
message: Config.LocalizationParam,
title: Config.LocalizationParam,
severity: Toaster.Severity,
options?: T,
): Observable<Toaster.Status> {
this.messageService.clear(this.key);
this.messageService.add({
@ -36,7 +42,7 @@ export abstract class AbstractToaster<T = Toaster.Options> {
summary: title || '',
...options,
key: this.key,
...(typeof (options || ({} as any)).sticky === 'undefined' && { sticky: this.sticky })
...(typeof (options || ({} as any)).sticky === 'undefined' && { sticky: this.sticky }),
});
this.status$ = new Subject<Toaster.Status>();
return this.status$;

9
npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts

@ -7,7 +7,7 @@ import { ABP } from '@abp/ng.core';
template: `
<button
#button
[attr.type]="buttonType || type"
[attr.type]="buttonType"
[ngClass]="buttonClass"
[disabled]="loading || disabled"
(click)="onClick($event)"
@ -23,7 +23,7 @@ export class ButtonComponent implements OnInit {
buttonClass = 'btn btn-primary';
@Input()
buttonType; // TODO: Add initial value.
buttonType = 'button';
@Input()
iconClass: string;
@ -49,11 +49,6 @@ export class ButtonComponent implements OnInit {
@ViewChild('button', { static: true })
buttonRef: ElementRef<HTMLButtonElement>;
/**
* @deprecated Use buttonType instead. To be deleted in v1
*/
@Input() type = 'button';
get icon(): string {
return `${this.loading ? 'fa fa-spinner fa-spin' : this.iconClass || 'd-none'}`;
}

2
npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.ts

@ -41,7 +41,7 @@ import { Toaster } from '../../models/toaster';
(click)="close(confirm)"
autofocus
>
<span>{{ message.yesText || 'AbpIdentity::Yes' | abpLocalization }}</span>
<span>{{ message.yesText || message.yesCopy || 'AbpIdentity::Yes' | abpLocalization }}</span>
</button>
</div>
</ng-template>

11
npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.html

@ -1,13 +1,12 @@
<div id="abp-error" class="error">
<button id="abp-close-button" class="mr-3" type="button" class="close" (click)="destroy()">
<div #container id="abp-error" class="error">
<button id="abp-close-button" type="button" class="close mr-3" (click)="destroy()">
<span aria-hidden="true">&times;</span>
</button>
<div class="row centered">
<div *ngIf="!customComponent" class="row centered">
<div class="col-md-12">
<div class="error-template">
<h1>
{{ title | abpLocalization }}
</h1>
<h1>{{ statusText }} {{ title | abpLocalization }}</h1>
<div class="error-details">
{{ details | abpLocalization }}
</div>

60
npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.ts

@ -1,23 +1,65 @@
import { Component, Renderer2, ElementRef } from '@angular/core';
import { Config } from '@abp/ng.core';
import { Config, takeUntilDestroy } from '@abp/ng.core';
import {
AfterViewInit,
Component,
ComponentFactoryResolver,
ElementRef,
EmbeddedViewRef,
OnDestroy,
Type,
ViewChild,
} from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { debounceTime, filter } from 'rxjs/operators';
@Component({
selector: 'abp-error',
templateUrl: './error.component.html',
styleUrls: ['error.component.scss'],
})
export class ErrorComponent {
title: string | Config.LocalizationWithDefault = 'Oops!';
export class ErrorComponent implements AfterViewInit, OnDestroy {
cfRes: ComponentFactoryResolver;
details: string | Config.LocalizationWithDefault = 'Sorry, an error has occured.';
status = 0;
renderer: Renderer2;
title: Config.LocalizationParam = 'Oops!';
elementRef: ElementRef;
details: Config.LocalizationParam = 'Sorry, an error has occured.';
host: any;
customComponent: Type<any> = null;
destroy$: Subject<void>;
@ViewChild('container', { static: false })
containerRef: ElementRef<HTMLDivElement>;
get statusText(): string {
return this.status ? `[${this.status}]` : '';
}
ngAfterViewInit() {
if (this.customComponent) {
const customComponentRef = this.cfRes.resolveComponentFactory(this.customComponent).create(null);
customComponentRef.instance.errorStatus = this.status;
this.containerRef.nativeElement.appendChild((customComponentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]);
customComponentRef.changeDetectorRef.detectChanges();
}
fromEvent(document, 'keyup')
.pipe(
takeUntilDestroy(this),
debounceTime(150),
filter((key: KeyboardEvent) => key && key.key === 'Escape'),
)
.subscribe(() => {
this.destroy();
});
}
ngOnDestroy() {}
destroy() {
this.renderer.removeChild(this.host, this.elementRef.nativeElement);
this.destroy$.next();
this.destroy$.complete();
}
}

221
npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts

@ -1,21 +1,25 @@
import { RestOccurError } from '@abp/ng.core';
import { Config, RestOccurError } from '@abp/ng.core';
import { HttpErrorResponse } from '@angular/common/http';
import {
ApplicationRef,
ComponentFactoryResolver,
EmbeddedViewRef,
Inject,
Injectable,
Injector,
NgZone,
RendererFactory2,
Type,
ComponentRef,
} from '@angular/core';
import { Router } from '@angular/router';
import { Navigate, RouterError, RouterState, RouterDataResolved } from '@ngxs/router-plugin';
import { Actions, ofActionSuccessful, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { Observable, Subject } from 'rxjs';
import snq from 'snq';
import { ErrorComponent } from '../components/error/error.component';
import { HttpErrorConfig, ErrorScreenErrorCodes } from '../models/common';
import { Toaster } from '../models/toaster';
import { ConfirmationService } from '../services/confirmation.service';
import { HTTP_ERROR_CONFIG } from '../tokens/error-pages.token';
export const DEFAULT_ERROR_MESSAGES = {
defaultError: {
@ -36,82 +40,142 @@ export const DEFAULT_ERROR_MESSAGES = {
},
defaultError500: {
title: '500',
details: { key: 'AbpAccount::InternalServerErrorMessage', defaultValue: 'Error detail not sent by server.' },
},
defaultErrorUnknown: {
title: 'Unknown Error',
details: { key: 'AbpAccount::InternalServerErrorMessage', defaultValue: 'Error detail not sent by server.' },
details: 'Error detail not sent by server.',
},
};
@Injectable({ providedIn: 'root' })
export class ErrorHandler {
componentRef: ComponentRef<ErrorComponent>;
constructor(
private actions: Actions,
private router: Router,
private ngZone: NgZone,
private store: Store,
private confirmationService: ConfirmationService,
private appRef: ApplicationRef,
private cfRes: ComponentFactoryResolver,
private rendererFactory: RendererFactory2,
private injector: Injector,
@Inject(HTTP_ERROR_CONFIG) private httpErrorConfig: HttpErrorConfig,
) {
actions.pipe(ofActionSuccessful(RestOccurError)).subscribe(res => {
const { payload: err = {} as HttpErrorResponse | any } = res;
const body = snq(() => (err as HttpErrorResponse).error.error, DEFAULT_ERROR_MESSAGES.defaultError.title);
this.actions.pipe(ofActionSuccessful(RestOccurError, RouterError, RouterDataResolved)).subscribe(res => {
if (res instanceof RestOccurError) {
const { payload: err = {} as HttpErrorResponse | any } = res;
const body = snq(() => (err as HttpErrorResponse).error.error, DEFAULT_ERROR_MESSAGES.defaultError.title);
if (err instanceof HttpErrorResponse && err.headers.get('_AbpErrorFormat')) {
const confirmation$ = this.showError(null, null, body);
if (err instanceof HttpErrorResponse && err.headers.get('_AbpErrorFormat')) {
const confirmation$ = this.showError(null, null, body);
if (err.status === 401) {
confirmation$.subscribe(() => {
this.navigateToLogin();
});
}
} else {
switch ((err as HttpErrorResponse).status) {
case 401:
this.showError(
DEFAULT_ERROR_MESSAGES.defaultError401.details,
DEFAULT_ERROR_MESSAGES.defaultError401.title,
).subscribe(() => this.navigateToLogin());
break;
case 403:
this.createErrorComponent({
title: DEFAULT_ERROR_MESSAGES.defaultError403.title,
details: DEFAULT_ERROR_MESSAGES.defaultError403.details,
if (err.status === 401) {
confirmation$.subscribe(() => {
this.navigateToLogin();
});
break;
case 404:
this.showError(
DEFAULT_ERROR_MESSAGES.defaultError404.details,
DEFAULT_ERROR_MESSAGES.defaultError404.title,
);
break;
case 500:
this.createErrorComponent({
title: DEFAULT_ERROR_MESSAGES.defaultError500.title,
details: DEFAULT_ERROR_MESSAGES.defaultError500.details,
});
break;
case 0:
if ((err as HttpErrorResponse).statusText === 'Unknown Error') {
}
} else {
switch ((err as HttpErrorResponse).status) {
case 401:
this.canCreateCustomError(401)
? this.show401Page()
: this.showError(
{
key: 'AbpAccount::DefaultErrorMessage401',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title,
},
{
key: 'AbpAccount::DefaultErrorMessage401Detail',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.details,
},
).subscribe(() => this.navigateToLogin());
break;
case 403:
this.createErrorComponent({
title: {
key: 'AbpAccount::DefaultErrorMessage403',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.title,
},
details: {
key: 'AbpAccount::DefaultErrorMessage403Detail',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.details,
},
status: 403,
});
break;
case 404:
this.canCreateCustomError(404)
? this.show404Page()
: this.showError(
{
key: 'AbpAccount::DefaultErrorMessage404',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title,
},
{
key: 'AbpAccount::DefaultErrorMessage404Detail',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.details,
},
);
break;
case 500:
this.createErrorComponent({
title: DEFAULT_ERROR_MESSAGES.defaultErrorUnknown.title,
details: DEFAULT_ERROR_MESSAGES.defaultErrorUnknown.details,
title: {
key: 'AbpAccount::500Message',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.title,
},
details: {
key: 'AbpAccount::InternalServerErrorMessage',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.details,
},
status: 500,
});
}
break;
default:
this.showError(DEFAULT_ERROR_MESSAGES.defaultError.details, DEFAULT_ERROR_MESSAGES.defaultError.title);
break;
break;
case 0:
if ((err as HttpErrorResponse).statusText === 'Unknown Error') {
this.createErrorComponent({
title: {
key: 'AbpAccount::DefaultErrorMessage',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title,
},
});
}
break;
default:
this.showError(DEFAULT_ERROR_MESSAGES.defaultError.details, DEFAULT_ERROR_MESSAGES.defaultError.title);
break;
}
}
} else if (res instanceof RouterError && snq(() => res.event.error.indexOf('Cannot match') > -1, false)) {
this.show404Page();
} else if (res instanceof RouterDataResolved && this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
}
});
}
private showError(message?: string, title?: string, body?: any): Observable<Toaster.Status> {
private show401Page() {
this.createErrorComponent({
title: {
key: 'AbpAccount::401Message',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title,
},
status: 401,
});
}
private show404Page() {
this.createErrorComponent({
title: {
key: 'AbpAccount::404Message',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title,
},
status: 404,
});
}
private showError(
message?: Config.LocalizationParam,
title?: Config.LocalizationParam,
body?: any,
): Observable<Toaster.Status> {
if (body) {
if (body.details) {
message = body.details;
@ -123,35 +187,50 @@ export class ErrorHandler {
return this.confirmationService.error(message, title, {
hideCancelBtn: true,
yesCopy: 'OK',
yesText: 'AbpAccount::Close',
});
}
private navigateToLogin() {
this.ngZone.run(() => {
this.router.navigate(['/account/login'], {
state: { redirectUrl: this.router.url },
});
});
console.warn(this.store.selectSnapshot(RouterState.url));
this.store.dispatch(
new Navigate(['/account/login'], null, { state: { redirectUrl: this.store.selectSnapshot(RouterState.url) } }),
);
}
createErrorComponent(instance: Partial<ErrorComponent>) {
const renderer = this.rendererFactory.createRenderer(null, null);
const host = renderer.selectRootElement(document.body, true);
const componentRef = this.cfRes.resolveComponentFactory(ErrorComponent).create(this.injector);
this.componentRef = this.cfRes.resolveComponentFactory(ErrorComponent).create(this.injector);
for (const key in componentRef.instance) {
if (componentRef.instance.hasOwnProperty(key)) {
componentRef.instance[key] = instance[key];
for (const key in this.componentRef.instance) {
if (this.componentRef.instance.hasOwnProperty(key)) {
this.componentRef.instance[key] = instance[key];
}
}
this.appRef.attachView(componentRef.hostView);
renderer.appendChild(host, (componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]);
if (this.canCreateCustomError(instance.status as ErrorScreenErrorCodes)) {
this.componentRef.instance.cfRes = this.cfRes;
this.componentRef.instance.customComponent = this.httpErrorConfig.errorScreen.component;
}
this.appRef.attachView(this.componentRef.hostView);
renderer.appendChild(host, (this.componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]);
const destroy$ = new Subject<void>();
this.componentRef.instance.destroy$ = destroy$;
destroy$.subscribe(() => {
this.componentRef.destroy();
this.componentRef = null;
});
}
componentRef.instance.renderer = renderer;
componentRef.instance.elementRef = componentRef.location;
componentRef.instance.host = host;
canCreateCustomError(status: ErrorScreenErrorCodes): boolean {
return snq(
() =>
this.httpErrorConfig.errorScreen.component &&
this.httpErrorConfig.errorScreen.forWhichErrors.indexOf(status) > -1,
);
}
}

18
npm/ng-packs/packages/theme-shared/src/lib/models/common.ts

@ -0,0 +1,18 @@
import { Type } from '@angular/core';
export interface RootParams {
httpErrorConfig: HttpErrorConfig;
}
export type ErrorScreenErrorCodes = 401 | 403 | 404 | 500;
export interface HttpErrorConfig {
errorScreen?: {
component: Type<any>;
forWhichErrors?:
| [ErrorScreenErrorCodes]
| [ErrorScreenErrorCodes, ErrorScreenErrorCodes]
| [ErrorScreenErrorCodes, ErrorScreenErrorCodes, ErrorScreenErrorCodes]
| [ErrorScreenErrorCodes, ErrorScreenErrorCodes, ErrorScreenErrorCodes, ErrorScreenErrorCodes];
};
}

9
npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts

@ -1,18 +1,19 @@
import { Toaster } from './toaster';
import { Config } from '@abp/ng.core';
export namespace Confirmation {
export interface Options extends Toaster.Options {
hideCancelBtn?: boolean;
hideYesBtn?: boolean;
cancelText?: string;
yesText?: string;
cancelText?: Config.LocalizationParam;
yesText?: Config.LocalizationParam;
/**
* @deprecated to be deleted in v2
*/
cancelCopy?: string;
cancelCopy?: Config.LocalizationParam;
/**
* @deprecated to be deleted in v2
*/
yesCopy?: string;
yesCopy?: Config.LocalizationParam;
}
}

1
npm/ng-packs/packages/theme-shared/src/lib/models/index.ts

@ -1,3 +1,4 @@
export * from './common';
export * from './confirmation';
export * from './setting-management';
export * from './statistics';

10
npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts

@ -18,6 +18,8 @@ import styles from './contants/styles';
import { TableSortDirective } from './directives/table-sort.directive';
import { ErrorHandler } from './handlers/error.handler';
import { chartJsLoaded$ } from './utils/widget-utils';
import { RootParams } from './models/common';
import { HTTP_ERROR_CONFIG, httpErrorConfigFactory } from './tokens/error-pages.token';
export function appendScript(injector: Injector) {
const fn = () => {
@ -69,7 +71,7 @@ export function appendScript(injector: Injector) {
entryComponents: [ErrorComponent],
})
export class ThemeSharedModule {
static forRoot(): ModuleWithProviders {
static forRoot(options = {} as RootParams): ModuleWithProviders {
return {
ngModule: ThemeSharedModule,
providers: [
@ -80,6 +82,12 @@ export class ThemeSharedModule {
useFactory: appendScript,
},
{ provide: MessageService, useClass: MessageService },
{ provide: HTTP_ERROR_CONFIG, useValue: options.httpErrorConfig },
{
provide: 'HTTP_ERROR_CONFIG',
useFactory: httpErrorConfigFactory,
deps: [HTTP_ERROR_CONFIG],
},
],
};
}

15
npm/ng-packs/packages/theme-shared/src/lib/tokens/error-pages.token.ts

@ -0,0 +1,15 @@
import { InjectionToken } from '@angular/core';
import { HttpErrorConfig } from '../models/common';
export function httpErrorConfigFactory(config = {} as HttpErrorConfig) {
if (config.errorScreen && config.errorScreen.component && !config.errorScreen.forWhichErrors) {
config.errorScreen.forWhichErrors = [401, 403, 404, 500];
}
return {
errorScreen: {},
...config,
} as HttpErrorConfig;
}
export const HTTP_ERROR_CONFIG = new InjectionToken('HTTP_ERROR_CONFIG');

24
templates/app/angular/package.json

@ -18,23 +18,23 @@
"@abp/ng.setting-management": "^1.0.2",
"@abp/ng.tenant-management": "^1.0.2",
"@abp/ng.theme.basic": "^1.0.2",
"@angular/animations": "~8.2.12",
"@angular/common": "~8.2.12",
"@angular/compiler": "~8.2.12",
"@angular/core": "~8.2.12",
"@angular/forms": "~8.2.12",
"@angular/platform-browser": "~8.2.12",
"@angular/platform-browser-dynamic": "~8.2.12",
"@angular/router": "~8.2.12",
"@angular/animations": "~8.2.13",
"@angular/common": "~8.2.13",
"@angular/compiler": "~8.2.13",
"@angular/core": "~8.2.13",
"@angular/forms": "~8.2.13",
"@angular/platform-browser": "~8.2.13",
"@angular/platform-browser-dynamic": "~8.2.13",
"@angular/router": "~8.2.13",
"rxjs": "~6.4.0",
"tslib": "^1.10.0",
"zone.js": "~0.9.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.803.15",
"@angular/cli": "~8.3.15",
"@angular/compiler-cli": "~8.2.12",
"@angular/language-service": "~8.2.12",
"@angular-devkit/build-angular": "~0.803.17",
"@angular/cli": "~8.3.17",
"@angular/compiler-cli": "~8.2.13",
"@angular/language-service": "~8.2.13",
"@angularclass/hmr": "^2.1.3",
"@ngxs/hmr-plugin": "^3.5.1",
"@ngxs/logger-plugin": "^3.5.1",

Loading…
Cancel
Save