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:
```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)

@ -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,
}),
);
//...
}
}
```

@ -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,
) {}
}

@ -24,6 +24,13 @@ import { TreeNode } from '../utils/tree-utils';
export class DynamicLayoutComponent implements OnDestroy {
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;
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;
}
});

@ -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> | boolean {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
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) {

@ -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<ReplaceableComponents.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<ReplaceableComponents.State>,
{ payload }: AddReplaceableComponent,
{ payload, reload }: AddReplaceableComponent,
) {
let { replaceableComponents } = getState();
@ -48,5 +68,7 @@ export class ReplaceableComponentsState {
patchState({
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 { 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<PermissionGuard>;
let guard: PermissionGuard;
let routes: SpyObject<RoutesService>;
let store: SpyObject<Store>;
@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<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 { 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<DummyComponent>;
let router: SpyObject<Router>;
const createHost = createHostFactory({
component: DummyComponent,
imports: [NgxsModule.forRoot([ReplaceableComponentsState])],
providers: [{ provide: APP_BASE_HREF, useValue: '/' }],
imports: [RouterModule.forRoot([]), NgxsModule.forRoot([ReplaceableComponentsState])],
});
beforeEach(() => {
spectator = createHost('<abp-dummy></abp-dummy>');
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);
});
});

@ -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', () => {

Loading…
Cancel
Save