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**. 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. 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请求 ## 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 # 如何懒加载 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 { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { APP_INITIALIZER, Injector, ModuleWithProviders, NgModule } from '@angular/core'; import { APP_INITIALIZER, Injector, ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@ -22,8 +22,9 @@ import { ReplaceableTemplateDirective } from './directives/replaceable-template.
import { StopPropagationDirective } from './directives/stop-propagation.directive'; import { StopPropagationDirective } from './directives/stop-propagation.directive';
import { VisibilityDirective } from './directives/visibility.directive'; import { VisibilityDirective } from './directives/visibility.directive';
import { ApiInterceptor } from './interceptors/api.interceptor'; import { ApiInterceptor } from './interceptors/api.interceptor';
import { LocalizationModule } from './localization.module';
import { ABP } from './models/common'; 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 { SortPipe } from './pipes/sort.pipe';
import { ConfigPlugin, NGXS_CONFIG_PLUGIN_OPTIONS } from './plugins/config.plugin'; import { ConfigPlugin, NGXS_CONFIG_PLUGIN_OPTIONS } from './plugins/config.plugin';
import { LocaleProvider } from './providers/locale.provider'; import { LocaleProvider } from './providers/locale.provider';
@ -32,77 +33,128 @@ import { ProfileState } from './states/profile.state';
import { ReplaceableComponentsState } from './states/replaceable-components.state'; import { ReplaceableComponentsState } from './states/replaceable-components.state';
import { SessionState } from './states/session.state'; import { SessionState } from './states/session.state';
import { CORE_OPTIONS } from './tokens/options.token'; import { CORE_OPTIONS } from './tokens/options.token';
import { getInitialData, localeInitializer } from './utils/initial-utils';
import './utils/date-extensions'; import './utils/date-extensions';
import { getInitialData, localeInitializer } from './utils/initial-utils';
export function storageFactory(): OAuthStorage { export function storageFactory(): OAuthStorage {
return localStorage; 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({ @NgModule({
imports: [ exports: [
NgxsModule.forFeature([ReplaceableComponentsState, ProfileState, SessionState, ConfigState]),
NgxsRouterPluginModule.forRoot(),
NgxsStoragePluginModule.forRoot({ key: ['SessionState'] }),
OAuthModule,
CommonModule, CommonModule,
HttpClientModule, HttpClientModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterModule, RouterModule,
],
declarations: [ AbstractNgModelComponent,
ReplaceableRouteContainerComponent,
RouterOutletComponent,
DynamicLayoutComponent,
AutofocusDirective, AutofocusDirective,
DynamicLayoutComponent,
EllipsisDirective, EllipsisDirective,
ForDirective, ForDirective,
FormSubmitDirective, FormSubmitDirective,
LocalizationPipe,
SortPipe,
InitDirective, InitDirective,
PermissionDirective,
VisibilityDirective,
InputEventDebounceDirective, InputEventDebounceDirective,
StopPropagationDirective, PermissionDirective,
ReplaceableRouteContainerComponent,
ReplaceableTemplateDirective, ReplaceableTemplateDirective,
AbstractNgModelComponent, RouterOutletComponent,
SortPipe,
StopPropagationDirective,
VisibilityDirective,
], ],
exports: [ imports: [
OAuthModule,
CommonModule, CommonModule,
HttpClientModule, HttpClientModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterModule, RouterModule,
RouterOutletComponent, ],
DynamicLayoutComponent, declarations: [
AbstractNgModelComponent, AbstractNgModelComponent,
ReplaceableRouteContainerComponent,
AutofocusDirective, AutofocusDirective,
DynamicLayoutComponent,
EllipsisDirective, EllipsisDirective,
ForDirective, ForDirective,
FormSubmitDirective, FormSubmitDirective,
InitDirective, InitDirective,
PermissionDirective,
VisibilityDirective,
InputEventDebounceDirective, InputEventDebounceDirective,
PermissionDirective,
ReplaceableRouteContainerComponent,
ReplaceableTemplateDirective, ReplaceableTemplateDirective,
StopPropagationDirective, RouterOutletComponent,
LocalizationPipe,
SortPipe, SortPipe,
LocalizationPipe, StopPropagationDirective,
VisibilityDirective,
], ],
providers: [LocalizationPipe],
entryComponents: [ entryComponents: [
RouterOutletComponent, RouterOutletComponent,
DynamicLayoutComponent, DynamicLayoutComponent,
ReplaceableRouteContainerComponent, 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 { 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 { return {
ngModule: CoreModule, ngModule: RootCoreModule,
providers: [ providers: [
LocaleProvider, 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 { EventEmitter } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { eLayoutType } from '../enums/common'; import { eLayoutType } from '../enums/common';
import { Config } from './config'; import { Config } from './config';
@ -14,6 +15,10 @@ export namespace ABP {
skipGetAppConfiguration?: boolean; skipGetAppConfiguration?: boolean;
} }
export interface Test {
baseHref?: Router;
}
export type PagedResponse<T> = { export type PagedResponse<T> = {
totalCount: number; totalCount: number;
} & PagedItemsResponse<T>; } & 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 { Store } from '@ngxs/store';
import { Config } from '../models'; import { Config } from '../models';
import { ConfigState } from '../states'; import { ConfigState } from '../states';
@ -10,12 +10,28 @@ import { ConfigState } from '../states';
export class LocalizationPipe implements PipeTransform { export class LocalizationPipe implements PipeTransform {
constructor(private store: Store) {} constructor(private store: Store) {}
transform(value: string | Config.LocalizationWithDefault = '', ...interpolateParams: string[]): string { transform(
value: string | Config.LocalizationWithDefault = '',
...interpolateParams: string[]
): string {
return this.store.selectSnapshot( return this.store.selectSnapshot(
ConfigState.getLocalization( ConfigState.getLocalization(
value, 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 { RouterTestingModule } from '@angular/router/testing';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; 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 { environment } from '../../../../../apps/dev-app/src/environments/environment';
import { LAYOUTS } from '@abp/ng.theme.basic';
import { RouterOutletComponent } from '../components'; import { RouterOutletComponent } from '../components';
import { CoreModule } from '../core.module'; import { CoreModule } from '../core.module';
import { eLayoutType } from '../enums/common'; import { eLayoutType } from '../enums/common';
import { ABP } from '../models'; import { ABP } from '../models';
import { ConfigPlugin, NGXS_CONFIG_PLUGIN_OPTIONS } from '../plugins'; import { ConfigPlugin } from '../plugins';
import { ConfigState } from '../states'; import { ConfigState } from '../states';
import { addAbpRoutes } from '../utils'; import { addAbpRoutes } from '../utils';
import { OAuthModule } from 'angular-oauth2-oidc';
addAbpRoutes([ addAbpRoutes([
{ {
@ -60,9 +59,6 @@ addAbpRoutes([
const expectedState = { const expectedState = {
environment, environment,
requirements: {
layouts: LAYOUTS,
},
routes: [ routes: [
{ {
name: '::Menu:Home', name: '::Menu:Home',
@ -323,9 +319,9 @@ describe('ConfigPlugin', () => {
const createService = createServiceFactory({ const createService = createServiceFactory({
service: ConfigPlugin, service: ConfigPlugin,
imports: [ imports: [
CoreModule, NgxsModule.forRoot([ConfigState]),
CoreModule.forRoot({ environment }),
OAuthModule.forRoot(), OAuthModule.forRoot(),
NgxsModule.forRoot([]),
RouterTestingModule.withRoutes([ RouterTestingModule.withRoutes([
{ {
path: '', path: '',
@ -341,17 +337,6 @@ describe('ConfigPlugin', () => {
{ path: 'tenant-management', component: RouterOutletComponent }, { 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(() => { beforeEach(() => {

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

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

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

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

@ -1,74 +1,101 @@
import { CoreModule } from '@abp/ng.core'; import { CoreModule } from '@abp/ng.core';
import { Component } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing'; import { fakeAsync, tick } from '@angular/core/testing';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { NgxsModule } from '@ngxs/store'; import { NgxsModule } from '@ngxs/store';
import { ConfirmationService } from '../services/confirmation.service'; import { timer } from 'rxjs';
import { ThemeSharedModule } from '../theme-shared.module'; import { take } from 'rxjs/operators';
import { OAuthModule, OAuthService } from 'angular-oauth2-oidc'; import { ConfirmationComponent } from '../components';
import { Confirmation } from '../models';
@Component({ import { ConfirmationService } from '../services';
selector: 'abp-dummy',
template: ` @NgModule({
<abp-confirmation></abp-confirmation> exports: [ConfirmationComponent],
`, entryComponents: [ConfirmationComponent],
declarations: [ConfirmationComponent],
imports: [CoreModule.forTest()],
}) })
class DummyComponent { export class MockModule {}
constructor(public confirmation: ConfirmationService) {}
}
describe('ConfirmationService', () => { describe('ConfirmationService', () => {
let spectator: Spectator<DummyComponent>; let spectator: SpectatorService<ConfirmationService>;
let service: ConfirmationService; let service: ConfirmationService;
const createComponent = createComponentFactory({ const createService = createServiceFactory({
component: DummyComponent, service: ConfirmationService,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule], imports: [NgxsModule.forRoot(), CoreModule.forTest(), MockModule],
mocks: [OAuthService],
}); });
beforeEach(() => { beforeEach(() => {
spectator = createComponent(); spectator = createService();
service = spectator.get(ConfirmationService); service = spectator.service;
});
afterEach(() => {
clearElements();
}); });
test.skip('should display a confirmation popup', () => { test('should display a confirmation popup', fakeAsync(() => {
service.info('test', 'title'); service.show('MESSAGE', 'TITLE');
spectator.detectChanges(); tick();
expect(spectator.query('div.confirmation .title')).toHaveText('title'); expect(selectConfirmationContent('.title')).toBe('TITLE');
expect(spectator.query('div.confirmation .message')).toHaveText('test'); 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 => { test('should close with ESC key', done => {
service.info('test', 'title').subscribe(() => { service
setTimeout(() => { .info('', '')
spectator.detectComponentChanges(); .pipe(take(1))
expect(spectator.query('div.confirmation')).toBeFalsy(); .subscribe(status => {
expect(status).toBe(Confirmation.Status.dismiss);
done(); done();
}, 0); });
});
spectator.detectChanges(); const escape = new KeyboardEvent('keyup', { key: 'Escape' });
expect(spectator.query('div.confirmation')).toBeTruthy(); document.dispatchEvent(escape);
spectator.dispatchKeyboardEvent('div.confirmation', 'keyup', 'Escape');
}); });
test.skip('should close when click cancel button', done => { test('should close when click cancel button', async done => {
service.info('test', 'title', { yesText: 'Sure', cancelText: 'Exit' }).subscribe(() => { service.info('', '', { yesText: 'Sure', cancelText: 'Exit' }).subscribe(status => {
spectator.detectComponentChanges(); expect(status).toBe(Confirmation.Status.reject);
setTimeout(() => { done();
expect(spectator.query('div.confirmation')).toBeFalsy();
done();
}, 0);
}); });
spectator.detectChanges(); await timer(0).toPromise();
expect(spectator.query('div.confirmation')).toBeTruthy(); expect(selectConfirmationContent('button#cancel')).toBe('Exit');
expect(spectator.query('button#cancel')).toHaveText('Exit'); expect(selectConfirmationContent('button#confirm')).toBe('Sure');
expect(spectator.query('button#confirm')).toHaveText('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 { CoreModule, RestOccurError } from '@abp/ng.core';
import { Location } from '@angular/common'; import { APP_BASE_HREF } from '@angular/common';
import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { Component, NgModule } from '@angular/core'; import { Component, NgModule } from '@angular/core';
import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; import { NavigationError, ResolveEnd, RouterModule } from '@angular/router';
import { NgxsModule, Store } from '@ngxs/store'; 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 { DEFAULT_ERROR_MESSAGES, ErrorHandler } from '../handlers';
import { ThemeSharedModule } from '../theme-shared.module'; import { ConfirmationService } from '../services';
import { RouterError, RouterDataResolved } from '@ngxs/router-plugin'; import { httpErrorConfigFactory } from '../tokens/http-error.token';
import { NavigationError, ResolveEnd } from '@angular/router';
import { OAuthModule, OAuthService } from 'angular-oauth2-oidc';
@Component({ @NgModule({
selector: 'abp-dummy', exports: [HttpErrorWrapperComponent],
template: 'dummy works! <abp-confirmation></abp-confirmation>', declarations: [HttpErrorWrapperComponent],
entryComponents: [HttpErrorWrapperComponent],
imports: [CoreModule],
}) })
class DummyComponent { class MockModule {}
constructor(public errorHandler: ErrorHandler) {}
}
let spectator: SpectatorRouting<DummyComponent>; let spectator: SpectatorService<ErrorHandler>;
let service: ErrorHandler;
let store: Store; let store: Store;
const errorConfirmation: jest.Mock = jest.fn(() => of(null));
const CONFIRMATION_BUTTONS = {
hideCancelBtn: true,
yesText: 'AbpAccount::Close',
};
describe('ErrorHandler', () => { describe('ErrorHandler', () => {
const createComponent = createRoutingFactory({ const createService = createServiceFactory({
component: DummyComponent, service: ErrorHandler,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot([])], imports: [RouterModule.forRoot([]), NgxsModule.forRoot([]), CoreModule, MockModule],
mocks: [OAuthService], mocks: [OAuthService],
stubsEnabled: false, providers: [
routes: [ { provide: APP_BASE_HREF, useValue: '/' },
{ path: '', component: DummyComponent }, {
{ path: 'account/login', component: RouterOutletComponent }, provide: 'HTTP_ERROR_CONFIG',
useFactory: httpErrorConfigFactory,
},
{
provide: ConfirmationService,
useValue: {
error: errorConfirmation,
},
},
], ],
}); });
beforeEach(() => { beforeEach(() => {
spectator = createComponent(); spectator = createService();
service = spectator.service;
store = spectator.get(Store); store = spectator.get(Store);
store.selectSnapshot = jest.fn(() => '/x');
});
const abpError = document.querySelector('abp-http-error-wrapper'); afterEach(() => {
if (abpError) document.body.removeChild(abpError); errorConfirmation.mockClear();
removeIfExistsInDom(selectHtmlErrorWrapper);
}); });
test.skip('should display the error component when server error occurs', () => { test('should display HttpErrorWrapperComponent when server error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 }))); const createComponent = jest.spyOn(service, 'createErrorComponent');
expect(document.querySelector('.error-template')).toHaveText( const error = new HttpErrorResponse({ status: 500 });
DEFAULT_ERROR_MESSAGES.defaultError500.title, const params = {
); title: {
expect(document.querySelector('.error-details')).toHaveText( key: 'AbpAccount::500Message',
DEFAULT_ERROR_MESSAGES.defaultError500.details, 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', () => { test('should display HttpErrorWrapperComponent when authorize error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 403 }))); const createComponent = jest.spyOn(service, 'createErrorComponent');
expect(document.querySelector('.error-template')).toHaveText( const error = new HttpErrorResponse({ status: 403 });
DEFAULT_ERROR_MESSAGES.defaultError403.title, const params = {
); title: {
expect(document.querySelector('.error-details')).toHaveText( key: 'AbpAccount::DefaultErrorMessage403',
DEFAULT_ERROR_MESSAGES.defaultError403.details, 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', () => { test('should display HttpErrorWrapperComponent when unknown error occurs', () => {
store.dispatch( const createComponent = jest.spyOn(service, 'createErrorComponent');
new RestOccurError(new HttpErrorResponse({ status: 0, statusText: 'Unknown Error' })), const error = new HttpErrorResponse({ status: 0, statusText: 'Unknown Error' });
); const params = {
expect(document.querySelector('.error-template')).toHaveText( title: {
DEFAULT_ERROR_MESSAGES.defaultError.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 }))); store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 404 })));
spectator.detectChanges();
expect(spectator.query('.confirmation .title')).toHaveText( expect(errorConfirmation).toHaveBeenCalledWith(
DEFAULT_ERROR_MESSAGES.defaultError404.title, {
); key: 'AbpAccount::DefaultErrorMessage404',
expect(spectator.query('.confirmation .message')).toHaveText( defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.details,
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 }))); store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 412 })));
spectator.detectChanges();
expect(spectator.query('.confirmation .title')).toHaveText( expect(errorConfirmation).toHaveBeenCalledWith(
DEFAULT_ERROR_MESSAGES.defaultError.title,
);
expect(spectator.query('.confirmation .message')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError.details, 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 }))); store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401 })));
spectator.detectChanges();
spectator.click('#confirm'); expect(errorConfirmation).toHaveBeenCalledWith(
await spectator.fixture.whenStable(); {
expect(spectator.get(Location).path()).toBe('/account/login'); 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 () => { test('should call error method of ConfirmationService when authenticated error occurs with _AbpErrorFormat header', done => {
let headers: HttpHeaders = new HttpHeaders(); spectator
headers = headers.append('_AbpErrorFormat', '_AbpErrorFormat'); .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 }))); store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401, headers })));
spectator.detectChanges();
spectator.click('#confirm'); expect(errorConfirmation).toHaveBeenCalledWith(
await spectator.fixture.whenStable(); DEFAULT_ERROR_MESSAGES.defaultError.title,
expect(spectator.get(Location).path()).toBe('/account/login'); 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(); let headers: HttpHeaders = new HttpHeaders();
headers = headers.append('_AbpErrorFormat', '_AbpErrorFormat'); headers = headers.append('_AbpErrorFormat', '_AbpErrorFormat');
store.dispatch( store.dispatch(
new RestOccurError( new RestOccurError(
new HttpErrorResponse({ new HttpErrorResponse({
@ -125,17 +223,28 @@ describe('ErrorHandler', () => {
}), }),
), ),
); );
spectator.detectChanges();
expect(spectator.query('.title')).toHaveText('test message'); expect(errorConfirmation).toHaveBeenCalledWith(
expect(spectator.query('.confirmation .message')).toHaveText('test detail'); '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({ @Component({
selector: 'abp-dummy-error', selector: 'abp-dummy-error',
template: template: '<p>{{errorStatus}}</p>',
'<p>{{errorStatus}}</p><button id="close-dummy" (click)="destroy$.next()">Close</button>',
}) })
class DummyErrorComponent { class DummyErrorComponent {
errorStatus; errorStatus;
@ -150,68 +259,103 @@ class DummyErrorComponent {
class ErrorModule {} class ErrorModule {}
describe('ErrorHandler with custom error component', () => { describe('ErrorHandler with custom error component', () => {
const createComponent = createRoutingFactory({ const createService = createServiceFactory({
component: DummyComponent, service: ErrorHandler,
imports: [ imports: [
CoreModule, RouterModule.forRoot([]),
ThemeSharedModule.forRoot({
httpErrorConfig: {
errorScreen: { component: DummyErrorComponent, forWhichErrors: [401, 403, 404, 500] },
},
}),
NgxsModule.forRoot([]), NgxsModule.forRoot([]),
CoreModule,
MockModule,
ErrorModule, ErrorModule,
], ],
mocks: [OAuthService], mocks: [OAuthService, ConfirmationService],
stubsEnabled: false, providers: [
{ provide: APP_BASE_HREF, useValue: '/' },
routes: [ {
{ path: '', component: DummyComponent }, provide: 'HTTP_ERROR_CONFIG',
{ path: 'account/login', component: RouterOutletComponent }, useFactory: customHttpErrorConfigFactory,
},
], ],
}); });
beforeEach(() => { beforeEach(() => {
spectator = createComponent(); spectator = createService();
service = spectator.service;
store = spectator.get(Store); store = spectator.get(Store);
store.selectSnapshot = jest.fn(() => '/x');
});
const abpError = document.querySelector('abp-http-error-wrapper'); afterEach(() => {
if (abpError) document.body.removeChild(abpError); removeIfExistsInDom(selectCustomError);
}); });
describe('Custom error component', () => { 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 }))); 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 }))); 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 }))); 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'))); 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 }))); 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', () => { test('should call destroy method of componentRef when destroy$ emits', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 }))); store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 401 })));
document.querySelector<HTMLButtonElement>('#close-dummy').click();
spectator.detectChanges(); expect(selectCustomErrorText()).toBe('401');
expect(document.querySelector('abp-dummy-error')).toBeFalsy();
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 { CoreModule } from '@abp/ng.core';
import { Component } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing'; import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { NgxsModule } from '@ngxs/store'; 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 { ToasterService } from '../services/toaster.service';
import { ThemeSharedModule } from '../theme-shared.module';
import { OAuthService } from 'angular-oauth2-oidc'; @NgModule({
exports: [ToastContainerComponent],
@Component({ entryComponents: [ToastContainerComponent],
selector: 'abp-dummy', declarations: [ToastContainerComponent, ToastComponent],
template: ` imports: [CoreModule.forTest()],
<abp-toast-container></abp-toast-container>
`,
}) })
class DummyComponent { export class MockModule {}
constructor(public toaster: ToasterService) {}
}
describe('ToasterService', () => { describe('ToasterService', () => {
let spectator: Spectator<DummyComponent>; let spectator: SpectatorService<ToasterService>;
let service: ToasterService; let service: ToasterService;
const createComponent = createComponentFactory({ const createService = createServiceFactory({
component: DummyComponent, service: ToasterService,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule], imports: [NgxsModule.forRoot(), CoreModule.forTest(), MockModule],
mocks: [OAuthService],
}); });
beforeEach(() => { beforeEach(() => {
spectator = createComponent(); spectator = createService();
service = spectator.get(ToasterService); service = spectator.service;
});
afterEach(() => {
clearElements();
}); });
test.skip('should display an error toast', () => { test('should display a toast', async () => {
service.error('test', 'title'); service.show('MESSAGE', 'TITLE');
spectator.detectChanges(); await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(spectator.query('div.toast')).toBeTruthy(); expect(selectToasterElement('.fa-exclamation-circle')).toBeTruthy();
expect(spectator.query('.toast-icon i')).toHaveClass('fa-times-circle'); expect(selectToasterContent('.toast-title')).toBe('TITLE');
expect(spectator.query('div.toast-title')).toHaveText('title'); expect(selectToasterContent('.toast-message')).toBe('MESSAGE');
expect(spectator.query('p.toast-message')).toHaveText('test');
}); });
test.skip('should display a warning toast', () => { test.each`
service.warn('test', 'title'); type | selector | icon
spectator.detectChanges(); ${'info'} | ${'.toast-info'} | ${'.fa-info-circle'}
expect(spectator.query('.toast-icon i')).toHaveClass('fa-exclamation-triangle'); ${'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', () => { test('should display multiple toasts', async () => {
service.success('test', 'title'); service.show('MESSAGE_1', 'TITLE_1');
spectator.detectChanges(); service.show('MESSAGE_2', 'TITLE_2');
expect(spectator.query('.toast-icon i')).toHaveClass('fa-check-circle');
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', () => { test('should remove a toast when remove is called', async () => {
service.info('test', 'title'); service.show('MESSAGE');
spectator.detectChanges(); service.remove(0);
expect(spectator.query('.toast-icon i')).toHaveClass('fa-info-circle');
await timer(0).toPromise();
service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(selectToasterElement()).toBeNull();
}); });
test.skip('should display multiple toasts', () => { test('should remove toasts when clear is called', async () => {
service.info('detail1', 'summary1'); service.show('MESSAGE');
service.info('detail2', 'summary2'); service.clear();
spectator.detectChanges(); await timer(0).toPromise();
expect(spectator.queryAll('div.toast-title').map(node => node.textContent.trim())).toEqual([ service['containerComponentRef'].changeDetectorRef.detectChanges();
'summary1',
'summary2', expect(selectToasterElement()).toBeNull();
]);
expect(spectator.queryAll('p.toast-message').map(node => node.textContent.trim())).toEqual([
'detail1',
'detail2',
]);
}); });
test.skip('should remove the opened toasts', () => { test('should remove toasts based on containerKey when clear is called with key', async () => {
service.info('test', 'title'); service.show('MESSAGE_1', 'TITLE_1', 'neutral', { containerKey: 'x' });
spectator.detectChanges(); service.show('MESSAGE_2', 'TITLE_2', 'neutral', { containerKey: 'y' });
expect(spectator.query('div.toast')).toBeTruthy(); service.clear('x');
service.clear(); await timer(0).toPromise();
spectator.detectChanges(); service['containerComponentRef'].changeDetectorRef.detectChanges();
expect(spectator.query('p-div.toast')).toBeFalsy();
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/directives';
export * from './lib/models'; export * from './lib/models';
export * from './lib/services'; export * from './lib/services';
export * from './lib/tokens';
export * from './lib/utils'; export * from './lib/utils';

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

@ -22,16 +22,29 @@
"main": "src/main.ts", "main": "src/main.ts",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"aot": false, "aot": true,
"extractCss": true, "extractCss": true,
"assets": ["src/favicon.ico", "src/assets"], "assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [ "styles": [
{
"input": "node_modules/bootstrap/dist/css/bootstrap.min.css",
"inject": true,
"bundleName": "bootstrap.min"
},
"src/styles.scss", "src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css", {
"node_modules/font-awesome/css/font-awesome.min.css", "input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"node_modules/primeng/resources/themes/nova-light/theme.css", "inject": false,
"node_modules/primeicons/primeicons.css", "bundleName": "fontawesome-all.min"
"node_modules/primeng/resources/primeng.min.css" },
{
"input": "node_modules/@fortawesome/fontawesome-free/css/v4-shims.min.css",
"inject": false,
"bundleName": "fontawesome-v4-shims.min"
}
], ],
"scripts": [] "scripts": []
}, },
@ -48,7 +61,6 @@
"sourceMap": false, "sourceMap": false,
"extractCss": true, "extractCss": true,
"namedChunks": false, "namedChunks": false,
"aot": true,
"extractLicenses": true, "extractLicenses": true,
"vendorChunk": false, "vendorChunk": false,
"buildOptimizer": true, "buildOptimizer": true,
@ -57,14 +69,10 @@
"type": "initial", "type": "initial",
"maximumWarning": "2mb", "maximumWarning": "2mb",
"maximumError": "5mb" "maximumError": "5mb"
} },
]
},
"hmr": {
"fileReplacements": [
{ {
"replace": "src/environments/environment.ts", "type": "anyComponentStyle",
"with": "src/environments/environment.hmr.ts" "maximumWarning": "6kb"
} }
] ]
} }
@ -78,10 +86,6 @@
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "myProjectName:build:production" "browserTarget": "myProjectName:build:production"
},
"hmr": {
"hmr": true,
"browserTarget": "myProjectName:build:hmr"
} }
} }
}, },
@ -98,14 +102,27 @@
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js", "karmaConfig": "karma.conf.js",
"assets": ["src/favicon.ico", "src/assets"], "assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [ "styles": [
{
"input": "node_modules/bootstrap/dist/css/bootstrap.min.css",
"inject": true,
"bundleName": "bootstrap.min"
},
"src/styles.scss", "src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css", {
"node_modules/font-awesome/css/font-awesome.min.css", "input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
"node_modules/primeng/resources/themes/nova-light/theme.css", "inject": false,
"node_modules/primeicons/primeicons.css", "bundleName": "fontawesome-all.min"
"node_modules/primeng/resources/primeng.min.css" },
{
"input": "node_modules/@fortawesome/fontawesome-free/css/v4-shims.min.css",
"inject": false,
"bundleName": "fontawesome-v4-shims.min"
}
], ],
"scripts": [] "scripts": []
} }
@ -113,8 +130,14 @@
"lint": { "lint": {
"builder": "@angular-devkit/build-angular:tslint", "builder": "@angular-devkit/build-angular:tslint",
"options": { "options": {
"tsConfig": ["tsconfig.app.json", "tsconfig.spec.json", "e2e/tsconfig.json"], "tsConfig": [
"exclude": ["**/node_modules/**"] "tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
} }
}, },
"e2e": { "e2e": {
@ -142,6 +165,11 @@
"options": { "options": {
"tsConfig": "projects/my-project-name/tsconfig.lib.json", "tsConfig": "projects/my-project-name/tsconfig.lib.json",
"project": "projects/my-project-name/ng-package.json" "project": "projects/my-project-name/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/my-project-name/tsconfig.lib.prod.json"
}
} }
}, },
"test": { "test": {
@ -177,6 +205,11 @@
"options": { "options": {
"tsConfig": "projects/my-project-name-config/tsconfig.lib.json", "tsConfig": "projects/my-project-name-config/tsconfig.lib.json",
"project": "projects/my-project-name-config/ng-package.json" "project": "projects/my-project-name-config/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "projects/my-project-name-config/tsconfig.lib.prod.json"
}
} }
}, },
"test": { "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", "version": "0.0.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"start:hmr": "ng serve --configuration hmr", "start:hmr": "ng serve --open",
"build": "ng build my-project-name", "build": "ng build",
"test": "ng test my-project-name", "test": "ng test",
"lint": "ng lint my-project-name", "lint": "ng lint",
"e2e": "ng e2e my-project-name" "e2e": "ng e2e"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@abp/ng.account": "~2.6.0", "@abp/ng.account": "~2.6.0",
"@abp/ng.theme.basic": "~2.6.0",
"@abp/ng.identity": "~2.6.0", "@abp/ng.identity": "~2.6.0",
"@abp/ng.tenant-management": "~2.6.0",
"@abp/ng.setting-management": "~2.6.0", "@abp/ng.setting-management": "~2.6.0",
"@angular/animations": "~8.2.14", "@abp/ng.tenant-management": "~2.6.0",
"@angular/common": "~8.2.14", "@abp/ng.theme.basic": "~2.6.0",
"@angular/compiler": "~8.2.14", "@angular/animations": "~9.1.2",
"@angular/core": "~8.2.14", "@angular/common": "~9.1.2",
"@angular/forms": "~8.2.14", "@angular/compiler": "~9.1.2",
"@angular/platform-browser": "~8.2.14", "@angular/core": "~9.1.2",
"@angular/platform-browser-dynamic": "~8.2.14", "@angular/forms": "~9.1.2",
"@angular/router": "~8.2.14", "@angular/platform-browser": "~9.1.2",
"rxjs": "~6.4.0", "@angular/platform-browser-dynamic": "~9.1.2",
"@angular/router": "~9.1.2",
"rxjs": "~6.5.5",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"zone.js": "~0.9.1" "zone.js": "~0.10.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~0.803.20", "@angular-devkit/build-angular": "~0.901.1",
"@angular-devkit/build-ng-packagr": "~0.803.20", "@angular-devkit/build-ng-packagr": "~0.901.1",
"@angular/cli": "~8.3.20", "@angular/cli": "~9.1.1",
"@angular/compiler-cli": "~8.2.14", "@angular/compiler-cli": "~9.1.2",
"@angular/language-service": "~8.2.14", "@angular/language-service": "~9.1.2",
"@angularclass/hmr": "^2.1.3",
"@ngxs/hmr-plugin": "^3.5.0",
"@ngxs/logger-plugin": "^3.5.1", "@ngxs/logger-plugin": "^3.5.1",
"@types/jasmine": "~3.3.8", "@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4", "@types/node": "^12.11.1",
"codelyzer": "^5.0.0", "codelyzer": "^5.1.2",
"jasmine-core": "~3.4.0", "jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1", "jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0", "karma": "~4.1.0",
@ -49,12 +47,11 @@
"karma-coverage-istanbul-reporter": "~2.0.1", "karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~2.0.1", "karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0", "karma-jasmine-html-reporter": "^1.4.0",
"ng-packagr": "^5.7.1", "ng-packagr": "^9.0.0",
"ngxs-schematic": "^1.1.9", "ngxs-schematic": "^1.1.9",
"protractor": "~5.4.0", "protractor": "~5.4.0",
"ts-node": "~7.0.0", "ts-node": "~7.0.0",
"tsickle": "^0.37.0",
"tslint": "~5.15.0", "tslint": "~5.15.0",
"typescript": "~3.5.3" "typescript": "~3.8.3"
} }
} }

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

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

@ -12,7 +12,6 @@
] ]
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true, "skipTemplateCodegen": true,
"strictMetadataEmit": true, "strictMetadataEmit": true,
"fullTemplateTypeCheck": 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({ @Component({
selector: 'app-root', selector: 'app-root',
@ -7,4 +9,17 @@ import { Component } from '@angular/core';
<router-outlet></router-outlet> <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 { NgModule } from '@angular/core';
import { ThemeBasicModule } from '@abp/ng.theme.basic'; import { ThemeBasicModule } from '@abp/ng.theme.basic';
import { ThemeSharedModule } from '@abp/ng.theme.shared'; import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { TableModule } from 'primeng/table';
import { NgxValidateCoreModule } from '@ngx-validate/core'; import { NgxValidateCoreModule } from '@ngx-validate/core';
@NgModule({ @NgModule({
@ -12,7 +11,6 @@ import { NgxValidateCoreModule } from '@ngx-validate/core';
CoreModule, CoreModule,
ThemeSharedModule, ThemeSharedModule,
ThemeBasicModule, ThemeBasicModule,
TableModule,
NgbDropdownModule, NgbDropdownModule,
NgxValidateCoreModule, NgxValidateCoreModule,
], ],
@ -20,7 +18,6 @@ import { NgxValidateCoreModule } from '@ngx-validate/core';
CoreModule, CoreModule,
ThemeSharedModule, ThemeSharedModule,
ThemeBasicModule, ThemeBasicModule,
TableModule,
NgbDropdownModule, NgbDropdownModule,
NgxValidateCoreModule, 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 = { export const environment = {
production: true, production: true,
hmr: false,
application: { application: {
name: 'MyProjectName', name: 'MyProjectName',
logoUrl: '', logoUrl: '',

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

@ -1,20 +1,13 @@
import { enableProdMode } from '@angular/core'; import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { BootstrapModuleFn as Bootstrap, hmr, WebpackModule } from '@ngxs/hmr-plugin';
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
declare const module: WebpackModule;
if (environment.production) { if (environment.production) {
enableProdMode(); enableProdMode();
} }
const bootstrap: Bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule); platformBrowserDynamic()
.bootstrapModule(AppModule)
if (environment.hmr) { .catch((err) => console.error(err));
hmr(module, bootstrap).catch(err => console.error(err));
} else {
bootstrap().catch(err => console.log(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. * This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file. * 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 * 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 * 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. * 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. * The flags allowed in zone-flags.ts are listed here.
* *
@ -55,8 +59,7 @@
/*************************************************************************************************** /***************************************************************************************************
* Zone JS is required by default for Angular itself. * 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 * APPLICATION IMPORTS

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

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

Loading…
Cancel
Save