Merge remote-tracking branch 'abpframework/dev' into docs

pull/3708/head
liangshiwei 6 years ago
commit 48d030c1da

@ -52,7 +52,7 @@ class DemoComponent {
The `load` method returns an observable to which you can subscibe in your component or with an `async` pipe. In the example above, the `NgIf` directive will render `<some-component>` only **if the script gets successfully loaded or is already loaded before**.
> You can subscribe multiple times in your template with `async` pipe. The styles will only be loaded once.
> You can subscribe multiple times in your template with `async` pipe. The Scripts will only be loaded once.
Please refer to [LoadingStrategy](./Loading-Strategy.md) to see all available loading strategies and how you can build your own loading strategy.

@ -1,3 +1,179 @@
## HTTP请求
TODO...
## 关于 HttpClient
Angular具有很棒的 `HttpClient` 与后端服务进行通信. 它位于顶层,是[XMLHttpRequest Web API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)的封装. 同时也是Angular建议用于任何HTTP请求的代理,在你的ABP项目中使用 `HttpClient` 是最佳做法.
但是 `HttpClient` 将错误处理留给调用方,换句话说HTTP错误是通过手动处理的,通过挂接到返回的 `Observable` 的观察者中来处理.
```js
getConfig() {
this.http.get(this.configUrl).subscribe(
config => this.updateConfig(config),
error => {
// Handle error here
},
);
}
```
上面的代码尽管清晰灵活,但即使将错误处理委派给Store或任何其他注入. 以这种方式处理错误也是重复性的工作.
`HttpInterceptor` 能够捕获 `HttpErrorResponse` 并可用于集中的错误处理. 然而,在必须放置错误处理程序(也就是拦截器)的情况下,需要额外的工作以及对Angular内部机制的理解. 检查[这个issue](https://github.com/angular/angular/issues/20203)了解详情.
## RestService
ABP核心模块有用于HTTP请求的实用程序服务: `RestService`. 除非另有明确配置,否则它将捕获HTTP错误并调度 `RestOccurError` 操作, 然后由 `ThemeSharedModule` 引入的 `ErrorHandler` 捕获此操作. 你应该已经在应用程序中导入了此模块,在使用 `RestService` 时,默认情况下将自动处理所有HTTP错误.
### RestService 入门
为了使用 `RestService`, 你必须将它注入到你的类中.
```js
import { RestService } from '@abp/ng.core';
@Injectable({
/* class metadata here */
})
class DemoService {
constructor(private rest: RestService) {}
}
```
你不必在模块或组件/指令级别提供 `estService`,因为它已经在**根中**中提供了.
### 如何使用RestService发出请求
你可以使用 `RestService``request` 方法来处理HTTP请求. 示例:
```js
getFoo(id: number) {
const request: Rest.Request<null> = {
method: 'GET',
url: '/api/some/path/to/foo/' + id,
};
return this.rest.request<null, FooResponse>(request);
}
```
`request` 方法始终返回 `Observable<T>`. 无论何时使用 `getFoo` 方法,都可以执行以下操作:
```js
doSomethingWithFoo(id: number) {
this.demoService.getFoo(id).subscribe(
foo => {
// Do something with foo.
}
)
}
```
**你不必担心关于取消订阅**. `RestService` 在内部使用 `HttpClient`,因此它返回的每个可观察对象都是有限的可观察对象,成功或出错后将自动关闭订阅.
如你所见,`request` 方法获取一个具有 `Rest.Reques<T>` 类型的请求选项对象. 此泛型类型需要请求主体的接口. 当没有正文时,例如在 `GET``DELETE` 请求中,你可以传递 `null`. 示例:
```js
postFoo(body: Foo) {
const request: Rest.Request<Foo> = {
method: 'POST',
url: '/api/some/path/to/foo',
body
};
return this.rest.request<Foo, FooResponse>(request);
}
```
你可以在[此处检查](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23)完整的 `Rest.Request<T>` 类型,与Angular中的[HttpRequest](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23)类相比只有很少的改动.
### 如何禁用RestService的默认错误处理程序
默认 `request` 方法始终处理错误. 让我们看看如何改变这种行为并由自己处理错误:
```js
deleteFoo(id: number) {
const request: Rest.Request<null> = {
method: 'DELETE',
url: '/api/some/path/to/foo/' + id,
};
return this.rest.request<null, void>(request, { skipHandleError: true });
}
```
`skipHandleError` 配置选项设置为 `true` 时,禁用错误处理程序,并返回 `observable` 引发错误,你可以在订阅中捕获该错误.
```js
removeFooFromList(id: number) {
this.demoService.deleteFoo(id).subscribe(
foo => {
// Do something with foo.
},
error => {
// Do something with error.
}
)
}
```
### 如何从应用程序配置获取特定的API端点
`request` 方法接收到的另一个配置选项是 `apiName` (在v2.4中可用),它用于从应用程序配置获取特定的模块端点.
```js
putFoo(body: Foo, id: string) {
const request: Rest.Request<Foo> = {
method: 'PUT',
url: '/' + id,
body
};
return this.rest.request<Foo, void>(request, {apiName: 'foo'});
}
```
上面的putFoo将请求 `https://localhost:44305/api/some/path/to/foo/{id}` 当环境变量如下:
```js
// environment.ts
export const environment = {
apis: {
default: {
url: 'https://localhost:44305',
},
foo: {
url: 'https://localhost:44305/api/some/path/to/foo',
},
},
/* rest of the environment variables here */
}
```
### 如何观察响应对象或HTTP事件而不是正文
`RestService` 假定你通常对响应的正文感兴趣,默认情况下将 `observe` 属性设置为 `body`. 但是有时你可能对其他内容(例如自定义标头)非常感兴趣. 为此, `request` 方法在 `config` 对象中接收 `watch` 属性.
```js
getSomeCustomHeaderValue() {
const request: Rest.Request<null> = {
method: 'GET',
url: '/api/some/path/that/sends/some-custom-header',
};
return this.rest.request<null, HttpResponse<any>>(
request,
{observe: Rest.Observe.Response},
).pipe(
map(response => response.headers.get('Some-Custom-Header'))
);
}
```
你可以在[此处](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10)找到 `Rest.Observe` 枚举.
## 下一步是什么?
* [本地化](./Localization.md)

@ -1,3 +1,189 @@
# 如何懒加载 Scripts 与 Styles
TODO...
你可以使用@abp/ng.core包中的 `LazyLoadService` 以简单明了的方式延迟加载脚本和样式.
## 入门
你不必在模块或组件/指令级别提供 `LazyLoadService`,因为它已经在**根中**中提供了. 你可以在组件,指令或服务中注入并使用它.
```js
import { LazyLoadService } from '@abp/ng.core';
@Component({
/* class metadata here */
})
class DemoComponent {
constructor(private lazyLoadService: LazyLoadService) {}
}
```
## 用法
你可以使用 `LazyLoadService``load` 方法在DOM中的所需位置创建 `<script>``<link>` 元素并强制浏览器下载目标资源.
### 如何加载 Scripts
`load` 方法的第一个参数需要一个 `LoadingStrategy`. 如果传递 `ScriptLoadingStrategy` 实例,`LazyLoadService` 将使用给定的 `src` 创建一个 `<script>` 元素并放置在指定的DOM位置.
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
@Component({
template: `
<some-component *ngIf="libraryLoaded$ | async"></some-component>
`
})
class DemoComponent {
libraryLoaded$ = this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/some-library.js'),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
`load` 方法返回一个 `observable`,你可以在组件中或通过 `AsyncPipe` 订阅它. 在上面的示例中**仅当脚本成功加载或之前已经加载脚本时**, `NgIf` 指令才会呈现 `<some-component>`.
> 你可以使用 `async` 管道在模板中多次订阅,脚本将仅加载一次
请参阅[LoadingStrategy](./Loading-Strategy.md)查看所有可用的加载策略以及如何构建自己的加载策略.
### 如何加载 Styles
如果传递给 `load` 方法第一个参数为 `StyleLoadingStrategy` 实例,`LazyLoadService` 将使用给定的 `href` 创建一个 `<link>` 元素并放置在指定的DOM位置.
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
@Component({
template: `
<some-component *ngIf="stylesLoaded$ | async"></some-component>
`
})
class DemoComponent {
stylesLoaded$ = this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousStyleToHead('/assets/some-styles.css'),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
`load` 方法返回一个 `observable`,你可以在组件中或通过 `AsyncPipe` 订阅它. 在上面的示例中**仅当样式成功加载或之前已经加载样式时**, `NgIf` 指令才会呈现 `<some-component>`.
> 你可以使用 `async` 管道在模板中多次订阅,样式将仅加载一次
请参阅[LoadingStrategy](./Loading-Strategy.md)查看所有可用的加载策略以及如何构建自己的加载策略.
### 高级用法
你有**很大的自由度来定义延迟加载的工作方式**. 示例:
```js
const domStrategy = DOM_STRATEGY.PrependToHead();
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(
'sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh',
);
const loadingStrategy = new StyleLoadingStrategy(
'https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css',
domStrategy,
crossOriginStrategy,
);
this.lazyLoad.load(loadingStrategy, 1, 2000);
```
此代码将创建具有给定URL和完整性哈希的 `<link>` 元素,将其插入到 `<head>` 元素的顶部,如果第一次尝试失败,则在2秒后重试一次.
一个常见的用例是在**使用功能之前加载多个脚本/样式**:
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { frokJoin } from 'rxjs';
@Component({
template: `
<some-component *ngIf="scriptsAndStylesLoaded$ | async"></some-component>
`
})
class DemoComponent {
private stylesLoaded$ = forkJoin(
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library-dark-theme.css'),
),
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('/assets/library.css'),
),
);
private scriptsLoaded$ = forkJoin(
this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/library.js'),
),
this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/other-library.css'),
),
);
scriptsAndStylesLoaded$ = forkJoin(this.scriptsLoaded$, this.stylesLoaded$);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
RxJS `forkJoin` 并行加载所有脚本和样式,并仅在加载所有脚本和样式时才放行. 因此放置 `<some-component>` 时,所有必需的依赖项都可用的.
> 注意到我们在文档头上添加了样式吗? 有时这是必需的因为你的应用程序样式可能会覆盖某些库样式. 在这种情况下你必须注意前置样式的顺序. 它们将一一放置,**并且在放置前,最后一个放置在最上面**.
另一个常见的用例是**按顺序加载依赖脚本**:
```js
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { concat } from 'rxjs';
@Component({
template: `
<some-component *ngIf="scriptsLoaded$ | async"></some-component>
`
})
class DemoComponent {
scriptsLoaded$ = concat(
this.lazyLoad.load(
LOADING_STRATEGY.PrependAnonymousScriptToHead('/assets/library.js'),
),
this.lazyLoad.load(
LOADING_STRATEGY.AppendAnonymousScriptToHead('/assets/script-that-requires-library.js'),
),
);
constructor(private lazyLoadService: LazyLoadService) {}
}
```
在此示例中,第二个文件需要预先加载第一个文件, RxJS `concat` 函数将允许你以给定的顺序一个接一个地加载所有脚本,并且仅在加载所有脚本时放行.
## API
### loaded
```js
loaded: Set<string>
```
所有以前加载的路径都可以通过此属性访问. 它是一个简单的[JavaScript集]
### load
```js
load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable<Event>
```
- `strategy` 是主要参数,上面已经介绍过.
- `retryTimes` 定义加载失败前再次尝试多少次(默认值:2).
- `retryDelay` 定义重试之间的延迟(默认为1000).
## 下一步是什么?
- [DomInsertionService](./Dom-Insertion-Service.md)

@ -1,4 +1,4 @@
import { CommonModule } from '@angular/common';
import { APP_BASE_HREF, CommonModule } from '@angular/common';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { APP_INITIALIZER, Injector, ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@ -22,8 +22,9 @@ import { ReplaceableTemplateDirective } from './directives/replaceable-template.
import { StopPropagationDirective } from './directives/stop-propagation.directive';
import { VisibilityDirective } from './directives/visibility.directive';
import { ApiInterceptor } from './interceptors/api.interceptor';
import { LocalizationModule } from './localization.module';
import { ABP } from './models/common';
import { LocalizationPipe } from './pipes/localization.pipe';
import { LocalizationPipe, MockLocalizationPipe } from './pipes/localization.pipe';
import { SortPipe } from './pipes/sort.pipe';
import { ConfigPlugin, NGXS_CONFIG_PLUGIN_OPTIONS } from './plugins/config.plugin';
import { LocaleProvider } from './providers/locale.provider';
@ -32,77 +33,128 @@ import { ProfileState } from './states/profile.state';
import { ReplaceableComponentsState } from './states/replaceable-components.state';
import { SessionState } from './states/session.state';
import { CORE_OPTIONS } from './tokens/options.token';
import { getInitialData, localeInitializer } from './utils/initial-utils';
import './utils/date-extensions';
import { getInitialData, localeInitializer } from './utils/initial-utils';
export function storageFactory(): OAuthStorage {
return localStorage;
}
/**
* BaseCoreModule is the module that holds
* all imports, declarations, exports, and entryComponents
* but not the providers.
* This module will be imported and exported by all others.
*/
@NgModule({
imports: [
NgxsModule.forFeature([ReplaceableComponentsState, ProfileState, SessionState, ConfigState]),
NgxsRouterPluginModule.forRoot(),
NgxsStoragePluginModule.forRoot({ key: ['SessionState'] }),
OAuthModule,
exports: [
CommonModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
RouterModule,
],
declarations: [
ReplaceableRouteContainerComponent,
RouterOutletComponent,
DynamicLayoutComponent,
AbstractNgModelComponent,
AutofocusDirective,
DynamicLayoutComponent,
EllipsisDirective,
ForDirective,
FormSubmitDirective,
LocalizationPipe,
SortPipe,
InitDirective,
PermissionDirective,
VisibilityDirective,
InputEventDebounceDirective,
StopPropagationDirective,
PermissionDirective,
ReplaceableRouteContainerComponent,
ReplaceableTemplateDirective,
AbstractNgModelComponent,
RouterOutletComponent,
SortPipe,
StopPropagationDirective,
VisibilityDirective,
],
exports: [
imports: [
OAuthModule,
CommonModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
RouterModule,
RouterOutletComponent,
DynamicLayoutComponent,
],
declarations: [
AbstractNgModelComponent,
ReplaceableRouteContainerComponent,
AutofocusDirective,
DynamicLayoutComponent,
EllipsisDirective,
ForDirective,
FormSubmitDirective,
InitDirective,
PermissionDirective,
VisibilityDirective,
InputEventDebounceDirective,
PermissionDirective,
ReplaceableRouteContainerComponent,
ReplaceableTemplateDirective,
StopPropagationDirective,
LocalizationPipe,
RouterOutletComponent,
SortPipe,
LocalizationPipe,
StopPropagationDirective,
VisibilityDirective,
],
providers: [LocalizationPipe],
entryComponents: [
RouterOutletComponent,
DynamicLayoutComponent,
ReplaceableRouteContainerComponent,
],
})
export class BaseCoreModule {}
/**
* RootCoreModule is the module that will be used at root level
* and it introduces imports useful at root level (e.g. NGXS)
*/
@NgModule({
exports: [BaseCoreModule, LocalizationModule],
imports: [
BaseCoreModule,
LocalizationModule,
NgxsModule.forFeature([ReplaceableComponentsState, ProfileState, SessionState, ConfigState]),
NgxsRouterPluginModule.forRoot(),
NgxsStoragePluginModule.forRoot({ key: ['SessionState'] }),
],
})
export class RootCoreModule {}
/**
* TestCoreModule is the module that will be used in tests
* and it provides mock alternatives
*/
@NgModule({
exports: [RouterModule, BaseCoreModule, MockLocalizationPipe],
imports: [RouterModule.forRoot([]), BaseCoreModule],
declarations: [MockLocalizationPipe],
})
export class TestCoreModule {}
/**
* CoreModule is the module that is publicly available
*/
@NgModule({
exports: [BaseCoreModule, LocalizationModule],
imports: [BaseCoreModule, LocalizationModule],
providers: [LocalizationPipe],
})
export class CoreModule {
static forRoot(options = {} as ABP.Root): ModuleWithProviders {
static forTest({ baseHref = '/' } = {} as ABP.Test): ModuleWithProviders<TestCoreModule> {
return {
ngModule: TestCoreModule,
providers: [
{ provide: APP_BASE_HREF, useValue: baseHref },
{
provide: LocalizationPipe,
useClass: MockLocalizationPipe,
},
],
};
}
static forRoot(options = {} as ABP.Root): ModuleWithProviders<RootCoreModule> {
return {
ngModule: CoreModule,
ngModule: RootCoreModule,
providers: [
LocaleProvider,
{

@ -0,0 +1,8 @@
import { NgModule } from '@angular/core';
import { LocalizationPipe } from './pipes/localization.pipe';
@NgModule({
exports: [LocalizationPipe],
declarations: [LocalizationPipe],
})
export class LocalizationModule {}

@ -1,4 +1,5 @@
import { EventEmitter } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
import { eLayoutType } from '../enums/common';
import { Config } from './config';
@ -14,6 +15,10 @@ export namespace ABP {
skipGetAppConfiguration?: boolean;
}
export interface Test {
baseHref?: Router;
}
export type PagedResponse<T> = {
totalCount: number;
} & PagedItemsResponse<T>;

@ -1,4 +1,4 @@
import { Pipe, PipeTransform, Injectable } from '@angular/core';
import { Injectable, Pipe, PipeTransform } from '@angular/core';
import { Store } from '@ngxs/store';
import { Config } from '../models';
import { ConfigState } from '../states';
@ -10,12 +10,28 @@ import { ConfigState } from '../states';
export class LocalizationPipe implements PipeTransform {
constructor(private store: Store) {}
transform(value: string | Config.LocalizationWithDefault = '', ...interpolateParams: string[]): string {
transform(
value: string | Config.LocalizationWithDefault = '',
...interpolateParams: string[]
): string {
return this.store.selectSnapshot(
ConfigState.getLocalization(
value,
...interpolateParams.reduce((acc, val) => (Array.isArray(val) ? [...acc, ...val] : [...acc, val]), []),
...interpolateParams.reduce(
(acc, val) => (Array.isArray(val) ? [...acc, ...val] : [...acc, val]),
[],
),
),
);
}
}
@Injectable()
@Pipe({
name: 'abpLocalization',
})
export class MockLocalizationPipe implements PipeTransform {
transform(value: string | Config.LocalizationWithDefault = '', ..._: string[]) {
return typeof value === 'string' ? value : value.defaultValue;
}
}

@ -1,16 +1,15 @@
import { RouterTestingModule } from '@angular/router/testing';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { NgxsModule, NGXS_PLUGINS, Store } from '@ngxs/store';
import { NgxsModule, Store } from '@ngxs/store';
import { OAuthModule } from 'angular-oauth2-oidc';
import { environment } from '../../../../../apps/dev-app/src/environments/environment';
import { LAYOUTS } from '@abp/ng.theme.basic';
import { RouterOutletComponent } from '../components';
import { CoreModule } from '../core.module';
import { eLayoutType } from '../enums/common';
import { ABP } from '../models';
import { ConfigPlugin, NGXS_CONFIG_PLUGIN_OPTIONS } from '../plugins';
import { ConfigPlugin } from '../plugins';
import { ConfigState } from '../states';
import { addAbpRoutes } from '../utils';
import { OAuthModule } from 'angular-oauth2-oidc';
addAbpRoutes([
{
@ -60,9 +59,6 @@ addAbpRoutes([
const expectedState = {
environment,
requirements: {
layouts: LAYOUTS,
},
routes: [
{
name: '::Menu:Home',
@ -323,9 +319,9 @@ describe('ConfigPlugin', () => {
const createService = createServiceFactory({
service: ConfigPlugin,
imports: [
CoreModule,
NgxsModule.forRoot([ConfigState]),
CoreModule.forRoot({ environment }),
OAuthModule.forRoot(),
NgxsModule.forRoot([]),
RouterTestingModule.withRoutes([
{
path: '',
@ -341,17 +337,6 @@ describe('ConfigPlugin', () => {
{ path: 'tenant-management', component: RouterOutletComponent },
]),
],
providers: [
{
provide: NGXS_PLUGINS,
useClass: ConfigPlugin,
multi: true,
},
{
provide: NGXS_CONFIG_PLUGIN_OPTIONS,
useValue: { environment, requirements: { layouts: LAYOUTS } } as ABP.Root,
},
],
});
beforeEach(() => {

@ -7,7 +7,7 @@ export * from './lib/abstracts';
export * from './lib/actions';
export * from './lib/components';
export * from './lib/constants';
export * from './lib/core.module';
export { CoreModule } from './lib/core.module';
export * from './lib/directives';
export * from './lib/enums';
export * from './lib/guards';

@ -1,7 +1,6 @@
import { Component } from '@angular/core';
import { ConfirmationService } from '../../services/confirmation.service';
import { Confirmation } from '../../models/confirmation';
import { LocalizationService } from '@abp/ng.core';
import { ConfirmationService } from '../../services/confirmation.service';
@Component({
selector: 'abp-confirmation',
@ -32,10 +31,7 @@ export class ConfirmationComponent {
}
}
constructor(
private confirmationService: ConfirmationService,
private localizationService: LocalizationService,
) {
constructor(private confirmationService: ConfirmationService) {
this.confirmationService.confirmation$.subscribe(confirmation => {
this.data = confirmation;
this.visible = !!confirmation;

@ -3,23 +3,22 @@ import { HttpErrorResponse } from '@angular/common/http';
import {
ApplicationRef,
ComponentFactoryResolver,
ComponentRef,
EmbeddedViewRef,
Inject,
Injectable,
Injector,
RendererFactory2,
Type,
ComponentRef,
} from '@angular/core';
import { Navigate, RouterError, RouterState, RouterDataResolved } from '@ngxs/router-plugin';
import { Navigate, RouterDataResolved, RouterError, RouterState } from '@ngxs/router-plugin';
import { Actions, ofActionSuccessful, Store } from '@ngxs/store';
import { Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import snq from 'snq';
import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component';
import { HttpErrorConfig, ErrorScreenErrorCodes } from '../models/common';
import { ErrorScreenErrorCodes, HttpErrorConfig } from '../models/common';
import { Confirmation } from '../models/confirmation';
import { ConfirmationService } from '../services/confirmation.service';
import { filter, tap } from 'rxjs/operators';
export const DEFAULT_ERROR_MESSAGES = {
defaultError: {
@ -58,7 +57,6 @@ export class ErrorHandler {
private injector: Injector,
@Inject('HTTP_ERROR_CONFIG') private httpErrorConfig: HttpErrorConfig,
) {
this.httpErrorConfig.skipHandledErrorCodes = this.httpErrorConfig.skipHandledErrorCodes || [];
this.listenToRestError();
this.listenToRouterError();
this.listenToRouterDataResolved();
@ -66,7 +64,7 @@ export class ErrorHandler {
private listenToRouterError() {
this.actions
.pipe(ofActionSuccessful(RouterError), filter(this.filterRouteErrors), tap(console.warn))
.pipe(ofActionSuccessful(RouterError), filter(this.filterRouteErrors))
.subscribe(() => this.show404Page());
}
@ -84,14 +82,15 @@ export class ErrorHandler {
private listenToRestError() {
this.actions
.pipe(ofActionSuccessful(RestOccurError), filter(this.filterRestErrors))
.subscribe(({ payload: { err = {} as HttpErrorResponse } }) => {
const body = snq(
() => (err as HttpErrorResponse).error.error,
DEFAULT_ERROR_MESSAGES.defaultError.title,
);
.pipe(
ofActionSuccessful(RestOccurError),
map(action => action.payload),
filter(this.filterRestErrors),
)
.subscribe(err => {
const body = snq(() => err.error.error, DEFAULT_ERROR_MESSAGES.defaultError.title);
if (err instanceof HttpErrorResponse && err.headers.get('_AbpErrorFormat')) {
if (err.headers.get('_AbpErrorFormat')) {
const confirmation$ = this.showError(null, null, body);
if (err.status === 401) {
@ -100,7 +99,7 @@ export class ErrorHandler {
});
}
} else {
switch ((err as HttpErrorResponse).status) {
switch (err.status) {
case 401:
this.canCreateCustomError(401)
? this.show401Page()
@ -156,7 +155,7 @@ export class ErrorHandler {
});
break;
case 0:
if ((err as HttpErrorResponse).statusText === 'Unknown Error') {
if (err.statusText === 'Unknown Error') {
this.createErrorComponent({
title: {
key: 'AbpAccount::DefaultErrorMessage',
@ -238,6 +237,7 @@ export class ErrorHandler {
.create(this.injector);
for (const key in instance) {
/* istanbul ignore else */
if (this.componentRef.instance.hasOwnProperty(key)) {
this.componentRef.instance[key] = instance[key];
}
@ -270,11 +270,8 @@ export class ErrorHandler {
);
}
private filterRestErrors = (instance: RestOccurError): boolean => {
const {
payload: { err: { status } = {} as HttpErrorResponse },
} = instance;
if (!status) return false;
private filterRestErrors = ({ status }: HttpErrorResponse): boolean => {
if (typeof status !== 'number') return false;
return this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === status) < 0;
};

@ -37,7 +37,7 @@ export class ToasterService {
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) {
): number {
return this.show(message, title, 'info', options);
}
@ -51,7 +51,7 @@ export class ToasterService {
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) {
): number {
return this.show(message, title, 'success', options);
}
@ -65,7 +65,7 @@ export class ToasterService {
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) {
): number {
return this.show(message, title, 'warning', options);
}
@ -79,7 +79,7 @@ export class ToasterService {
message: Config.LocalizationParam,
title?: Config.LocalizationParam,
options?: Partial<Toaster.ToastOptions>,
) {
): number {
return this.show(message, title, 'error', options);
}
@ -96,7 +96,7 @@ export class ToasterService {
title: Config.LocalizationParam = null,
severity: Toaster.Severity = 'neutral',
options = {} as Partial<Toaster.ToastOptions>,
) {
): number {
if (!this.containerComponentRef) this.setContainer();
const id = ++this.lastId;
@ -114,7 +114,7 @@ export class ToasterService {
* Removes the toast with given id.
* @param id ID of the toast to be removed.
*/
remove(id: number) {
remove(id: number): void {
this.toasts = this.toasts.filter(toast => snq(() => toast.options.id) !== id);
this.toasts$.next(this.toasts);
}
@ -122,7 +122,7 @@ export class ToasterService {
/**
* Removes all open toasts at once.
*/
clear(key?: string) {
clear(key?: string): void {
this.toasts = !key
? []
: this.toasts.filter(toast => snq(() => toast.options.containerKey) !== key);

@ -1,74 +1,101 @@
import { CoreModule } from '@abp/ng.core';
import { Component } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { NgModule } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { NgxsModule } from '@ngxs/store';
import { ConfirmationService } from '../services/confirmation.service';
import { ThemeSharedModule } from '../theme-shared.module';
import { OAuthModule, OAuthService } from 'angular-oauth2-oidc';
@Component({
selector: 'abp-dummy',
template: `
<abp-confirmation></abp-confirmation>
`,
import { timer } from 'rxjs';
import { take } from 'rxjs/operators';
import { ConfirmationComponent } from '../components';
import { Confirmation } from '../models';
import { ConfirmationService } from '../services';
@NgModule({
exports: [ConfirmationComponent],
entryComponents: [ConfirmationComponent],
declarations: [ConfirmationComponent],
imports: [CoreModule.forTest()],
})
class DummyComponent {
constructor(public confirmation: ConfirmationService) {}
}
export class MockModule {}
describe('ConfirmationService', () => {
let spectator: Spectator<DummyComponent>;
let spectator: SpectatorService<ConfirmationService>;
let service: ConfirmationService;
const createComponent = createComponentFactory({
component: DummyComponent,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule],
mocks: [OAuthService],
const createService = createServiceFactory({
service: ConfirmationService,
imports: [NgxsModule.forRoot(), CoreModule.forTest(), MockModule],
});
beforeEach(() => {
spectator = createComponent();
service = spectator.get(ConfirmationService);
spectator = createService();
service = spectator.service;
});
afterEach(() => {
clearElements();
});
test.skip('should display a confirmation popup', () => {
service.info('test', 'title');
test('should display a confirmation popup', fakeAsync(() => {
service.show('MESSAGE', 'TITLE');
spectator.detectChanges();
tick();
expect(spectator.query('div.confirmation .title')).toHaveText('title');
expect(spectator.query('div.confirmation .message')).toHaveText('test');
expect(selectConfirmationContent('.title')).toBe('TITLE');
expect(selectConfirmationContent('.message')).toBe('MESSAGE');
}));
test.each`
type | selector | icon
${'info'} | ${'.info'} | ${'.fa-info-circle'}
${'success'} | ${'.success'} | ${'.fa-check-circle'}
${'warn'} | ${'.warning'} | ${'.fa-exclamation-triangle'}
${'error'} | ${'.error'} | ${'.fa-times-circle'}
`('should display $type confirmation popup', async ({ type, selector, icon }) => {
service[type]('MESSAGE', 'TITLE');
await timer(0).toPromise();
expect(selectConfirmationContent('.title')).toBe('TITLE');
expect(selectConfirmationContent('.message')).toBe('MESSAGE');
expect(selectConfirmationElement(selector)).toBeTruthy();
expect(selectConfirmationElement(icon)).toBeTruthy();
});
test.skip('should close with ESC key', done => {
service.info('test', 'title').subscribe(() => {
setTimeout(() => {
spectator.detectComponentChanges();
expect(spectator.query('div.confirmation')).toBeFalsy();
test('should close with ESC key', done => {
service
.info('', '')
.pipe(take(1))
.subscribe(status => {
expect(status).toBe(Confirmation.Status.dismiss);
done();
}, 0);
});
});
spectator.detectChanges();
expect(spectator.query('div.confirmation')).toBeTruthy();
spectator.dispatchKeyboardEvent('div.confirmation', 'keyup', 'Escape');
const escape = new KeyboardEvent('keyup', { key: 'Escape' });
document.dispatchEvent(escape);
});
test.skip('should close when click cancel button', done => {
service.info('test', 'title', { yesText: 'Sure', cancelText: 'Exit' }).subscribe(() => {
spectator.detectComponentChanges();
setTimeout(() => {
expect(spectator.query('div.confirmation')).toBeFalsy();
done();
}, 0);
test('should close when click cancel button', async done => {
service.info('', '', { yesText: 'Sure', cancelText: 'Exit' }).subscribe(status => {
expect(status).toBe(Confirmation.Status.reject);
done();
});
spectator.detectChanges();
await timer(0).toPromise();
expect(spectator.query('div.confirmation')).toBeTruthy();
expect(spectator.query('button#cancel')).toHaveText('Exit');
expect(spectator.query('button#confirm')).toHaveText('Sure');
expect(selectConfirmationContent('button#cancel')).toBe('Exit');
expect(selectConfirmationContent('button#confirm')).toBe('Sure');
spectator.click('button#cancel');
selectConfirmationElement<HTMLButtonElement>('button#cancel').click();
});
});
function clearElements(selector = '.confirmation') {
document.querySelectorAll(selector).forEach(element => element.parentNode.removeChild(element));
}
function selectConfirmationContent(selector = '.confirmation'): string {
return selectConfirmationElement(selector).textContent.trim();
}
function selectConfirmationElement<T extends HTMLElement>(selector = '.confirmation'): T {
return document.querySelector(selector);
}

@ -1,121 +1,219 @@
import { CoreModule, RestOccurError, RouterOutletComponent } from '@abp/ng.core';
import { Location } from '@angular/common';
import { CoreModule, RestOccurError } from '@abp/ng.core';
import { APP_BASE_HREF } from '@angular/common';
import { HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Component, NgModule } from '@angular/core';
import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest';
import { NgxsModule, Store } from '@ngxs/store';
import { NavigationError, ResolveEnd, RouterModule } from '@angular/router';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { Navigate, RouterDataResolved, RouterError } from '@ngxs/router-plugin';
import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store';
import { OAuthService } from 'angular-oauth2-oidc';
import { of } from 'rxjs';
import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component';
import { DEFAULT_ERROR_MESSAGES, ErrorHandler } from '../handlers';
import { ThemeSharedModule } from '../theme-shared.module';
import { RouterError, RouterDataResolved } from '@ngxs/router-plugin';
import { NavigationError, ResolveEnd } from '@angular/router';
import { OAuthModule, OAuthService } from 'angular-oauth2-oidc';
import { ConfirmationService } from '../services';
import { httpErrorConfigFactory } from '../tokens/http-error.token';
@Component({
selector: 'abp-dummy',
template: 'dummy works! <abp-confirmation></abp-confirmation>',
@NgModule({
exports: [HttpErrorWrapperComponent],
declarations: [HttpErrorWrapperComponent],
entryComponents: [HttpErrorWrapperComponent],
imports: [CoreModule],
})
class DummyComponent {
constructor(public errorHandler: ErrorHandler) {}
}
class MockModule {}
let spectator: SpectatorRouting<DummyComponent>;
let spectator: SpectatorService<ErrorHandler>;
let service: ErrorHandler;
let store: Store;
const errorConfirmation: jest.Mock = jest.fn(() => of(null));
const CONFIRMATION_BUTTONS = {
hideCancelBtn: true,
yesText: 'AbpAccount::Close',
};
describe('ErrorHandler', () => {
const createComponent = createRoutingFactory({
component: DummyComponent,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot([])],
const createService = createServiceFactory({
service: ErrorHandler,
imports: [RouterModule.forRoot([]), NgxsModule.forRoot([]), CoreModule, MockModule],
mocks: [OAuthService],
stubsEnabled: false,
routes: [
{ path: '', component: DummyComponent },
{ path: 'account/login', component: RouterOutletComponent },
providers: [
{ provide: APP_BASE_HREF, useValue: '/' },
{
provide: 'HTTP_ERROR_CONFIG',
useFactory: httpErrorConfigFactory,
},
{
provide: ConfirmationService,
useValue: {
error: errorConfirmation,
},
},
],
});
beforeEach(() => {
spectator = createComponent();
spectator = createService();
service = spectator.service;
store = spectator.get(Store);
store.selectSnapshot = jest.fn(() => '/x');
});
const abpError = document.querySelector('abp-http-error-wrapper');
if (abpError) document.body.removeChild(abpError);
afterEach(() => {
errorConfirmation.mockClear();
removeIfExistsInDom(selectHtmlErrorWrapper);
});
test.skip('should display the error component when server error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 })));
expect(document.querySelector('.error-template')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError500.title,
);
expect(document.querySelector('.error-details')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError500.details,
);
test('should display HttpErrorWrapperComponent when server error occurs', () => {
const createComponent = jest.spyOn(service, 'createErrorComponent');
const error = new HttpErrorResponse({ status: 500 });
const params = {
title: {
key: 'AbpAccount::500Message',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.title,
},
details: {
key: 'AbpAccount::InternalServerErrorMessage',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.details,
},
status: 500,
};
expect(selectHtmlErrorWrapper()).toBeNull();
store.dispatch(new RestOccurError(error));
expect(createComponent).toHaveBeenCalledWith(params);
const wrapper = service.componentRef.instance;
expect(wrapper.title).toEqual(params.title);
expect(wrapper.details).toEqual(params.details);
expect(wrapper.status).toBe(params.status);
expect(selectHtmlErrorWrapper()).not.toBeNull();
});
test.skip('should display the error component when authorize error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 403 })));
expect(document.querySelector('.error-template')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError403.title,
);
expect(document.querySelector('.error-details')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError403.details,
);
test('should display HttpErrorWrapperComponent when authorize error occurs', () => {
const createComponent = jest.spyOn(service, 'createErrorComponent');
const error = new HttpErrorResponse({ status: 403 });
const params = {
title: {
key: 'AbpAccount::DefaultErrorMessage403',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.title,
},
details: {
key: 'AbpAccount::DefaultErrorMessage403Detail',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.details,
},
status: 403,
};
expect(selectHtmlErrorWrapper()).toBeNull();
store.dispatch(new RestOccurError(error));
expect(createComponent).toHaveBeenCalledWith(params);
const wrapper = service.componentRef.instance;
expect(wrapper.title).toEqual(params.title);
expect(wrapper.details).toEqual(params.details);
expect(wrapper.status).toBe(params.status);
expect(selectHtmlErrorWrapper()).not.toBeNull();
});
test.skip('should display the error component when unknown error occurs', () => {
store.dispatch(
new RestOccurError(new HttpErrorResponse({ status: 0, statusText: 'Unknown Error' })),
);
expect(document.querySelector('.error-template')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError.title,
);
test('should display HttpErrorWrapperComponent when unknown error occurs', () => {
const createComponent = jest.spyOn(service, 'createErrorComponent');
const error = new HttpErrorResponse({ status: 0, statusText: 'Unknown Error' });
const params = {
title: {
key: 'AbpAccount::DefaultErrorMessage',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title,
},
details: error.message,
isHomeShow: false,
};
expect(selectHtmlErrorWrapper()).toBeNull();
store.dispatch(new RestOccurError(error));
expect(createComponent).toHaveBeenCalledWith(params);
const wrapper = service.componentRef.instance;
expect(wrapper.title).toEqual(params.title);
expect(wrapper.details).toEqual(params.details);
expect(wrapper.isHomeShow).toBe(params.isHomeShow);
expect(selectHtmlErrorWrapper()).not.toBeNull();
});
test.skip('should display the confirmation when not found error occurs', () => {
test('should call error method of ConfirmationService when not found error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 404 })));
spectator.detectChanges();
expect(spectator.query('.confirmation .title')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError404.title,
);
expect(spectator.query('.confirmation .message')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError404.details,
expect(errorConfirmation).toHaveBeenCalledWith(
{
key: 'AbpAccount::DefaultErrorMessage404',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.details,
},
{
key: 'AbpAccount::DefaultErrorMessage404Detail',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title,
},
CONFIRMATION_BUTTONS,
);
});
test.skip('should display the confirmation when default error occurs', () => {
test('should call error method of ConfirmationService when default error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 412 })));
spectator.detectChanges();
expect(spectator.query('.confirmation .title')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError.title,
);
expect(spectator.query('.confirmation .message')).toHaveText(
expect(errorConfirmation).toHaveBeenCalledWith(
DEFAULT_ERROR_MESSAGES.defaultError.details,
DEFAULT_ERROR_MESSAGES.defaultError.title,
CONFIRMATION_BUTTONS,
);
});
test.skip('should display the confirmation when authenticated error occurs', async () => {
test('should call error method of ConfirmationService when authenticated error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401 })));
spectator.detectChanges();
spectator.click('#confirm');
await spectator.fixture.whenStable();
expect(spectator.get(Location).path()).toBe('/account/login');
expect(errorConfirmation).toHaveBeenCalledWith(
{
key: 'AbpAccount::DefaultErrorMessage401',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title,
},
{
key: 'AbpAccount::DefaultErrorMessage401Detail',
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.details,
},
CONFIRMATION_BUTTONS,
);
});
test.skip('should display the confirmation when authenticated error occurs with _AbpErrorFormat header', async () => {
let headers: HttpHeaders = new HttpHeaders();
headers = headers.append('_AbpErrorFormat', '_AbpErrorFormat');
test('should call error method of ConfirmationService when authenticated error occurs with _AbpErrorFormat header', done => {
spectator
.get(Actions)
.pipe(ofActionDispatched(Navigate))
.subscribe(({ path, queryParams, extras }) => {
expect(path).toEqual(['/account/login']);
expect(queryParams).toBeNull();
expect(extras).toEqual({ state: { redirectUrl: '/x' } });
done();
});
const headers: HttpHeaders = new HttpHeaders({
_AbpErrorFormat: '_AbpErrorFormat',
});
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401, headers })));
spectator.detectChanges();
spectator.click('#confirm');
await spectator.fixture.whenStable();
expect(spectator.get(Location).path()).toBe('/account/login');
expect(errorConfirmation).toHaveBeenCalledWith(
DEFAULT_ERROR_MESSAGES.defaultError.title,
null,
CONFIRMATION_BUTTONS,
);
});
test.skip('should display the confirmation when error occurs with _AbpErrorFormat header', () => {
test('should call error method of ConfirmationService when error occurs with _AbpErrorFormat header', () => {
let headers: HttpHeaders = new HttpHeaders();
headers = headers.append('_AbpErrorFormat', '_AbpErrorFormat');
store.dispatch(
new RestOccurError(
new HttpErrorResponse({
@ -125,17 +223,28 @@ describe('ErrorHandler', () => {
}),
),
);
spectator.detectChanges();
expect(spectator.query('.title')).toHaveText('test message');
expect(spectator.query('.confirmation .message')).toHaveText('test detail');
expect(errorConfirmation).toHaveBeenCalledWith(
'test detail',
'test message',
CONFIRMATION_BUTTONS,
);
});
test('should call destroy method of componentRef when ResolveEnd is dispatched', () => {
store.dispatch(new RouterError(null, null, new NavigationError(1, 'test', 'Cannot match')));
const destroyComponent = jest.spyOn(service.componentRef, 'destroy');
store.dispatch(new RouterDataResolved(null, new ResolveEnd(1, 'test', 'test', null)));
expect(destroyComponent).toHaveBeenCalledTimes(1);
});
});
@Component({
selector: 'abp-dummy-error',
template:
'<p>{{errorStatus}}</p><button id="close-dummy" (click)="destroy$.next()">Close</button>',
template: '<p>{{errorStatus}}</p>',
})
class DummyErrorComponent {
errorStatus;
@ -150,68 +259,103 @@ class DummyErrorComponent {
class ErrorModule {}
describe('ErrorHandler with custom error component', () => {
const createComponent = createRoutingFactory({
component: DummyComponent,
const createService = createServiceFactory({
service: ErrorHandler,
imports: [
CoreModule,
ThemeSharedModule.forRoot({
httpErrorConfig: {
errorScreen: { component: DummyErrorComponent, forWhichErrors: [401, 403, 404, 500] },
},
}),
RouterModule.forRoot([]),
NgxsModule.forRoot([]),
CoreModule,
MockModule,
ErrorModule,
],
mocks: [OAuthService],
stubsEnabled: false,
routes: [
{ path: '', component: DummyComponent },
{ path: 'account/login', component: RouterOutletComponent },
mocks: [OAuthService, ConfirmationService],
providers: [
{ provide: APP_BASE_HREF, useValue: '/' },
{
provide: 'HTTP_ERROR_CONFIG',
useFactory: customHttpErrorConfigFactory,
},
],
});
beforeEach(() => {
spectator = createComponent();
spectator = createService();
service = spectator.service;
store = spectator.get(Store);
store.selectSnapshot = jest.fn(() => '/x');
});
const abpError = document.querySelector('abp-http-error-wrapper');
if (abpError) document.body.removeChild(abpError);
afterEach(() => {
removeIfExistsInDom(selectCustomError);
});
describe('Custom error component', () => {
test.skip('should create when occur 401', () => {
test('should be created when 401 error is dispatched', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401 })));
expect(document.querySelector('abp-dummy-error')).toBeTruthy();
expect(document.querySelector('p')).toHaveExactText('401');
expect(selectCustomErrorText()).toBe('401');
});
test.skip('should create when occur 403', () => {
test('should be created when 403 error is dispatched', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 403 })));
expect(document.querySelector('p')).toHaveExactText('403');
expect(selectCustomErrorText()).toBe('403');
});
test.skip('should create when occur 404', () => {
test('should be created when 404 error is dispatched', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 404 })));
expect(document.querySelector('p')).toHaveExactText('404');
expect(selectCustomErrorText()).toBe('404');
});
test.skip('should create when dispatched the RouterError', () => {
test('should be created when RouterError is dispatched', () => {
store.dispatch(new RouterError(null, null, new NavigationError(1, 'test', 'Cannot match')));
expect(document.querySelector('p')).toHaveExactText('404');
store.dispatch(new RouterDataResolved(null, new ResolveEnd(1, 'test', 'test', null)));
expect(selectCustomErrorText()).toBe('404');
});
test.skip('should create when occur 500', () => {
test('should be created when 500 error is dispatched', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 })));
expect(document.querySelector('p')).toHaveExactText('500');
expect(selectCustomErrorText()).toBe('500');
});
test.skip('should be destroyed when click the close button', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 })));
document.querySelector<HTMLButtonElement>('#close-dummy').click();
spectator.detectChanges();
expect(document.querySelector('abp-dummy-error')).toBeFalsy();
test('should call destroy method of componentRef when destroy$ emits', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401 })));
expect(selectCustomErrorText()).toBe('401');
const destroyComponent = jest.spyOn(service.componentRef, 'destroy');
service.componentRef.instance.destroy$.next();
expect(destroyComponent).toHaveBeenCalledTimes(1);
});
});
});
export function customHttpErrorConfigFactory() {
return httpErrorConfigFactory({
errorScreen: {
component: DummyErrorComponent,
forWhichErrors: [401, 403, 404, 500],
},
});
}
function removeIfExistsInDom(errorSelector: () => HTMLDivElement | null) {
const abpError = errorSelector();
if (abpError) abpError.parentNode.removeChild(abpError);
}
function selectHtmlErrorWrapper(): HTMLDivElement | null {
return document.querySelector('abp-http-error-wrapper');
}
function selectCustomError(): HTMLDivElement | null {
return document.querySelector('abp-dummy-error');
}
function selectCustomErrorText(): string {
return selectCustomError().querySelector('p').textContent;
}

@ -1,87 +1,122 @@
import { CoreModule } from '@abp/ng.core';
import { Component } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { NgModule } from '@angular/core';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { NgxsModule } from '@ngxs/store';
import { timer } from 'rxjs';
import { ToastContainerComponent } from '../components/toast-container/toast-container.component';
import { ToastComponent } from '../components/toast/toast.component';
import { ToasterService } from '../services/toaster.service';
import { ThemeSharedModule } from '../theme-shared.module';
import { OAuthService } from 'angular-oauth2-oidc';
@Component({
selector: 'abp-dummy',
template: `
<abp-toast-container></abp-toast-container>
`,
@NgModule({
exports: [ToastContainerComponent],
entryComponents: [ToastContainerComponent],
declarations: [ToastContainerComponent, ToastComponent],
imports: [CoreModule.forTest()],
})
class DummyComponent {
constructor(public toaster: ToasterService) {}
}
export class MockModule {}
describe('ToasterService', () => {
let spectator: Spectator<DummyComponent>;
let spectator: SpectatorService<ToasterService>;
let service: ToasterService;
const createComponent = createComponentFactory({
component: DummyComponent,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule],
mocks: [OAuthService],
const createService = createServiceFactory({
service: ToasterService,
imports: [NgxsModule.forRoot(), CoreModule.forTest(), MockModule],
});
beforeEach(() => {
spectator = createComponent();
service = spectator.get(ToasterService);
spectator = createService();
service = spectator.service;
});
afterEach(() => {
clearElements();
});
test.skip('should display an error toast', () => {
service.error('test', 'title');
test('should display a toast', async () => {
service.show('MESSAGE', 'TITLE');
spectator.detectChanges();
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(spectator.query('div.toast')).toBeTruthy();
expect(spectator.query('.toast-icon i')).toHaveClass('fa-times-circle');
expect(spectator.query('div.toast-title')).toHaveText('title');
expect(spectator.query('p.toast-message')).toHaveText('test');
expect(selectToasterElement('.fa-exclamation-circle')).toBeTruthy();
expect(selectToasterContent('.toast-title')).toBe('TITLE');
expect(selectToasterContent('.toast-message')).toBe('MESSAGE');
});
test.skip('should display a warning toast', () => {
service.warn('test', 'title');
spectator.detectChanges();
expect(spectator.query('.toast-icon i')).toHaveClass('fa-exclamation-triangle');
test.each`
type | selector | icon
${'info'} | ${'.toast-info'} | ${'.fa-info-circle'}
${'success'} | ${'.toast-success'} | ${'.fa-check-circle'}
${'warn'} | ${'.toast-warning'} | ${'.fa-exclamation-triangle'}
${'error'} | ${'.toast-error'} | ${'.fa-times-circle'}
`('should display $type toast', async ({ type, selector, icon }) => {
service[type]('MESSAGE', 'TITLE');
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(selectToasterContent('.toast-title')).toBe('TITLE');
expect(selectToasterContent('.toast-message')).toBe('MESSAGE');
expect(selectToasterElement()).toBe(document.querySelector(selector));
expect(selectToasterElement(icon)).toBeTruthy();
});
test.skip('should display a success toast', () => {
service.success('test', 'title');
spectator.detectChanges();
expect(spectator.query('.toast-icon i')).toHaveClass('fa-check-circle');
test('should display multiple toasts', async () => {
service.show('MESSAGE_1', 'TITLE_1');
service.show('MESSAGE_2', 'TITLE_2');
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
const titles = document.querySelectorAll('.toast-title');
expect(titles.length).toBe(2);
const messages = document.querySelectorAll('.toast-message');
expect(messages.length).toBe(2);
});
test.skip('should display an info toast', () => {
service.info('test', 'title');
spectator.detectChanges();
expect(spectator.query('.toast-icon i')).toHaveClass('fa-info-circle');
test('should remove a toast when remove is called', async () => {
service.show('MESSAGE');
service.remove(0);
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(selectToasterElement()).toBeNull();
});
test.skip('should display multiple toasts', () => {
service.info('detail1', 'summary1');
service.info('detail2', 'summary2');
spectator.detectChanges();
expect(spectator.queryAll('div.toast-title').map(node => node.textContent.trim())).toEqual([
'summary1',
'summary2',
]);
expect(spectator.queryAll('p.toast-message').map(node => node.textContent.trim())).toEqual([
'detail1',
'detail2',
]);
test('should remove toasts when clear is called', async () => {
service.show('MESSAGE');
service.clear();
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(selectToasterElement()).toBeNull();
});
test.skip('should remove the opened toasts', () => {
service.info('test', 'title');
spectator.detectChanges();
expect(spectator.query('div.toast')).toBeTruthy();
test('should remove toasts based on containerKey when clear is called with key', async () => {
service.show('MESSAGE_1', 'TITLE_1', 'neutral', { containerKey: 'x' });
service.show('MESSAGE_2', 'TITLE_2', 'neutral', { containerKey: 'y' });
service.clear('x');
service.clear();
spectator.detectChanges();
expect(spectator.query('p-div.toast')).toBeFalsy();
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(selectToasterElement('.fa-exclamation-circle')).toBeTruthy();
expect(selectToasterContent('.toast-title')).toBe('TITLE_2');
expect(selectToasterContent('.toast-message')).toBe('MESSAGE_2');
});
});
function clearElements(selector = '.toast') {
document.querySelectorAll(selector).forEach(element => element.parentNode.removeChild(element));
}
function selectToasterContent(selector = '.toast'): string {
return selectToasterElement(selector).textContent.trim();
}
function selectToasterElement<T extends HTMLElement>(selector = '.toast'): T {
return document.querySelector(selector);
}

@ -0,0 +1,2 @@
export * from './append-content.token';
export * from './http-error.token';

@ -8,4 +8,5 @@ export * from './lib/components';
export * from './lib/directives';
export * from './lib/models';
export * from './lib/services';
export * from './lib/tokens';
export * from './lib/utils';

@ -26,8 +26,12 @@
"extractCss": true,
"assets": ["src/favicon.ico", "src/assets"],
"styles": [
{
"input": "node_modules/bootstrap/dist/css/bootstrap.min.css",
"inject": true,
"bundleName": "bootstrap.min"
},
"src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css",
{
"input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"inject": false,
@ -98,10 +102,22 @@
"karmaConfig": "karma.conf.js",
"assets": ["src/favicon.ico", "src/assets"],
"styles": [
{
"input": "node_modules/bootstrap/dist/css/bootstrap.min.css",
"inject": true,
"bundleName": "bootstrap.min"
},
"src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"node_modules/@fortawesome/fontawesome-free/css/v4-shims.min.css"
{
"input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"inject": false,
"bundleName": "fontawesome-all.min"
},
{
"input": "node_modules/@fortawesome/fontawesome-free/css/v4-shims.min.css",
"inject": false,
"bundleName": "fontawesome-v4-shims.min"
}
],
"scripts": []
}

@ -22,16 +22,29 @@
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": false,
"aot": true,
"extractCss": true,
"assets": ["src/favicon.ico", "src/assets"],
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
{
"input": "node_modules/bootstrap/dist/css/bootstrap.min.css",
"inject": true,
"bundleName": "bootstrap.min"
},
"src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/font-awesome/css/font-awesome.min.css",
"node_modules/primeng/resources/themes/nova-light/theme.css",
"node_modules/primeicons/primeicons.css",
"node_modules/primeng/resources/primeng.min.css"
{
"input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"inject": false,
"bundleName": "fontawesome-all.min"
},
{
"input": "node_modules/@fortawesome/fontawesome-free/css/v4-shims.min.css",
"inject": false,
"bundleName": "fontawesome-v4-shims.min"
}
],
"scripts": []
},
@ -48,7 +61,6 @@
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
@ -57,14 +69,10 @@
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
},
"hmr": {
"fileReplacements": [
},
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.hmr.ts"
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
]
}
@ -78,10 +86,6 @@
"configurations": {
"production": {
"browserTarget": "myProjectName:build:production"
},
"hmr": {
"hmr": true,
"browserTarget": "myProjectName:build:hmr"
}
}
},
@ -98,14 +102,27 @@
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": ["src/favicon.ico", "src/assets"],
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
{
"input": "node_modules/bootstrap/dist/css/bootstrap.min.css",
"inject": true,
"bundleName": "bootstrap.min"
},
"src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/font-awesome/css/font-awesome.min.css",
"node_modules/primeng/resources/themes/nova-light/theme.css",
"node_modules/primeicons/primeicons.css",
"node_modules/primeng/resources/primeng.min.css"
{
"input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"inject": false,
"bundleName": "fontawesome-all.min"
},
{
"input": "node_modules/@fortawesome/fontawesome-free/css/v4-shims.min.css",
"inject": false,
"bundleName": "fontawesome-v4-shims.min"
}
],
"scripts": []
}
@ -113,8 +130,14 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": ["tsconfig.app.json", "tsconfig.spec.json", "e2e/tsconfig.json"],
"exclude": ["**/node_modules/**"]
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
@ -142,6 +165,11 @@
"options": {
"tsConfig": "projects/my-project-name/tsconfig.lib.json",
"project": "projects/my-project-name/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/my-project-name/tsconfig.lib.prod.json"
}
}
},
"test": {
@ -177,6 +205,11 @@
"options": {
"tsConfig": "projects/my-project-name-config/tsconfig.lib.json",
"project": "projects/my-project-name-config/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/my-project-name-config/tsconfig.lib.prod.json"
}
}
},
"test": {
@ -202,5 +235,8 @@
}
}
},
"defaultProject": "myProjectName"
}
"defaultProject": "myProjectName",
"cli": {
"analytics": false
}
}

@ -1,47 +1,45 @@
{
"name": "my-project-name",
"name": "MyProjectName",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"start:hmr": "ng serve --configuration hmr",
"build": "ng build my-project-name",
"test": "ng test my-project-name",
"lint": "ng lint my-project-name",
"e2e": "ng e2e my-project-name"
"start:hmr": "ng serve --open",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@abp/ng.account": "~2.6.0",
"@abp/ng.theme.basic": "~2.6.0",
"@abp/ng.identity": "~2.6.0",
"@abp/ng.tenant-management": "~2.6.0",
"@abp/ng.setting-management": "~2.6.0",
"@angular/animations": "~8.2.14",
"@angular/common": "~8.2.14",
"@angular/compiler": "~8.2.14",
"@angular/core": "~8.2.14",
"@angular/forms": "~8.2.14",
"@angular/platform-browser": "~8.2.14",
"@angular/platform-browser-dynamic": "~8.2.14",
"@angular/router": "~8.2.14",
"rxjs": "~6.4.0",
"@abp/ng.tenant-management": "~2.6.0",
"@abp/ng.theme.basic": "~2.6.0",
"@angular/animations": "~9.1.2",
"@angular/common": "~9.1.2",
"@angular/compiler": "~9.1.2",
"@angular/core": "~9.1.2",
"@angular/forms": "~9.1.2",
"@angular/platform-browser": "~9.1.2",
"@angular/platform-browser-dynamic": "~9.1.2",
"@angular/router": "~9.1.2",
"rxjs": "~6.5.5",
"tslib": "^1.10.0",
"zone.js": "~0.9.1"
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.803.20",
"@angular-devkit/build-ng-packagr": "~0.803.20",
"@angular/cli": "~8.3.20",
"@angular/compiler-cli": "~8.2.14",
"@angular/language-service": "~8.2.14",
"@angularclass/hmr": "^2.1.3",
"@ngxs/hmr-plugin": "^3.5.0",
"@angular-devkit/build-angular": "~0.901.1",
"@angular-devkit/build-ng-packagr": "~0.901.1",
"@angular/cli": "~9.1.1",
"@angular/compiler-cli": "~9.1.2",
"@angular/language-service": "~9.1.2",
"@ngxs/logger-plugin": "^3.5.1",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "^5.0.0",
"@types/node": "^12.11.1",
"codelyzer": "^5.1.2",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0",
@ -49,12 +47,11 @@
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0",
"ng-packagr": "^5.7.1",
"ng-packagr": "^9.0.0",
"ngxs-schematic": "^1.1.9",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tsickle": "^0.37.0",
"tslint": "~5.15.0",
"typescript": "~3.5.3"
"typescript": "~3.8.3"
}
}

@ -12,7 +12,6 @@
]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,

@ -0,0 +1,6 @@
{
"extends": "./tsconfig.lib.json",
"angularCompilerOptions": {
"enableIvy": false
}
}

@ -12,7 +12,6 @@
]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,

@ -0,0 +1,6 @@
{
"extends": "./tsconfig.lib.json",
"angularCompilerOptions": {
"enableIvy": false
}
}

@ -1,4 +1,6 @@
import { Component } from '@angular/core';
import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { forkJoin } from 'rxjs';
@Component({
selector: 'app-root',
@ -7,4 +9,17 @@ import { Component } from '@angular/core';
<router-outlet></router-outlet>
`,
})
export class AppComponent {}
export class AppComponent implements OnInit {
constructor(private lazyLoadService: LazyLoadService) {}
ngOnInit() {
forkJoin([
this.lazyLoadService.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('fontawesome-v4-shims.min.css'),
),
this.lazyLoadService.load(
LOADING_STRATEGY.PrependAnonymousStyleToHead('fontawesome-all.min.css'),
),
]).subscribe();
}
}

@ -3,7 +3,6 @@ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { NgModule } from '@angular/core';
import { ThemeBasicModule } from '@abp/ng.theme.basic';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { TableModule } from 'primeng/table';
import { NgxValidateCoreModule } from '@ngx-validate/core';
@NgModule({
@ -12,7 +11,6 @@ import { NgxValidateCoreModule } from '@ngx-validate/core';
CoreModule,
ThemeSharedModule,
ThemeBasicModule,
TableModule,
NgbDropdownModule,
NgxValidateCoreModule,
],
@ -20,7 +18,6 @@ import { NgxValidateCoreModule } from '@ngx-validate/core';
CoreModule,
ThemeSharedModule,
ThemeBasicModule,
TableModule,
NgbDropdownModule,
NgxValidateCoreModule,
],

@ -1,25 +0,0 @@
export const environment = {
production: false,
hmr: true,
application: {
name: 'MyProjectName',
logoUrl: '',
},
oAuthConfig: {
issuer: 'https://localhost:44301',
clientId: 'MyProjectName_ConsoleTestApp',
dummyClientSecret: '1q2w3e*',
scope: 'MyProjectName',
showDebugInformation: true,
oidc: false,
requireHttps: true,
},
apis: {
default: {
url: 'https://localhost:44300',
},
},
localization: {
defaultResourceName: 'MyProjectName',
},
};

@ -1,6 +1,5 @@
export const environment = {
production: true,
hmr: false,
application: {
name: 'MyProjectName',
logoUrl: '',

@ -1,6 +1,5 @@
export const environment = {
production: false,
hmr: false,
application: {
name: 'MyProjectName',
logoUrl: '',

@ -1,20 +1,13 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { BootstrapModuleFn as Bootstrap, hmr, WebpackModule } from '@ngxs/hmr-plugin';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
declare const module: WebpackModule;
if (environment.production) {
enableProdMode();
}
const bootstrap: Bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule);
if (environment.hmr) {
hmr(module, bootstrap).catch(err => console.error(err));
} else {
bootstrap().catch(err => console.log(err));
}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));

@ -1,3 +1,7 @@
/***************************************************************************************************
* Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
*/
import '@angular/localize/init';
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
@ -35,7 +39,7 @@
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags.ts';
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
@ -55,8 +59,7 @@
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS

@ -4,6 +4,9 @@
"outDir": "./out-tsc/app",
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/test.ts", "src/**/*.spec.ts"]
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": ["src/**/*.d.ts"]
}

@ -12,4 +12,4 @@
"AuthServer": {
"Authority": "https://localhost:44301/"
}
}
}

Loading…
Cancel
Save