Merge branch 'master' into dev

pull/4637/head^2
mehmet-erim 5 years ago
commit 963200bf5e

@ -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: Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below:
```js ```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 { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum
import { Store } from '@ngxs/store'; // imported Store import { Store } from '@ngxs/store'; // imported Store
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent {
constructor(..., private store: Store) {} // injected Store constructor(
private store: Store // injected Store
ngOnInit() { )
// added dispatch {
// dispatched the AddReplaceableComponent action
this.store.dispatch( this.store.dispatch(
new AddReplaceableComponent({ new AddReplaceableComponent({
component: YourNewRoleComponent, component: YourNewRoleComponent,
key: eIdentityComponents.Roles, 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: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```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 { 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 { Store } from '@ngxs/store'; // imported Store
//... import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent {
constructor(..., private store: Store) {} // injected Store constructor(
private store: Store, // injected Store
ngOnInit() { ) {
// added dispatch // dispatched the AddReplaceableComponent action
this.store.dispatch( this.store.dispatch(
new AddReplaceableComponent({ new AddReplaceableComponent({
component: MyApplicationLayoutComponent, component: MyApplicationLayoutComponent,
key: eThemeBasicComponents.ApplicationLayout, 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
![Layout Components](./images/layout-components.png) ![Layout Components](./images/layout-components.png)

@ -11,25 +11,24 @@
然后打开 `app.component.ts` 使用 `AddReplaceableComponent` 将你的组件替换ABP组件. 如下所示: 然后打开 `app.component.ts` 使用 `AddReplaceableComponent` 将你的组件替换ABP组件. 如下所示:
```js ```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 { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum
import { Store } from '@ngxs/store'; // imported Store import { Store } from '@ngxs/store'; // imported Store
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent {
constructor(..., private store: Store) {} // injected Store constructor(
private store: Store // injected Store
ngOnInit() { )
// added dispatch {
// dispatched the AddReplaceableComponent action
this.store.dispatch( this.store.dispatch(
new AddReplaceableComponent({ new AddReplaceableComponent({
component: YourNewRoleComponent, component: YourNewRoleComponent,
key: eIdentityComponents.Roles, key: eIdentityComponents.Roles,
}), }),
); );
//...
} }
} }
``` ```
@ -47,9 +46,7 @@ export class AppComponent implements OnInit {
运行以下命令在 `angular` 文件夹中生成布局: 运行以下命令在 `angular` 文件夹中生成布局:
```bash ```bash
yarn ng generate component shared/my-application-layout --export --entryComponent yarn ng generate component my-application-layout
# You don't need the --entryComponent option in Angular 9
``` ```
在你的布局模板(`my-layout.component.html`)中添加以下代码: 在你的布局模板(`my-layout.component.html`)中添加以下代码:
@ -61,26 +58,23 @@ yarn ng generate component shared/my-application-layout --export --entryComponen
打开 `src/app` 文件夹下的 `app.component.ts` 文件添加以下内容: 打开 `src/app` 文件夹下的 `app.component.ts` 文件添加以下内容:
```js ```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 { 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 { Store } from '@ngxs/store'; // imported Store
//... import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent {
constructor(..., private store: Store) {} // injected Store constructor(
private store: Store, // injected Store
ngOnInit() { ) {
// added dispatch // dispatched the AddReplaceableComponent action
this.store.dispatch( this.store.dispatch(
new AddReplaceableComponent({ new AddReplaceableComponent({
component: MyApplicationLayoutComponent, component: MyApplicationLayoutComponent,
key: eThemeBasicComponents.ApplicationLayout, key: eThemeBasicComponents.ApplicationLayout,
}), }),
); );
//...
} }
} }
``` ```

@ -5,5 +5,8 @@ import { ReplaceableComponents } from '../models/replaceable-components';
*/ */
export class AddReplaceableComponent { export class AddReplaceableComponent {
static readonly type = '[ReplaceableComponents] Add'; static readonly type = '[ReplaceableComponents] Add';
constructor(public payload: ReplaceableComponents.ReplaceableComponent) {} constructor(
public payload: ReplaceableComponents.ReplaceableComponent,
public reload?: boolean,
) {}
} }

@ -24,6 +24,13 @@ import { TreeNode } from '../utils/tree-utils';
export class DynamicLayoutComponent implements OnDestroy { export class DynamicLayoutComponent implements OnDestroy {
layout: Type<any>; layout: Type<any>;
// TODO: Consider a shared enum (eThemeSharedComponents) for known layouts
readonly layouts = new Map([
['application', 'Theme.ApplicationLayoutComponent'],
['account', 'Theme.AccountLayoutComponent'],
['empty', 'Theme.EmptyLayoutComponent'],
]);
isLayoutVisible = true; isLayoutVisible = true;
constructor( constructor(
@ -36,11 +43,6 @@ export class DynamicLayoutComponent implements OnDestroy {
const route = injector.get(ActivatedRoute); const route = injector.get(ActivatedRoute);
const router = injector.get(Router); const router = injector.get(Router);
const routes = injector.get(RoutesService); 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 => { router.events.pipe(takeUntilDestroy(this)).subscribe(event => {
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd) {
@ -62,7 +64,8 @@ export class DynamicLayoutComponent implements OnDestroy {
if (!expectedLayout) expectedLayout = eLayoutType.empty; if (!expectedLayout) expectedLayout = eLayoutType.empty;
this.layout = layouts[expectedLayout].component; const key = this.layouts.get(expectedLayout);
this.layout = this.getComponent(key).component;
} }
}); });

@ -1,12 +1,11 @@
import { Injectable } from '@angular/core'; 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 { Store } from '@ngxs/store';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import snq from 'snq';
import { RestOccurError } from '../actions/rest.actions'; import { RestOccurError } from '../actions/rest.actions';
import { ConfigState } from '../states/config.state';
import { RoutesService } from '../services/routes.service'; import { RoutesService } from '../services/routes.service';
import { ConfigState } from '../states/config.state';
import { findRoute, getRoutePath } from '../utils/route-utils'; import { findRoute, getRoutePath } from '../utils/route-utils';
@Injectable({ @Injectable({
@ -15,18 +14,16 @@ import { findRoute, getRoutePath } from '../utils/route-utils';
export class PermissionGuard implements CanActivate { export class PermissionGuard implements CanActivate {
constructor(private router: Router, private routes: RoutesService, private store: Store) {} constructor(private router: Router, private routes: RoutesService, private store: Store) {}
canActivate( canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> | boolean {
let { requiredPolicy } = route.data || {}; let { requiredPolicy } = route.data || {};
if (!requiredPolicy) { if (!requiredPolicy) {
requiredPolicy = findRoute(this.routes, getRoutePath(this.router, state.url))?.requiredPolicy; const route = findRoute(this.routes, getRoutePath(this.router, state.url));
requiredPolicy = route?.requiredPolicy;
if (!requiredPolicy) return true;
} }
if (!requiredPolicy) return of(true);
return this.store.select(ConfigState.getGrantedPolicy(requiredPolicy)).pipe( return this.store.select(ConfigState.getGrantedPolicy(requiredPolicy)).pipe(
tap(access => { tap(access => {
if (!access) { if (!access) {

@ -1,8 +1,10 @@
import { Injectable } from '@angular/core'; import { noop } from '@abp/ng.core';
import { State, Action, StateContext, Selector, createSelector } from '@ngxs/store'; 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 { AddReplaceableComponent } from '../actions/replaceable-components.actions';
import { ReplaceableComponents } from '../models/replaceable-components'; import { ReplaceableComponents } from '../models/replaceable-components';
import snq from 'snq';
@State<ReplaceableComponents.State>({ @State<ReplaceableComponents.State>({
name: 'ReplaceableComponentsState', name: 'ReplaceableComponentsState',
@ -28,10 +30,28 @@ export class ReplaceableComponentsState {
return selector; 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) @Action(AddReplaceableComponent)
replaceableComponentsAction( replaceableComponentsAction(
{ getState, patchState }: StateContext<ReplaceableComponents.State>, { getState, patchState }: StateContext<ReplaceableComponents.State>,
{ payload }: AddReplaceableComponent, { payload, reload }: AddReplaceableComponent,
) { ) {
let { replaceableComponents } = getState(); let { replaceableComponents } = getState();
@ -48,5 +68,7 @@ export class ReplaceableComponentsState {
patchState({ patchState({
replaceableComponents, replaceableComponents,
}); });
if (reload) this.reloadRoute();
} }
} }

@ -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 { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest';
import { Store } from '@ngxs/store'; import { Actions, Store } from '@ngxs/store';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { PermissionGuard } from '../guards/permission.guard';
import { RestOccurError } from '../actions'; import { RestOccurError } from '../actions';
import { PermissionGuard } from '../guards/permission.guard';
import { RoutesService } from '../services/routes.service';
describe('PermissionGuard', () => { describe('PermissionGuard', () => {
let spectator: SpectatorService<PermissionGuard>; let spectator: SpectatorService<PermissionGuard>;
let guard: PermissionGuard; let guard: PermissionGuard;
let routes: SpyObject<RoutesService>;
let store: SpyObject<Store>; let store: SpyObject<Store>;
@Component({ template: '' })
class DummyComponent {}
const createService = createServiceFactory({ const createService = createServiceFactory({
service: PermissionGuard, service: PermissionGuard,
mocks: [Store], 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(() => { beforeEach(() => {
spectator = createService(); spectator = createService();
guard = spectator.service; guard = spectator.service;
routes = spectator.inject(RoutesService);
store = spectator.get(Store); 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)); store.select.andReturn(of(false));
const spy = jest.spyOn(store, 'select'); const spy = jest.spyOn(store, 'select');
guard guard.canActivate({ data: {} } as any, { url: 'test' } as any).subscribe(() => {
.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); expect(spy.mock.calls[0][0]({ auth: { grantedPolicies: { TestPolicy: true } } })).toBe(true);
done(); done();
}); });
}); });
it('should return Observable<true> 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();
});
});
}); });

@ -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 { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { NgxsModule, Store } from '@ngxs/store'; import { NgxsModule, Store } from '@ngxs/store';
import { ReplaceableComponentsState } from '../states/replaceable-components.state';
import { Component } from '@angular/core';
import { AddReplaceableComponent } from '../actions'; import { AddReplaceableComponent } from '../actions';
import { ReplaceableComponentsState } from '../states/replaceable-components.state';
@Component({ selector: 'abp-dummy', template: 'dummy works' }) @Component({ selector: 'abp-dummy', template: 'dummy works' })
class DummyComponent {} class DummyComponent {}
describe('ReplaceableComponentsState', () => { describe('ReplaceableComponentsState', () => {
let spectator: SpectatorHost<DummyComponent>; let spectator: SpectatorHost<DummyComponent>;
let router: SpyObject<Router>;
const createHost = createHostFactory({ const createHost = createHostFactory({
component: DummyComponent, component: DummyComponent,
imports: [NgxsModule.forRoot([ReplaceableComponentsState])], providers: [{ provide: APP_BASE_HREF, useValue: '/' }],
imports: [RouterModule.forRoot([]), NgxsModule.forRoot([ReplaceableComponentsState])],
}); });
beforeEach(() => { beforeEach(() => {
spectator = createHost('<abp-dummy></abp-dummy>'); spectator = createHost('<abp-dummy></abp-dummy>');
router = spectator.inject(Router);
}); });
it('should add a component to the state', () => { it('should add a component to the state', () => {
@ -38,4 +44,16 @@ describe('ReplaceableComponentsState', () => {
}); });
expect(store.selectSnapshot(ReplaceableComponentsState.getAll)).toHaveLength(1); 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);
});
}); });

@ -1,10 +1,10 @@
import { ConfigState } from '@abp/ng.core';
import { createHttpFactory, HttpMethod, SpectatorHttp, SpyObject } from '@ngneat/spectator/jest'; import { createHttpFactory, HttpMethod, SpectatorHttp, SpyObject } from '@ngneat/spectator/jest';
import { NgxsModule, Store } from '@ngxs/store'; import { NgxsModule, Store } from '@ngxs/store';
import { of, throwError } from 'rxjs'; import { of, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { Rest } from '../models'; import { Rest } from '../models';
import { RestService } from '../services/rest.service'; import { RestService } from '../services/rest.service';
import { ConfigState } from '../states/config.state';
import { CORE_OPTIONS } from '../tokens'; import { CORE_OPTIONS } from '../tokens';
describe('HttpClient testing', () => { describe('HttpClient testing', () => {

Loading…
Cancel
Save