diff --git a/npm/ng-packs/packages/core/src/lib/core.module.ts b/npm/ng-packs/packages/core/src/lib/core.module.ts index 67350b88ce..b3c687672d 100644 --- a/npm/ng-packs/packages/core/src/lib/core.module.ts +++ b/npm/ng-packs/packages/core/src/lib/core.module.ts @@ -28,6 +28,7 @@ import { ConfigState } from './states/config.state'; import { ProfileState } from './states/profile.state'; import { SessionState } from './states/session.state'; import { getInitialData, localeInitializer } from './utils/initial-utils'; +import './utils/date-extensions'; @NgModule({ imports: [ diff --git a/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts index 68a5ae6f75..870e4ec144 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts @@ -1,31 +1,68 @@ -import { Directive, ElementRef, Input, OnDestroy, OnInit, Optional, Renderer2 } from '@angular/core'; +import { + Directive, + ElementRef, + Input, + OnDestroy, + OnInit, + Renderer2, + ViewContainerRef, + TemplateRef, + Optional, + SimpleChanges, + OnChanges, +} from '@angular/core'; import { Store } from '@ngxs/store'; import { ConfigState } from '../states'; import { takeUntilDestroy } from '../utils'; +import { Subscription } from 'rxjs'; @Directive({ selector: '[abpPermission]', }) -export class PermissionDirective implements OnInit, OnDestroy { +export class PermissionDirective implements OnInit, OnDestroy, OnChanges { @Input('abpPermission') condition: string; - constructor(@Optional() private elRef: ElementRef, private renderer: Renderer2, private store: Store) {} + subscription: Subscription; + + constructor( + private elRef: ElementRef, + private renderer: Renderer2, + private store: Store, + @Optional() private templateRef: TemplateRef, + private vcRef: ViewContainerRef, + ) {} + + private check() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + + this.subscription = this.store + .select(ConfigState.getGrantedPolicy(this.condition)) + .pipe(takeUntilDestroy(this)) + .subscribe(isGranted => { + if (this.templateRef && isGranted) { + this.vcRef.clear(); + this.vcRef.createEmbeddedView(this.templateRef); + } else if (this.templateRef && !isGranted) { + this.vcRef.clear(); + } else if (!isGranted && !this.templateRef) { + this.renderer.removeChild((this.elRef.nativeElement as HTMLElement).parentElement, this.elRef.nativeElement); + } + }); + } ngOnInit() { - if (this.condition) { - this.store - .select(ConfigState.getGrantedPolicy(this.condition)) - .pipe(takeUntilDestroy(this)) - .subscribe(isGranted => { - if (!isGranted) { - this.renderer.removeChild( - (this.elRef.nativeElement as HTMLElement).parentElement, - this.elRef.nativeElement, - ); - } - }); + if (this.templateRef && !this.condition) { + this.vcRef.createEmbeddedView(this.templateRef); } } ngOnDestroy(): void {} + + ngOnChanges({ condition }: SimpleChanges) { + if ((condition || { currentValue: null }).currentValue) { + this.check(); + } + } } 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 97c6c3ebc8..e23eeb40c2 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,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate } from '@angular/router'; +import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; import { Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @@ -13,8 +13,14 @@ import { ConfigState } from '../states'; export class PermissionGuard implements CanActivate { constructor(private store: Store) {} - canActivate({ data }: ActivatedRouteSnapshot): Observable { - const resource = snq(() => data.routes.requiredPolicy) || (data.requiredPolicy as string); + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + let resource = snq(() => route.data.routes.requiredPolicy) || snq(() => route.data.requiredPolicy as string); + if (!resource) { + resource = snq( + () => route.routeConfig.children.find(child => state.url.indexOf(child.path) > -1).data.requiredPolicy, + ); + } + return this.store.select(ConfigState.getGrantedPolicy(resource)).pipe( tap(access => { if (!access) { diff --git a/npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts index c1d324620d..6d9dd3964f 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts @@ -43,4 +43,43 @@ describe('PermissionDirective', () => { expect(spy.mock.calls).toHaveLength(0); }); }); + + describe('structural', () => { + beforeEach(() => { + spectator = createDirective( + '
Testing Permission Directive
', + { hostProps: { condition: '' } }, + ); + directive = spectator.directive; + }); + + it('should be created', () => { + expect(directive).toBeTruthy(); + }); + + it('should remove the element from DOM', () => { + expect(spectator.query('#test-element')).toBeTruthy(); + expect(spectator.directive.subscription).toBeUndefined(); + spectator.setHostInput({ condition: 'test' }); + expect(spectator.directive.subscription).toBeTruthy(); + grantedPolicy$.next(true); + expect(spectator.query('#test-element')).toBeTruthy(); + grantedPolicy$.next(false); + expect(spectator.query('#test-element')).toBeFalsy(); + grantedPolicy$.next(true); + grantedPolicy$.next(true); + expect(spectator.queryAll('#test-element')).toHaveLength(1); + }); + + describe('#subscription', () => { + it('should call the unsubscribe', () => { + const spy = jest.fn(() => {}); + spectator.setHostInput({ condition: 'test' }); + spectator.directive.subscription.unsubscribe = spy; + spectator.setHostInput({ condition: 'test2' }); + + expect(spy).toHaveBeenCalled(); + }); + }); + }); }); 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 b358005837..a995a4c822 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 @@ -23,7 +23,7 @@ describe('PermissionGuard', () => { it('should return true when the grantedPolicy is true', done => { store.select.andReturn(of(true)); const spy = jest.spyOn(store, 'dispatch'); - guard.canActivate({ data: { requiredPolicy: '' } } as any).subscribe(res => { + guard.canActivate({ data: { requiredPolicy: 'test' } } as any, null).subscribe(res => { expect(res).toBe(true); expect(spy.mock.calls).toHaveLength(0); done(); @@ -33,11 +33,25 @@ describe('PermissionGuard', () => { it('should return false and dispatch RestOccurError when the grantedPolicy is false', done => { store.select.andReturn(of(false)); const spy = jest.spyOn(store, 'dispatch'); - guard.canActivate({ data: { requiredPolicy: '' } } as any).subscribe(res => { + guard.canActivate({ data: { requiredPolicy: 'test' } } as any, null).subscribe(res => { expect(res).toBe(false); expect(spy.mock.calls[0][0] instanceof RestOccurError).toBeTruthy(); expect((spy.mock.calls[0][0] as RestOccurError).payload).toEqual({ status: 403 }); done(); }); }); + + it('should find the requiredPolicy from child route', done => { + 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(); + }); + }); }); diff --git a/npm/ng-packs/packages/core/src/lib/utils/date-extensions.ts b/npm/ng-packs/packages/core/src/lib/utils/date-extensions.ts new file mode 100644 index 0000000000..3522b000c8 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/date-extensions.ts @@ -0,0 +1,13 @@ +export {}; + +declare global { + interface Date { + toLocalISOString(): string; + } +} + +Date.prototype.toLocalISOString = function(this: Date): string { + const timezoneOffset = this.getTimezoneOffset(); + + return new Date(this.getTime() - timezoneOffset * 60000).toISOString(); +}; diff --git a/npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html b/npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html index fa0ee93d1a..59fa3f4199 100644 --- a/npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html +++ b/npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html @@ -6,7 +6,7 @@
-
-
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.scss b/npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.scss similarity index 87% rename from npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.scss rename to npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.scss index a98b5a3ca6..b9f7690a5b 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.scss +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.scss @@ -1,7 +1,6 @@ .error { position: fixed; top: 0; - background-color: #fff; width: 100vw; height: 100vh; z-index: 999999; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts similarity index 79% rename from npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.ts rename to npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts index 70ddce726e..a102672ad6 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/error/error.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/http-error-wrapper/http-error-wrapper.component.ts @@ -1,25 +1,27 @@ import { Config, takeUntilDestroy } from '@abp/ng.core'; import { AfterViewInit, + ApplicationRef, Component, ComponentFactoryResolver, ElementRef, EmbeddedViewRef, + Injector, OnDestroy, + OnInit, Type, ViewChild, - ApplicationRef, - Injector, } from '@angular/core'; import { fromEvent, Subject } from 'rxjs'; import { debounceTime, filter } from 'rxjs/operators'; +import snq from 'snq'; @Component({ - selector: 'abp-error', - templateUrl: './error.component.html', - styleUrls: ['error.component.scss'], + selector: 'abp-http-error-wrapper', + templateUrl: './http-error-wrapper.component.html', + styleUrls: ['http-error-wrapper.component.scss'], }) -export class ErrorComponent implements AfterViewInit, OnDestroy { +export class HttpErrorWrapperComponent implements AfterViewInit, OnDestroy, OnInit { appRef: ApplicationRef; cfRes: ComponentFactoryResolver; @@ -38,6 +40,8 @@ export class ErrorComponent implements AfterViewInit, OnDestroy { hideCloseIcon = false; + backgroundColor: string; + @ViewChild('container', { static: false }) containerRef: ElementRef; @@ -45,6 +49,11 @@ export class ErrorComponent implements AfterViewInit, OnDestroy { return this.status ? `[${this.status}]` : ''; } + ngOnInit() { + this.backgroundColor = + snq(() => window.getComputedStyle(document.body).getPropertyValue('background-color')) || '#fff'; + } + ngAfterViewInit() { if (this.customComponent) { const customComponentRef = this.cfRes.resolveComponentFactory(this.customComponent).create(this.injector); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts index ec0d3ac808..e4663c38aa 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts @@ -15,7 +15,7 @@ import { Navigate, RouterError, RouterState, RouterDataResolved } from '@ngxs/ro import { Actions, ofActionSuccessful, Store } from '@ngxs/store'; import { Observable, Subject } from 'rxjs'; import snq from 'snq'; -import { ErrorComponent } from '../components/error/error.component'; +import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; import { HttpErrorConfig, ErrorScreenErrorCodes } from '../models/common'; import { Toaster } from '../models/toaster'; import { ConfirmationService } from '../services/confirmation.service'; @@ -45,7 +45,7 @@ export const DEFAULT_ERROR_MESSAGES = { @Injectable({ providedIn: 'root' }) export class ErrorHandler { - componentRef: ComponentRef; + componentRef: ComponentRef; constructor( private actions: Actions, @@ -196,11 +196,11 @@ export class ErrorHandler { ); } - createErrorComponent(instance: Partial) { + createErrorComponent(instance: Partial) { const renderer = this.rendererFactory.createRenderer(null, null); const host = renderer.selectRootElement(document.body, true); - this.componentRef = this.cfRes.resolveComponentFactory(ErrorComponent).create(this.injector); + this.componentRef = this.cfRes.resolveComponentFactory(HttpErrorWrapperComponent).create(this.injector); for (const key in this.componentRef.instance) { if (this.componentRef.instance.hasOwnProperty(key)) { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts index 05e303e4c9..1d1f256c68 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.component.spec.ts @@ -1,14 +1,14 @@ import { SpectatorHost, createHostFactory } from '@ngneat/spectator/jest'; -import { ErrorComponent } from '../components/error/error.component'; +import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; import { LocalizationPipe } from '@abp/ng.core'; import { Store } from '@ngxs/store'; import { Renderer2, ElementRef } from '@angular/core'; import { Subject } from 'rxjs'; describe('ErrorComponent', () => { - let spectator: SpectatorHost; + let spectator: SpectatorHost; const createHost = createHostFactory({ - component: ErrorComponent, + component: HttpErrorWrapperComponent, declarations: [LocalizationPipe], mocks: [Store], providers: [ @@ -18,7 +18,7 @@ describe('ErrorComponent', () => { }); beforeEach(() => { - spectator = createHost(''); + spectator = createHost(''); spectator.component.destroy$ = new Subject(); }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts index d3265691de..88ae00a874 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts @@ -22,14 +22,17 @@ describe('ErrorHandler', () => { component: DummyComponent, imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot([])], stubsEnabled: false, - routes: [{ path: '', component: DummyComponent }, { path: 'account/login', component: RouterOutletComponent }], + routes: [ + { path: '', component: DummyComponent }, + { path: 'account/login', component: RouterOutletComponent }, + ], }); beforeEach(() => { spectator = createComponent(); store = spectator.get(Store); - const abpError = document.querySelector('abp-error'); + const abpError = document.querySelector('abp-http-error-wrapper'); if (abpError) document.body.removeChild(abpError); }); @@ -133,14 +136,17 @@ describe('ErrorHandler with custom error component', () => { ErrorModule, ], stubsEnabled: false, - routes: [{ path: '', component: DummyComponent }, { path: 'account/login', component: RouterOutletComponent }], + routes: [ + { path: '', component: DummyComponent }, + { path: 'account/login', component: RouterOutletComponent }, + ], }); beforeEach(() => { spectator = createComponent(); store = spectator.get(Store); - const abpError = document.querySelector('abp-error'); + const abpError = document.querySelector('abp-http-error-wrapper'); if (abpError) document.body.removeChild(abpError); }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts b/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts index f9cf4097cd..7464df328a 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts @@ -8,7 +8,7 @@ import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.componen import { ButtonComponent } from './components/button/button.component'; import { ChartComponent } from './components/chart/chart.component'; import { ConfirmationComponent } from './components/confirmation/confirmation.component'; -import { ErrorComponent } from './components/error/error.component'; +import { HttpErrorWrapperComponent } from './components/http-error-wrapper/http-error-wrapper.component'; import { LoaderBarComponent } from './components/loader-bar/loader-bar.component'; import { ModalComponent } from './components/modal/modal.component'; import { SortOrderIconComponent } from './components/sort-order-icon/sort-order-icon.component'; @@ -42,7 +42,7 @@ export function appendScript(injector: Injector) { ButtonComponent, ChartComponent, ConfirmationComponent, - ErrorComponent, + HttpErrorWrapperComponent, LoaderBarComponent, ModalComponent, TableEmptyMessageComponent, @@ -63,7 +63,7 @@ export function appendScript(injector: Injector) { TableSortDirective, ], providers: [DatePipe], - entryComponents: [ErrorComponent], + entryComponents: [HttpErrorWrapperComponent], }) export class ThemeSharedModule { constructor(private errorHandler: ErrorHandler) {}