Skip to content

Commit

Permalink
feat: add server configuration pipe and feature toggle pipe
Browse files Browse the repository at this point in the history
Co-authored-by: Stefan Hauke <s.hauke@intershop.de>
Co-authored-by: Danilo Hoffmann <d.hoffmann@intershop.de>
  • Loading branch information
3 people authored Nov 11, 2020
1 parent 55a5755 commit 906a5b4
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 7 deletions.
6 changes: 6 additions & 0 deletions docs/concepts/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ If the feature is deactivated, the user is sent to the error page on accessing.
<ish-product-add-to-compare *ishFeature="'compare'"> ...</ish-product-add-to-compare>
```
**Pipe**
```html
<ish-product-add-to-compare *ngIf="'compare' | ishFeature"> ...</ish-product-add-to-compare>
```
**Service**
```typescript
Expand Down
1 change: 1 addition & 0 deletions src/app/core/directives/feature-toggle.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FeatureToggleService } from 'ish-core/utils/feature-toggle/feature-togg
* Used on an element, this element will only be rendered if the specified feature *is enabled*.
*
* For the negated case see {@link NotFeatureToggleDirective}.
* For the corresponding pipe see {@link FeatureTogglePipe}.
*
* @example
* <div *ishFeature="'quoting'">
Expand Down
13 changes: 9 additions & 4 deletions src/app/core/facades/app.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ import { getServerConfigParameter } from 'ish-core/store/general/server-config';

@Injectable({ providedIn: 'root' })
export class AppFacade {
icmBaseUrl: string;

constructor(private store: Store, private router: Router) {
this.routingInProgress$.subscribe(noop);

store.pipe(select(getICMBaseURL)).subscribe(icmBaseUrl => (this.icmBaseUrl = icmBaseUrl));
}
icmBaseUrl: string;

headerType$ = this.store.pipe(select(getHeaderType));
deviceType$ = this.store.pipe(select(getDeviceType));
Expand All @@ -46,8 +45,6 @@ export class AppFacade {
),
]).pipe(map(classes => classes.filter(c => !!c)));

// COUNTRIES AND REGIONS

countriesLoading$ = this.store.pipe(select(getCountriesLoading));

routingInProgress$ = merge(
Expand Down Expand Up @@ -90,6 +87,14 @@ export class AppFacade {
return isAppTypeREST && !isBusinessCustomer ? 'privatecustomers' : 'customers';
}

/**
* extracts a specific server setting from the store.
* @param path the path to the server setting, starting from the serverConfig/_config store.
*/
serverSetting$<T>(path: string) {
return this.store.pipe(select(getServerConfigParameter<T>(path)));
}

countries$() {
this.store.dispatch(loadCountries());
return this.store.pipe(select(getAllCountries));
Expand Down
4 changes: 4 additions & 0 deletions src/app/core/pipes.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@ import { ModuleWithProviders, NgModule } from '@angular/core';
import { AttributeToStringPipe } from './models/attribute/attribute.pipe';
import { PricePipe } from './models/price/price.pipe';
import { DatePipe } from './pipes/date.pipe';
import { FeatureTogglePipe } from './pipes/feature-toggle.pipe';
import { HighlightPipe } from './pipes/highlight.pipe';
import { MakeHrefPipe } from './pipes/make-href.pipe';
import { SanitizePipe } from './pipes/sanitize.pipe';
import { ServerSettingPipe } from './pipes/server-setting.pipe';
import { CategoryRoutePipe } from './routing/category/category-route.pipe';
import { ProductRoutePipe } from './routing/product/product-route.pipe';

const pipes = [
AttributeToStringPipe,
CategoryRoutePipe,
DatePipe,
FeatureTogglePipe,
HighlightPipe,
MakeHrefPipe,
PricePipe,
ProductRoutePipe,
SanitizePipe,
ServerSettingPipe,
];

@NgModule({
Expand Down
56 changes: 56 additions & 0 deletions src/app/core/pipes/feature-toggle.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { FeatureToggleModule } from 'ish-core/feature-toggle.module';

import { FeatureTogglePipe } from './feature-toggle.pipe';

@Component({
template: `
<div>unrelated</div>
<div *ngIf="'feature1' | ishFeature">content1</div>
<div *ngIf="'feature2' | ishFeature">content2</div>
<div *ngIf="'always' | ishFeature">contentAlways</div>
<div *ngIf="'never' | ishFeature">contentNever</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestComponent {}

describe('Feature Toggle Pipe', () => {
let fixture: ComponentFixture<TestComponent>;
let element: HTMLElement;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [FeatureToggleModule.forTesting('feature1')],
declarations: [FeatureTogglePipe, TestComponent],
});
});

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

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

it('should render content of enabled features', () => {
expect(element.textContent).toContain('content1');
});

it('should not render content of disabled features', () => {
expect(element.textContent).not.toContain('content2');
});

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

it("should never render content for 'never'", () => {
expect(element.textContent).not.toContain('contentNever');
});
});
21 changes: 21 additions & 0 deletions src/app/core/pipes/feature-toggle.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Pipe, PipeTransform } from '@angular/core';

import { FeatureToggleService } from 'ish-core/utils/feature-toggle/feature-toggle.service';

/**
* Pipe
*
* Used on a string, this pipe will only return true if the specified feature *is enabled*.
* For the corresponding directive, see {@link FeatureToggleDirective}.
*
* @example
* <ish-product-add-to-compare *ngIf="'compare' | ishFeature"> ...</ish-product-add-to-compare>
*/
@Pipe({ name: 'ishFeature', pure: true })
export class FeatureTogglePipe implements PipeTransform {
constructor(private featureToggleService: FeatureToggleService) {}

transform(feature: string): boolean {
return this.featureToggleService.enabled(feature);
}
}
70 changes: 70 additions & 0 deletions src/app/core/pipes/server-setting.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Component } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { concat, of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { anything, instance, mock, when } from 'ts-mockito';

import { AppFacade } from 'ish-core/facades/app.facade';

import { ServerSettingPipe } from './server-setting.pipe';

@Component({ template: `<ng-container *ngIf="'service.ABC.runnable' | ishServerSetting">TEST</ng-container>` })
class TestComponent {}

describe('Server Setting Pipe', () => {
let fixture: ComponentFixture<TestComponent>;
let element: HTMLElement;
let appFacade: AppFacade;

beforeEach(async () => {
appFacade = mock(AppFacade);
TestBed.configureTestingModule({
declarations: [ServerSettingPipe, TestComponent],
providers: [{ provide: AppFacade, useFactory: () => instance(appFacade) }],
}).compileComponents();
});

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

it('should render TEST when setting is set', () => {
when(appFacade.serverSetting$('service.ABC.runnable')).thenReturn(of(true));
fixture.detectChanges();

expect(element).toMatchInlineSnapshot(`TEST`);
});

it('should render TEST when setting is set to anything truthy', () => {
when(appFacade.serverSetting$('service.ABC.runnable')).thenReturn(of('ABC'));
fixture.detectChanges();

expect(element).toMatchInlineSnapshot(`TEST`);
});

it('should render nothing when setting is not set', () => {
when(appFacade.serverSetting$(anything())).thenReturn(of(undefined));
fixture.detectChanges();

expect(element).toMatchInlineSnapshot(`N/A`);
});

it('should render nothing when setting is set to sth falsy', () => {
when(appFacade.serverSetting$(anything())).thenReturn(of(''));
fixture.detectChanges();

expect(element).toMatchInlineSnapshot(`N/A`);
});

it('should render TEST when setting is set', fakeAsync(() => {
when(appFacade.serverSetting$('service.ABC.runnable')).thenReturn(concat(of(false), of(true).pipe(delay(1000))));
fixture.detectChanges();

expect(element).toMatchInlineSnapshot(`N/A`);
tick(1000);

fixture.detectChanges();
expect(element).toMatchInlineSnapshot(`TEST`);
}));
});
43 changes: 43 additions & 0 deletions src/app/core/pipes/server-setting.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { OnDestroy, Pipe, PipeTransform } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { AppFacade } from 'ish-core/facades/app.facade';

/**
* Pipe
*
* Used on a string, this pipe will return the corresponding server setting by checking the general/serverConfig store.
* If it is set, the Pipe will return a truthy value.
*
* @example
* <example *ngIf="'services.ABC.runnable' | ishServerSetting"> ...</example>
*/
@Pipe({ name: 'ishServerSetting', pure: false })
export class ServerSettingPipe implements PipeTransform, OnDestroy {
private returnValue: unknown;

private destroy$ = new Subject();
private sub: Subscription;

constructor(private appFacade: AppFacade) {}

transform(path: string) {
if (this.sub) {
return this.returnValue;
} else {
this.sub = this.appFacade
.serverSetting$(path)
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.returnValue = value;
});
return this.returnValue;
}
}

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<ul *ngIf="item.value.children" class="account-navigation list-unstyled">
<ng-container *ngFor="let subItem of item.value.children | keyvalue: unsorted">
<ng-container *ishIsAuthorizedTo="item.value.permission || 'always'">
<li *ishFeature="subItem.value.feature || 'always'">
<li *ngIf="subItem.value.feature || 'always' | ishFeature">
<a [routerLink]="item.key + subItem.key" [attr.data-testing-id]="subItem.value.dataTestingId">{{
subItem.value.localizationKey | translate
}}</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { MockComponent, MockPipe } from 'ng-mocks';
import { of } from 'rxjs';
import { mock, when } from 'ts-mockito';

import { AuthorizationToggleModule } from 'ish-core/authorization-toggle.module';
import { AccountFacade } from 'ish-core/facades/account.facade';
import { FeatureToggleModule } from 'ish-core/feature-toggle.module';
import { FeatureTogglePipe } from 'ish-core/pipes/feature-toggle.pipe';

import { AccountUserInfoComponent } from '../account-user-info/account-user-info.component';

Expand All @@ -22,7 +23,7 @@ describe('Account Navigation Component', () => {
beforeEach(async () => {
accountFacadeMock = mock(AccountFacade);
await TestBed.configureTestingModule({
declarations: [AccountNavigationComponent, MockComponent(AccountUserInfoComponent)],
declarations: [AccountNavigationComponent, MockComponent(AccountUserInfoComponent), MockPipe(FeatureTogglePipe)],
imports: [
AuthorizationToggleModule.forTesting('APP_B2B_MANAGE_USERS'),
FeatureToggleModule.forTesting('quoting', 'orderTemplates'),
Expand Down

0 comments on commit 906a5b4

Please sign in to comment.