Skip to content

Commit

Permalink
feat: add structural not-role-toggle-directive to hide html elements …
Browse files Browse the repository at this point in the history
…if the current user has a certain role
  • Loading branch information
SGrueber authored and shauke committed Jan 29, 2021
1 parent 68a3fc6 commit 24fb46b
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 0 deletions.
76 changes: 76 additions & 0 deletions src/app/core/directives/not-role-toggle.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { RoleToggleModule } from 'ish-core/role-toggle.module';

@Component({
template: `
<div>unrelated</div>
<div *ishHasNotRole="'ROLE1'">content1</div>
<div *ishHasNotRole="'ROLE2'">content2</div>
<div *ishHasNotRole="dynamicRole">content3</div>
`,
// Default change detection for dynamic role test
changeDetection: ChangeDetectionStrategy.Default,
})
class TestComponent {
dynamicRole: string;
}

describe('Not Role Toggle Directive', () => {
let fixture: ComponentFixture<TestComponent>;
let element: HTMLElement;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TestComponent],
imports: [RoleToggleModule.forTesting('ROLE1')],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(TestComponent);
element = fixture.nativeElement;
fixture.detectChanges();
});

it('should always render unrelated content', () => {
expect(element.textContent).toContain('unrelated');
});

it('should not render content if role is given', () => {
expect(element.textContent).not.toContain('content1');
});

it('should render content if the role is not given', () => {
expect(element.textContent).toContain('content2');
});

it('should react on changing roles in store', () => {
RoleToggleModule.switchTestingRoles('ROLE2');
fixture.detectChanges();

expect(element.textContent).toContain('content1');
expect(element.textContent).not.toContain('content2');

RoleToggleModule.switchTestingRoles('ROLE1');
fixture.detectChanges();

expect(element.textContent).not.toContain('content1');
expect(element.textContent).toContain('content2');
});

it('should react on changing roles from input', () => {
expect(element.textContent).toContain('content3');

fixture.componentInstance.dynamicRole = 'ROLE1';
fixture.detectChanges();

expect(element.textContent).not.toContain('content3');

fixture.componentInstance.dynamicRole = 'ROLE2';
fixture.detectChanges();

expect(element.textContent).toContain('content3');
});
});
55 changes: 55 additions & 0 deletions src/app/core/directives/not-role-toggle.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Directive, Input, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
import { ReplaySubject, Subject, Subscription } from 'rxjs';
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';

import { RoleToggleService } from 'ish-core/utils/role-toggle/role-toggle.service';

/**
* Structural directive.
* Used on an element, this element will only be rendered if the logged in user has NOT the specified role.
*
* @example
* <div *ishHasNotRole="'APP_B2B_OCI_USER'">
* Only visible when the current user is not an OCI punchout user.
* </div>
*/
@Directive({
selector: '[ishHasNotRole]',
})
export class NotRoleToggleDirective implements OnDestroy {
private subscription: Subscription;
private enabled$ = new ReplaySubject<boolean>(1);

private destroy$ = new Subject();

constructor(
private templateRef: TemplateRef<unknown>,
private viewContainer: ViewContainerRef,
private roleToggleService: RoleToggleService
) {
this.enabled$.pipe(distinctUntilChanged(), takeUntil(this.destroy$)).subscribe(enabled => {
if (enabled) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
});
}

@Input() set ishHasNotRole(roleId: string) {
// end previous subscription and newly subscribe
if (this.subscription) {
// tslint:disable-next-line: ban
this.subscription.unsubscribe();
}
this.subscription = this.roleToggleService
.hasRole(roleId)
.pipe(takeUntil(this.destroy$))
.subscribe({ next: val => this.enabled$.next(!val) });
}

ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
40 changes: 40 additions & 0 deletions src/app/core/role-toggle.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { ReplaySubject } from 'rxjs';
import { map } from 'rxjs/operators';

import { NotRoleToggleDirective } from './directives/not-role-toggle.directive';
import { whenTruthy } from './utils/operators';
import { RoleToggleService, checkRole } from './utils/role-toggle/role-toggle.service';

@NgModule({
declarations: [NotRoleToggleDirective],
exports: [NotRoleToggleDirective],
})
export class RoleToggleModule {
private static roleIds = new ReplaySubject<string[]>(1);

static forTesting(...roleIds: string[]): ModuleWithProviders<RoleToggleModule> {
RoleToggleModule.switchTestingRoles(...roleIds);
return {
ngModule: RoleToggleModule,
providers: [
{
provide: RoleToggleService,
useValue: {
hasRole: (roleId: string) =>
RoleToggleModule.roleIds.pipe(
whenTruthy(),
map(roles => checkRole(roles, roleId))
),
},
},
],
};
}

static switchTestingRoles(...roleIds: string[]) {
RoleToggleModule.roleIds.next(roleIds);
}
}

export { RoleToggleService } from './utils/role-toggle/role-toggle.service';
39 changes: 39 additions & 0 deletions src/app/core/utils/role-toggle/role-toggle.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { TestBed } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { cold } from 'jest-marbles';

import { CoreStoreModule } from 'ish-core/store/core/core-store.module';
import { loadRolesAndPermissionsSuccess } from 'ish-core/store/customer/authorization';
import { CustomerStoreModule } from 'ish-core/store/customer/customer-store.module';

import { RoleToggleService } from './role-toggle.service';

describe('Role Toggle Service', () => {
let store$: Store;
let roleToggleService: RoleToggleService;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [CoreStoreModule.forTesting(), CustomerStoreModule.forTesting('authorization')],
});

store$ = TestBed.inject(Store);
store$.dispatch(
loadRolesAndPermissionsSuccess({
authorization: { roles: [{ roleId: 'ROLE1', displayName: 'Role 1' }], permissionIDs: [] },
})
);

roleToggleService = TestBed.inject(RoleToggleService);
});

describe('hasRole', () => {
it('should return true if user has the role', () => {
expect(roleToggleService.hasRole('ROLE1')).toBeObservable(cold('a', { a: true }));
});

it("should return false if user doesn't have the role", () => {
expect(roleToggleService.hasRole('ROLE2')).toBeObservable(cold('a', { a: false }));
});
});
});
28 changes: 28 additions & 0 deletions src/app/core/utils/role-toggle/role-toggle.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { getUserRoles } from 'ish-core/store/customer/authorization';
import { whenTruthy } from 'ish-core/utils/operators';

export function checkRole(roleIds: string[], roleId: string): boolean {
return roleIds.includes(roleId);
}

@Injectable({ providedIn: 'root' })
export class RoleToggleService {
private roleIds$: Observable<string[]>;

constructor(store: Store) {
this.roleIds$ = store.pipe(select(getUserRoles)).pipe(map(roles => roles.map(role => role.roleId)));
}

hasRole(roleId: string): Observable<boolean> {
return this.roleIds$.pipe(
// wait for permissions to be loaded
whenTruthy(),
map(roleIds => checkRole(roleIds, roleId))
);
}
}
2 changes: 2 additions & 0 deletions src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { AuthorizationToggleModule } from 'ish-core/authorization-toggle.module'
import { FeatureToggleModule } from 'ish-core/feature-toggle.module';
import { IconModule } from 'ish-core/icon.module';
import { PipesModule } from 'ish-core/pipes.module';
import { RoleToggleModule } from 'ish-core/role-toggle.module';
import { ShellModule } from 'ish-shell/shell.module';

import { AddressFormsSharedModule } from './address-forms/address-forms.module';
Expand Down Expand Up @@ -129,6 +130,7 @@ const importExportModules = [
NgbPopoverModule,
PipesModule,
ReactiveFormsModule,
RoleToggleModule,
RouterModule,
ShellModule,
SwiperModule,
Expand Down
2 changes: 2 additions & 0 deletions src/app/shell/shell.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DirectivesModule } from 'ish-core/directives.module';
import { FeatureToggleModule } from 'ish-core/feature-toggle.module';
import { IconModule } from 'ish-core/icon.module';
import { PipesModule } from 'ish-core/pipes.module';
import { RoleToggleModule } from 'ish-core/role-toggle.module';

import { CaptchaExportsModule } from '../extensions/captcha/exports/captcha-exports.module';
import { OrderTemplatesExportsModule } from '../extensions/order-templates/exports/order-templates-exports.module';
Expand Down Expand Up @@ -40,6 +41,7 @@ const importExportModules = [
OrderTemplatesExportsModule,
QuickorderExportsModule,
QuotingExportsModule,
RoleToggleModule,
TactonExportsModule,
WishlistsExportsModule,
];
Expand Down

0 comments on commit 24fb46b

Please sign in to comment.