Skip to content

Commit

Permalink
feat: Allow initial navigation to a conditionally registered activity
Browse files Browse the repository at this point in the history
Some activities may only be registered if some condition is met, like given a specific query parameter in the URL, or if the user is authorized to see the activity.

Bug: If entering an URL into the browser address bar with the query parameter set, still, initial navigation fails because query parameters are evaluated after checking the route for a valid and registered activity.

Fixes #8
  • Loading branch information
danielwiehl committed Dec 11, 2018
1 parent 5a446fd commit 065f7ce
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,56 @@
*/

import { Injectable, OnDestroy } from '@angular/core';
import { NavigationEnd, Router, UrlSegment } from '@angular/router';
import { NavigationEnd, Router, RouterEvent, UrlSegment } from '@angular/router';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { ACTIVITY_OUTLET_NAME } from '../workbench.constants';
import { WbActivityDirective } from './wb-activity.directive';
import { InternalWorkbenchRouter } from '../routing/workbench-router.service';
import { UrlSegmentGroup } from '@angular/router/src/url_tree';
import { filter, takeUntil } from 'rxjs/operators';

@Injectable()
export class WorkbenchActivityPartService implements OnDestroy {

private _destroy$ = new Subject<void>();
private _activeActivity: WbActivityDirective;
private _activeActivityPath: string | null;

public activities: WbActivityDirective[] = [];

constructor(private _router: Router, private _wbRouter: InternalWorkbenchRouter) {
// Register all primary routes as activity auxiliary routes
const routes = this._wbRouter.createAuxiliaryRoutesFor(ACTIVITY_OUTLET_NAME);
this._wbRouter.replaceRouterConfig([...this._router.config, ...routes]);

// Subscribe for routing events to open/close activity panel
// Compute the active activity when a navigation ends successfully
this._router.events
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntil(this._destroy$)
)
.subscribe(() => {
this._activeActivity = this.parseActivityFromUrl(this._router.url);
.subscribe((event: RouterEvent) => {
this._activeActivityPath = this.parseActivityPathElseNull(event.url);
});
}

/**
* Returns the activity which is currently toggled.
*/
public get activeActivity(): WbActivityDirective {
return this._activeActivity;
public get activeActivity(): WbActivityDirective | null {
if (!this._activeActivityPath) {
return null;
}

return this.activities
.filter(it => it.target === 'activity-panel')
.find(it => it.path === this._activeActivityPath) || null;
}

/**
* Returns true if the specified activity is the active activity.
*/
public isActive(activity: WbActivityDirective): boolean {
return this._activeActivity === activity;
return activity.path === this._activeActivityPath;
}

/**
Expand All @@ -58,8 +68,7 @@ export class WorkbenchActivityPartService implements OnDestroy {
*/
public activateActivity(activity: WbActivityDirective): Promise<boolean> {
if (activity.target === 'activity-panel') {
const commands = (this._activeActivity === activity ? null : activity.commands); // toogle activity
return this._router.navigate([{outlets: {[ACTIVITY_OUTLET_NAME]: commands}}], {
return this._router.navigate([{outlets: {[ACTIVITY_OUTLET_NAME]: this.isActive(activity) ? null : activity.commands}}], {
queryParamsHandling: 'preserve'
});
}
Expand All @@ -73,7 +82,7 @@ export class WorkbenchActivityPartService implements OnDestroy {
this._destroy$.next();
}

private parseActivityFromUrl(url: string): WbActivityDirective {
private parseActivityPathElseNull(url: string): string | null {
const activitySegmentGroup: UrlSegmentGroup = this._router
.parseUrl(url)
.root.children[ACTIVITY_OUTLET_NAME];
Expand All @@ -82,16 +91,6 @@ export class WorkbenchActivityPartService implements OnDestroy {
return null; // no activity selected
}

// Resolve the activity
const activityPath = activitySegmentGroup.segments.map((it: UrlSegment) => it.path).join('/');
const activity = this.activities
.filter(it => it.target === 'activity-panel')
.find(it => it.path === activityPath);

if (!activity) {
throw Error(`Illegal state: unknown activity in URL [${activityPath}]`);
}

return activity;
return activitySegmentGroup.segments.map((it: UrlSegment) => it.path).join('/');
}
}
110 changes: 110 additions & 0 deletions projects/scion/workbench/src/lib/spec/activity.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2018 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

import { async, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
import { Component, NgModule, OnDestroy } from '@angular/core';
import { WorkbenchModule } from '../workbench.module';
import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { advance } from './util/util.spec';
import { expect, jasmineCustomMatchers } from './util/jasmine-custom-matchers.spec';
import { Subject } from 'rxjs/index';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';

describe('Activity part', () => {

beforeEach(async(() => {
jasmine.addMatchers(jasmineCustomMatchers);

TestBed.configureTestingModule({
imports: [AppTestModule]
});

TestBed.get(Router).initialNavigation();
}));

it('does not throw if an unknown activity is given in URL', fakeAsync(inject([Router], (router: Router) => {
router.navigateByUrl('(activity:activity-debug)');

expect((): void => {
const fixture = TestBed.createComponent(AppComponent);
advance(fixture);
}).not.toThrowError();

tick();
})));

it('supports initial navigation with an activity registered conditionally based on the value of a query parameter', fakeAsync(inject([Router], (router: Router) => {
router.navigateByUrl('(activity:activity-debug)?debug=true'); // initial navigation with the `debug` query param set to `true` to register the activity

const fixture = TestBed.createComponent(AppComponent);
advance(fixture);

expect(fixture).toShow(ActivityDebugComponent);

tick();
})));
});

/****************************************************************************************************
* Definition of App Test Module *
****************************************************************************************************/
@Component({
template: `
<wb-workbench style="position: relative; width: 100%; height: 500px">
<wb-activity *ngIf="debug"
cssClass="activity-debug"
label="activity-debug"
routerLink="activity-debug">
</wb-activity>
</wb-workbench>
`
})
class AppComponent implements OnDestroy {

private _destroy$ = new Subject<void>();

public debug: boolean;

constructor(route: ActivatedRoute) {
route.queryParamMap
.pipe(
map((paramMap: ParamMap) => coerceBooleanProperty(paramMap.get('debug'))),
distinctUntilChanged(),
takeUntil(this._destroy$)
)
.subscribe((debug: boolean) => {
this.debug = debug;
});
}

public ngOnDestroy(): void {
this._destroy$.next();
}
}

@Component({template: 'Activity-Debug'})
class ActivityDebugComponent {
}

@NgModule({
imports: [
WorkbenchModule.forRoot(),
NoopAnimationsModule,
RouterTestingModule.withRoutes([
{path: 'activity-debug', component: ActivityDebugComponent},
]),
],
declarations: [AppComponent, ActivityDebugComponent]
})
class AppTestModule {
}

0 comments on commit 065f7ce

Please sign in to comment.