diff --git a/docs/en/UI/Angular/Component-Replacement.md b/docs/en/UI/Angular/Component-Replacement.md index d2c75cd67c..fd34fcbae7 100644 --- a/docs/en/UI/Angular/Component-Replacement.md +++ b/docs/en/UI/Angular/Component-Replacement.md @@ -11,25 +11,24 @@ Create a new component that you want to use instead of an ABP component. Add tha Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below: ```js -import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action +import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum import { Store } from '@ngxs/store'; // imported Store //... @Component(/* component metadata */) -export class AppComponent implements OnInit { - constructor(..., private store: Store) {} // injected Store - - ngOnInit() { - // added dispatch +export class AppComponent { + constructor( + private store: Store // injected Store + ) + { + // dispatched the AddReplaceableComponent action this.store.dispatch( new AddReplaceableComponent({ component: YourNewRoleComponent, key: eIdentityComponents.Roles, }), ); - - //... } } ``` @@ -60,30 +59,29 @@ Add the following code in your layout template (`my-layout.component.html`) wher Open `app.component.ts` in `src/app` folder and modify it as shown below: ```js -import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent +import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys -import { MyApplicationLayoutComponent } from './shared/my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent import { Store } from '@ngxs/store'; // imported Store -//... +import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent @Component(/* component metadata */) -export class AppComponent implements OnInit { - constructor(..., private store: Store) {} // injected Store - - ngOnInit() { - // added dispatch +export class AppComponent { + constructor( + private store: Store, // injected Store + ) { + // dispatched the AddReplaceableComponent action this.store.dispatch( new AddReplaceableComponent({ component: MyApplicationLayoutComponent, key: eThemeBasicComponents.ApplicationLayout, }), ); - - //... } } ``` +> If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the AddReplaceableComponent action as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of AddReplaceableComponent is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated. + ### Layout Components ![Layout Components](./images/layout-components.png) diff --git a/docs/zh-Hans/UI/Angular/Component-Replacement.md b/docs/zh-Hans/UI/Angular/Component-Replacement.md index 145359c348..221ac6775a 100644 --- a/docs/zh-Hans/UI/Angular/Component-Replacement.md +++ b/docs/zh-Hans/UI/Angular/Component-Replacement.md @@ -11,25 +11,24 @@ 然后打开 `app.component.ts` 使用 `AddReplaceableComponent` 将你的组件替换ABP组件. 如下所示: ```js -import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action +import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum import { Store } from '@ngxs/store'; // imported Store //... @Component(/* component metadata */) -export class AppComponent implements OnInit { - constructor(..., private store: Store) {} // injected Store - - ngOnInit() { - // added dispatch +export class AppComponent { + constructor( + private store: Store // injected Store + ) + { + // dispatched the AddReplaceableComponent action this.store.dispatch( new AddReplaceableComponent({ component: YourNewRoleComponent, key: eIdentityComponents.Roles, }), ); - - //... } } ``` @@ -47,9 +46,7 @@ export class AppComponent implements OnInit { 运行以下命令在 `angular` 文件夹中生成布局: ```bash -yarn ng generate component shared/my-application-layout --export --entryComponent - -# You don't need the --entryComponent option in Angular 9 +yarn ng generate component my-application-layout ``` 在你的布局模板(`my-layout.component.html`)中添加以下代码: @@ -61,26 +58,23 @@ yarn ng generate component shared/my-application-layout --export --entryComponen 打开 `src/app` 文件夹下的 `app.component.ts` 文件添加以下内容: ```js -import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent +import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys -import { MyApplicationLayoutComponent } from './shared/my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent import { Store } from '@ngxs/store'; // imported Store -//... +import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent @Component(/* component metadata */) -export class AppComponent implements OnInit { - constructor(..., private store: Store) {} // injected Store - - ngOnInit() { - // added dispatch +export class AppComponent { + constructor( + private store: Store, // injected Store + ) { + // dispatched the AddReplaceableComponent action this.store.dispatch( new AddReplaceableComponent({ component: MyApplicationLayoutComponent, key: eThemeBasicComponents.ApplicationLayout, }), ); - - //... } } ``` diff --git a/npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts b/npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts index 3b948cf63d..8df6a384ad 100644 --- a/npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts +++ b/npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts @@ -5,5 +5,8 @@ import { ReplaceableComponents } from '../models/replaceable-components'; */ export class AddReplaceableComponent { static readonly type = '[ReplaceableComponents] Add'; - constructor(public payload: ReplaceableComponents.ReplaceableComponent) {} + constructor( + public payload: ReplaceableComponents.ReplaceableComponent, + public reload?: boolean, + ) {} } diff --git a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts index 2548cfece6..7b1395223c 100644 --- a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts @@ -24,6 +24,13 @@ import { TreeNode } from '../utils/tree-utils'; export class DynamicLayoutComponent implements OnDestroy { layout: Type; + // TODO: Consider a shared enum (eThemeSharedComponents) for known layouts + readonly layouts = new Map([ + ['application', 'Theme.ApplicationLayoutComponent'], + ['account', 'Theme.AccountLayoutComponent'], + ['empty', 'Theme.EmptyLayoutComponent'], + ]); + isLayoutVisible = true; constructor( @@ -36,11 +43,6 @@ export class DynamicLayoutComponent implements OnDestroy { const route = injector.get(ActivatedRoute); const router = injector.get(Router); const routes = injector.get(RoutesService); - const layouts = { - application: this.getComponent('Theme.ApplicationLayoutComponent'), - account: this.getComponent('Theme.AccountLayoutComponent'), - empty: this.getComponent('Theme.EmptyLayoutComponent'), - }; router.events.pipe(takeUntilDestroy(this)).subscribe(event => { if (event instanceof NavigationEnd) { @@ -62,7 +64,8 @@ export class DynamicLayoutComponent implements OnDestroy { if (!expectedLayout) expectedLayout = eLayoutType.empty; - this.layout = layouts[expectedLayout].component; + const key = this.layouts.get(expectedLayout); + this.layout = this.getComponent(key).component; } }); diff --git a/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts b/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts index 6ba6bd18b5..5de83fef7a 100644 --- a/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts +++ b/npm/ng-packs/packages/core/src/lib/guards/permission.guard.ts @@ -1,12 +1,11 @@ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot, Router } from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; import { Store } from '@ngxs/store'; import { Observable, of } from 'rxjs'; import { tap } from 'rxjs/operators'; -import snq from 'snq'; import { RestOccurError } from '../actions/rest.actions'; -import { ConfigState } from '../states/config.state'; import { RoutesService } from '../services/routes.service'; +import { ConfigState } from '../states/config.state'; import { findRoute, getRoutePath } from '../utils/route-utils'; @Injectable({ @@ -15,18 +14,16 @@ import { findRoute, getRoutePath } from '../utils/route-utils'; export class PermissionGuard implements CanActivate { constructor(private router: Router, private routes: RoutesService, private store: Store) {} - canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, - ): Observable | boolean { + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { let { requiredPolicy } = route.data || {}; if (!requiredPolicy) { - requiredPolicy = findRoute(this.routes, getRoutePath(this.router, state.url))?.requiredPolicy; - - if (!requiredPolicy) return true; + const route = findRoute(this.routes, getRoutePath(this.router, state.url)); + requiredPolicy = route?.requiredPolicy; } + if (!requiredPolicy) return of(true); + return this.store.select(ConfigState.getGrantedPolicy(requiredPolicy)).pipe( tap(access => { if (!access) { diff --git a/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts b/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts index 7c07621ad4..fe9cecaa79 100644 --- a/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts @@ -1,8 +1,10 @@ -import { Injectable } from '@angular/core'; -import { State, Action, StateContext, Selector, createSelector } from '@ngxs/store'; +import { noop } from '@abp/ng.core'; +import { Injectable, NgZone } from '@angular/core'; +import { Router } from '@angular/router'; +import { Action, createSelector, Selector, State, StateContext } from '@ngxs/store'; +import snq from 'snq'; import { AddReplaceableComponent } from '../actions/replaceable-components.actions'; import { ReplaceableComponents } from '../models/replaceable-components'; -import snq from 'snq'; @State({ name: 'ReplaceableComponentsState', @@ -28,10 +30,28 @@ export class ReplaceableComponentsState { return selector; } + constructor(private ngZone: NgZone, private router: Router) {} + + // TODO: Create a shared service for route reload and more + private reloadRoute() { + const { shouldReuseRoute } = this.router.routeReuseStrategy; + const setRouteReuse = (reuse: typeof shouldReuseRoute) => { + this.router.routeReuseStrategy.shouldReuseRoute = reuse; + }; + + setRouteReuse(() => false); + this.router.navigated = false; + + this.ngZone.run(async () => { + await this.router.navigateByUrl(this.router.url).catch(noop); + setRouteReuse(shouldReuseRoute); + }); + } + @Action(AddReplaceableComponent) replaceableComponentsAction( { getState, patchState }: StateContext, - { payload }: AddReplaceableComponent, + { payload, reload }: AddReplaceableComponent, ) { let { replaceableComponents } = getState(); @@ -48,5 +68,7 @@ export class ReplaceableComponentsState { patchState({ replaceableComponents, }); + + if (reload) this.reloadRoute(); } } diff --git a/npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts index a995a4c822..dcfd27ab69 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts @@ -1,22 +1,57 @@ +import { APP_BASE_HREF } from '@angular/common'; +import { Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest'; -import { Store } from '@ngxs/store'; +import { Actions, Store } from '@ngxs/store'; import { of } from 'rxjs'; -import { PermissionGuard } from '../guards/permission.guard'; import { RestOccurError } from '../actions'; +import { PermissionGuard } from '../guards/permission.guard'; +import { RoutesService } from '../services/routes.service'; describe('PermissionGuard', () => { let spectator: SpectatorService; let guard: PermissionGuard; + let routes: SpyObject; let store: SpyObject; + @Component({ template: '' }) + class DummyComponent {} + const createService = createServiceFactory({ service: PermissionGuard, mocks: [Store], + declarations: [DummyComponent], + imports: [ + RouterModule.forRoot([ + { + path: 'test', + component: DummyComponent, + data: { + requiredPolicy: 'TestPolicy', + }, + }, + ]), + ], + providers: [ + { + provide: APP_BASE_HREF, + useValue: '/', + }, + { + provide: Actions, + useValue: { + pipe() { + return of(null); + }, + }, + }, + ], }); beforeEach(() => { spectator = createService(); guard = spectator.service; + routes = spectator.inject(RoutesService); store = spectator.get(Store); }); @@ -41,17 +76,32 @@ describe('PermissionGuard', () => { }); }); - it('should find the requiredPolicy from child route', done => { + it('should check the requiredPolicy from RoutesService', done => { + routes.add([ + { + path: '/test', + name: 'Test', + requiredPolicy: 'TestPolicy', + }, + ]); store.select.andReturn(of(false)); const spy = jest.spyOn(store, 'select'); - guard - .canActivate( - { data: {}, routeConfig: { children: [{ path: 'test', data: { requiredPolicy: 'TestPolicy' } }] } } as any, - { url: 'test' } as any, - ) - .subscribe(() => { - expect(spy.mock.calls[0][0]({ auth: { grantedPolicies: { TestPolicy: true } } })).toBe(true); - done(); - }); + guard.canActivate({ data: {} } as any, { url: 'test' } as any).subscribe(() => { + expect(spy.mock.calls[0][0]({ auth: { grantedPolicies: { TestPolicy: true } } })).toBe(true); + done(); + }); + }); + + it('should return Observable if RoutesService does not have requiredPolicy for given URL', done => { + routes.add([ + { + path: '/test', + name: 'Test', + }, + ]); + guard.canActivate({ data: {} } as any, { url: 'test' } as any).subscribe(result => { + expect(result).toBe(true); + done(); + }); }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/replaceable-components.state.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/replaceable-components.state.spec.ts index c294b131e1..dc113c433c 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/replaceable-components.state.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/replaceable-components.state.spec.ts @@ -1,21 +1,27 @@ +import { APP_BASE_HREF } from '@angular/common'; +import { Component } from '@angular/core'; +import { Router, RouterModule } from '@angular/router'; +import { SpyObject } from '@ngneat/spectator'; import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; import { NgxsModule, Store } from '@ngxs/store'; -import { ReplaceableComponentsState } from '../states/replaceable-components.state'; -import { Component } from '@angular/core'; import { AddReplaceableComponent } from '../actions'; +import { ReplaceableComponentsState } from '../states/replaceable-components.state'; @Component({ selector: 'abp-dummy', template: 'dummy works' }) class DummyComponent {} describe('ReplaceableComponentsState', () => { let spectator: SpectatorHost; + let router: SpyObject; const createHost = createHostFactory({ component: DummyComponent, - imports: [NgxsModule.forRoot([ReplaceableComponentsState])], + providers: [{ provide: APP_BASE_HREF, useValue: '/' }], + imports: [RouterModule.forRoot([]), NgxsModule.forRoot([ReplaceableComponentsState])], }); beforeEach(() => { spectator = createHost(''); + router = spectator.inject(Router); }); it('should add a component to the state', () => { @@ -38,4 +44,16 @@ describe('ReplaceableComponentsState', () => { }); expect(store.selectSnapshot(ReplaceableComponentsState.getAll)).toHaveLength(1); }); + + it('should call reloadRoute when reload parameter is given as true to AddReplaceableComponent', async () => { + const spy = jest.spyOn(router, 'navigateByUrl'); + const store = spectator.get(Store); + store.dispatch(new AddReplaceableComponent({ component: DummyComponent, key: 'Dummy' })); + store.dispatch(new AddReplaceableComponent({ component: null, key: 'Dummy' }, true)); + + await spectator.fixture.whenStable(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(router.url); + }); }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/rest.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/rest.service.spec.ts index cb865787fa..96bbc197d0 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/rest.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/rest.service.spec.ts @@ -1,10 +1,10 @@ -import { ConfigState } from '@abp/ng.core'; import { createHttpFactory, HttpMethod, SpectatorHttp, SpyObject } from '@ngneat/spectator/jest'; import { NgxsModule, Store } from '@ngxs/store'; import { of, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { Rest } from '../models'; import { RestService } from '../services/rest.service'; +import { ConfigState } from '../states/config.state'; import { CORE_OPTIONS } from '../tokens'; describe('HttpClient testing', () => {