diff --git a/projects/scion/workbench/src/lib/activity-part/activity-part.component.html b/projects/scion/workbench/src/lib/activity-part/activity-part.component.html
index 8070e80a4..72273023e 100644
--- a/projects/scion/workbench/src/lib/activity-part/activity-part.component.html
+++ b/projects/scion/workbench/src/lib/activity-part/activity-part.component.html
@@ -1,28 +1,36 @@
+ [@panel-enter-or-leave]
+ (@panel-enter-or-leave.done)="onPanelAnimationDone()">
diff --git a/projects/scion/workbench/src/lib/activity-part/activity-part.component.ts b/projects/scion/workbench/src/lib/activity-part/activity-part.component.ts
index 6fe6be934..45dcaad84 100644
--- a/projects/scion/workbench/src/lib/activity-part/activity-part.component.ts
+++ b/projects/scion/workbench/src/lib/activity-part/activity-part.component.ts
@@ -8,13 +8,13 @@
* SPDX-License-Identifier: EPL-2.0
*/
-import { Component, ElementRef, ViewChild } from '@angular/core';
+import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';
import { animate, AnimationBuilder, AnimationPlayer, style, transition, trigger } from '@angular/animations';
import { WorkbenchActivityPartService } from './workbench-activity-part.service';
import { WorkbenchLayoutService } from '../workbench-layout.service';
import { noop, Observable, Subject } from 'rxjs';
import { ACTIVITY_OUTLET_NAME, ROUTER_OUTLET_NAME } from '../workbench.constants';
-import { WbActivityDirective } from './wb-activity.directive';
+import { Activity } from './activity';
@Component({
selector: 'wb-activity-part',
@@ -55,7 +55,17 @@ export class ActivityPartComponent {
constructor(public host: ElementRef
,
public activityPartService: WorkbenchActivityPartService,
private _workbenchLayout: WorkbenchLayoutService,
- private _animationBuilder: AnimationBuilder) {
+ private _animationBuilder: AnimationBuilder,
+ private _cd: ChangeDetectorRef) {
+ }
+
+ public get activities(): Activity[] {
+ return this.activityPartService.activities.filter(it => it.visible);
+ }
+
+ public get activeActivity(): Activity {
+ const activeActivity = this.activityPartService.activeActivity;
+ return activeActivity && activeActivity.visible ? activeActivity : null;
}
public set panelWidth(panelWidth: number) {
@@ -71,7 +81,7 @@ export class ActivityPartComponent {
return this._panelWidth$.asObservable();
}
- public onActivate(activity: WbActivityDirective): false {
+ public onActivate(activity: Activity): false {
this.activityPartService.activateActivity(activity).then(noop);
return false; // prevent UA to follow 'href'
}
@@ -93,6 +103,10 @@ export class ActivityPartComponent {
this.panelWidth = ActivityPartComponent.PANEL_INITIAL_WIDTH;
}
+ public onPanelAnimationDone(): void {
+ this._cd.detectChanges();
+ }
+
private ensureMinimalPanelWidth(): void {
if (this.panelWidth >= ActivityPartComponent.PANEL_MIN_WIDTH) {
return;
diff --git a/projects/scion/workbench/src/lib/activity-part/activity.ts b/projects/scion/workbench/src/lib/activity-part/activity.ts
new file mode 100644
index 000000000..ce6cf3d44
--- /dev/null
+++ b/projects/scion/workbench/src/lib/activity-part/activity.ts
@@ -0,0 +1,169 @@
+import { Injector, TemplateRef } from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { ComponentType, PortalInjector } from '@angular/cdk/portal';
+import { Disposable } from '../disposable';
+import { InternalWorkbenchRouter } from '../routing/workbench-router.service';
+
+/**
+ * Represents an activity in the activity panel.
+ *
+ * Register this activity in {WorkbenchActivityPartService} and its route as primary Angular route.
+ */
+export abstract class Activity {
+
+ /**
+ * Specifies the title of the activity.
+ */
+ public title: string;
+
+ /**
+ * Use in combination with an icon font to specify the icon.
+ */
+ public label: string;
+
+ /**
+ * Specifies the CSS class(es) used for the icon, e.g. 'material-icons' when using Angular Material Design.
+ */
+ public cssClass: string | string[];
+
+ /**
+ * Controls whether to open this activity in the activity panel or to open it in a separate view.
+ */
+ public target: 'activity-panel' | 'view';
+
+ /**
+ * Controls whether to show or hide this activity. By default, this activity is showing.
+ */
+ public visible: boolean;
+
+ /**
+ * Specifies where to insert this activity in the list of activities.
+ */
+ public position: number;
+
+ /**
+ * Specifies the number of pixels added to the activity panel width if this is the active activity.
+ */
+ public panelWidthDelta: number;
+
+ /**
+ * Specifies the routing commands used by Angular router to navigate when this activity is activated.
+ * The route must be registered as primary Angular route.
+ */
+ public abstract set routerLink(routerLink: any[] | string);
+
+ /**
+ * Returns the routing commands of this activity.
+ */
+ public abstract get commands(): any[];
+
+ /**
+ * Returns the routing path of this activity.
+ */
+ public abstract get path(): string;
+
+ /**
+ * Emits upon activation change of this activity.
+ */
+ public abstract get active$(): Observable;
+
+ /**
+ * Indicates if this activity is currently active.
+ */
+ public abstract get active(): boolean;
+
+ /**
+ * Returns the actions associated with this activity.
+ */
+ public abstract get actions(): ActivityAction[];
+
+ /**
+ * Associates an action with this activity. When this activity is active, it is displayed in the activity panel header.
+ *
+ * Either provide a template or component to render the action, and optionally an injector to provide data to the component.
+ */
+ public abstract registerAction(action: TemplateRef | ComponentType, injector?: Injector): Disposable;
+}
+
+export class InternalActivity implements Activity {
+
+ private _commands: any[] = [];
+ private _actions: ActivityAction[] = [];
+ private _path: string;
+ private _active$ = new BehaviorSubject(false);
+
+ public panelWidthDelta = 0;
+ public title: string;
+ public label: string;
+ public cssClass: string | string[];
+ public target: 'activity-panel' | 'view' = 'activity-panel';
+ public visible = true;
+ public position: number;
+
+ constructor(private _wbRouter: InternalWorkbenchRouter, private _injector: Injector) {
+ }
+
+ public set routerLink(routerLink: any[] | string) {
+ this._commands = this._wbRouter.normalizeCommands(routerLink ? (Array.isArray(routerLink) ? routerLink : [routerLink]) : []);
+ this._path = this.commands.filter(it => typeof it === 'string').join('/');
+ }
+
+ public get actions(): ActivityAction[] {
+ return this._actions;
+ }
+
+ public get commands(): any[] {
+ return this._commands;
+ }
+
+ public get path(): string {
+ return this._path;
+ }
+
+ public get active$(): Observable {
+ return this._active$.asObservable();
+ }
+
+ public get active(): boolean {
+ return this._active$.getValue();
+ }
+
+ public set active(active: boolean) {
+ this._active$.next(active);
+ }
+
+ public registerAction(templateOrComponent: TemplateRef | ComponentType, injector?: Injector): Disposable {
+ const action = ((): ActivityAction => {
+ if (templateOrComponent instanceof TemplateRef) {
+ return {template: templateOrComponent, injector: injector};
+ } else {
+ const injectionTokens = new WeakMap();
+ injectionTokens.set(Activity, this);
+ const portalInjector = new PortalInjector(injector || this._injector, injectionTokens);
+ return {component: templateOrComponent, injector: portalInjector};
+ }
+ })();
+ this._actions.push(action);
+
+ return {
+ dispose: (): void => {
+ const index = this._actions.indexOf(action);
+ if (index === -1) {
+ throw Error('[IllegalStateError] Action not registered in activity');
+ }
+ this._actions.splice(index, 1);
+ }
+ };
+ }
+}
+
+/**
+ * Action to be added to an activity.
+ *
+ * Specify either a template or a component to render the action.
+ */
+export interface ActivityAction {
+ template?: TemplateRef;
+ component?: ComponentType;
+ injector?: Injector;
+}
diff --git a/projects/scion/workbench/src/lib/activity-part/wb-activity-action.directive.ts b/projects/scion/workbench/src/lib/activity-part/wb-activity-action.directive.ts
index 664612f04..e59d5743c 100644
--- a/projects/scion/workbench/src/lib/activity-part/wb-activity-action.directive.ts
+++ b/projects/scion/workbench/src/lib/activity-part/wb-activity-action.directive.ts
@@ -8,9 +8,11 @@
* SPDX-License-Identifier: EPL-2.0
*/
-import { Directive, OnDestroy, Optional, TemplateRef } from '@angular/core';
-import { WbActivityDirective } from './wb-activity.directive';
+import { Directive, OnDestroy, TemplateRef } from '@angular/core';
import { WorkbenchActivityPartService } from './workbench-activity-part.service';
+import { Activity } from './activity';
+import { ActivatedRoute } from '@angular/router';
+import { Disposable } from '../disposable';
/**
* Use this directive to model an action for an activity, which is displayed
@@ -32,19 +34,21 @@ import { WorkbenchActivityPartService } from './workbench-activity-part.service'
})
export class WbActivityActionDirective implements OnDestroy {
- private readonly _activity: WbActivityDirective;
+ private readonly _activity: Activity;
+ private readonly _action: Disposable;
- constructor(@Optional() private _template: TemplateRef,
- activityService: WorkbenchActivityPartService) {
- if (!this._template) {
- throw Error('Illegal usage: Host element of this modelling directive must be a ');
+ constructor(private _template: TemplateRef,
+ activityService: WorkbenchActivityPartService,
+ route: ActivatedRoute) {
+ this._activity = activityService.getActivityFromRoutingContext(route.snapshot);
+ if (!this._activity) {
+ throw Error('[RoutingContextError] Route not in the context of an activity');
}
- this._activity = activityService.activeActivity;
- this._activity.registerAction(this._template);
+ this._action = this._activity.registerAction(this._template);
}
public ngOnDestroy(): void {
- this._activity.unregisterAction(this._template);
+ this._action.dispose();
}
}
diff --git a/projects/scion/workbench/src/lib/activity-part/wb-activity.directive.ts b/projects/scion/workbench/src/lib/activity-part/wb-activity.directive.ts
index 51aa924c2..b1871f969 100644
--- a/projects/scion/workbench/src/lib/activity-part/wb-activity.directive.ts
+++ b/projects/scion/workbench/src/lib/activity-part/wb-activity.directive.ts
@@ -8,8 +8,9 @@
* SPDX-License-Identifier: EPL-2.0
*/
-import { Directive, Input, OnChanges, SimpleChanges, TemplateRef } from '@angular/core';
-import { InternalWorkbenchRouter } from '../routing/workbench-router.service';
+import { Directive, Input, OnDestroy, OnInit } from '@angular/core';
+import { Activity } from './activity';
+import { WorkbenchActivityPartService } from './workbench-activity-part.service';
/**
* Use this directive to model an activity as content child of .
@@ -28,38 +29,33 @@ import { InternalWorkbenchRouter } from '../routing/workbench-router.service';
*
*/
@Directive({selector: 'wb-activity', exportAs: 'activity'}) // tslint:disable-line:directive-selector
-export class WbActivityDirective implements OnChanges {
+export class WbActivityDirective implements OnInit, OnDestroy {
- private _commands: any[] = [];
- private _path: string;
-
- /**
- * Actions of this activity.
- */
- public actions: TemplateRef[] = [];
-
- /**
- * Number of pixels added to the activity panel width if this is the active activity.
- */
- public panelWidthDelta = 0;
+ public readonly activity: Activity;
/**
* Specifies the title of the activity.
*/
@Input()
- public title: string;
+ public set title(title: string) {
+ this.activity.title = title;
+ }
/**
* Use in combination with an icon font to specify the icon.
*/
@Input()
- public label: string;
+ public set label(label: string) {
+ this.activity.label = label;
+ }
/**
* Specifies the CSS class(es) used for the icon, e.g. 'material-icons' when using Angular Material Design.
*/
@Input()
- public cssClass: string | string[];
+ public set cssClass(cssClass: string | string[]) {
+ this.activity.cssClass = cssClass;
+ }
/**
* Specifies the routing commands used by Angular router to navigate when this activity is clicked.
@@ -72,47 +68,45 @@ export class WbActivityDirective implements OnChanges {
* @see Router
*/
@Input()
- public routerLink: any[] | string;
+ public set routerLink(routerLink: any[] | string) {
+ this.activity.routerLink = routerLink;
+ }
/**
- * Controls where to open the resolved component:
- *
- * - activity-panel: component is opened in the activity panel (default)
- * - view: component is displayed as a view
+ * Controls whether to open this activity in the activity panel or to open it in a separate view.
*/
@Input()
- public target: 'activity-panel' | 'view' = 'activity-panel';
-
- constructor(private _wbRouter: InternalWorkbenchRouter) {
+ public set target(target: 'activity-panel' | 'view') {
+ this.activity.target = target;
}
- public registerAction(action: TemplateRef): void {
- this.actions.push(action);
+ /**
+ * Controls whether to show or hide this activity. By default, this activity is showing.
+ *
+ * Use over *ngIf directive to show or hide this activity based on a conditional.
+ */
+ @Input()
+ public set visible(visible: boolean) {
+ this.activity.visible = visible;
}
- public unregisterAction(action: TemplateRef): void {
- const index = this.actions.indexOf(action);
- if (index === -1) {
- throw Error('Illegal argument: action not contained');
- }
- this.actions.splice(index, 1);
+ /**
+ * Specifies where to insert this activity in the list of activities.
+ */
+ @Input()
+ public set position(insertionOrder: number) {
+ this.activity.position = insertionOrder;
}
- public get path(): string {
- return this._path;
+ constructor(private _activityPartService: WorkbenchActivityPartService) {
+ this.activity = this._activityPartService.createActivity();
}
- public get commands(): any[] {
- return this._commands;
+ public ngOnInit(): void {
+ this._activityPartService.addActivity(this.activity);
}
- public ngOnChanges(changes: SimpleChanges): void {
- if (!changes.routerLink) {
- return;
- }
-
- const commands = this.routerLink;
- this._commands = this._wbRouter.normalizeCommands(commands ? (Array.isArray(commands) ? commands : [commands]) : []);
- this._path = this.commands.filter(it => typeof it === 'string').join('/');
+ public ngOnDestroy(): void {
+ this._activityPartService.removeActivity(this.activity);
}
}
diff --git a/projects/scion/workbench/src/lib/activity-part/workbench-activity-part.service.ts b/projects/scion/workbench/src/lib/activity-part/workbench-activity-part.service.ts
index 793368996..3f95e7a35 100644
--- a/projects/scion/workbench/src/lib/activity-part/workbench-activity-part.service.ts
+++ b/projects/scion/workbench/src/lib/activity-part/workbench-activity-part.service.ts
@@ -8,64 +8,41 @@
* SPDX-License-Identifier: EPL-2.0
*/
-import { Injectable, OnDestroy } from '@angular/core';
-import { NavigationCancel, NavigationError, Router, RouterEvent, RoutesRecognized, UrlSegment } from '@angular/router';
-import { Subject } from 'rxjs';
-import { ACTIVITY_OUTLET_NAME } from '../workbench.constants';
-import { WbActivityDirective } from './wb-activity.directive';
+import { Injectable, Injector } from '@angular/core';
+import { ActivatedRouteSnapshot, Router } from '@angular/router';
+import { ACTIVITY_DATA_KEY, ACTIVITY_OUTLET_NAME } from '../workbench.constants';
import { InternalWorkbenchRouter } from '../routing/workbench-router.service';
-import { UrlSegmentGroup } from '@angular/router/src/url_tree';
-import { takeUntil } from 'rxjs/operators';
+import { Activity, InternalActivity } from './activity';
@Injectable()
-export class WorkbenchActivityPartService implements OnDestroy {
+export class WorkbenchActivityPartService {
- private _destroy$ = new Subject();
- private _activeActivityPath: string | null;
+ private _activities: Activity[] = [];
- 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]);
-
- // Compute the active activity when a navigation ends successfully
- this._router.events
- .pipe(takeUntil(this._destroy$))
- .subscribe((event: RouterEvent) => {
- switch (event.constructor) {
- case RoutesRecognized:
- // set activity prior to route activation to be available in activity construction, e.g. to contribute activity actions
- this._activeActivityPath = this.parseActivityPathElseNull(event.url);
- break;
- case NavigationCancel:
- case NavigationError:
- // unset activity if navigation fails or was cancelled
- this._activeActivityPath = this.parseActivityPathElseNull(this._router.url);
- break;
- }
- });
+ constructor(private _router: Router,
+ private _wbRouter: InternalWorkbenchRouter,
+ private _injector: Injector) {
}
/**
- * Returns the activity which is currently toggled.
+ * Returns the list of activities.
*/
- public get activeActivity(): WbActivityDirective | null {
- if (!this._activeActivityPath) {
- return null;
- }
+ public get activities(): Activity[] {
+ return this._activities;
+ }
- return this.activities
- .filter(it => it.target === 'activity-panel')
- .find(it => it.path === this._activeActivityPath) || null;
+ /**
+ * Creates an activity to be added to this service.
+ */
+ public createActivity(): Activity {
+ return new InternalActivity(this._wbRouter, this._injector);
}
/**
- * Returns true if the specified activity is the active activity.
+ * Returns the activity which is currently active, or `null` otherwise.
*/
- public isActive(activity: WbActivityDirective): boolean {
- return activity.path === this._activeActivityPath;
+ public get activeActivity(): Activity | null {
+ return this._activities.find(it => it.active) || null;
}
/**
@@ -73,31 +50,47 @@ export class WorkbenchActivityPartService implements OnDestroy {
*
* Note: This instruction runs asynchronously via URL routing.
*/
- public activateActivity(activity: WbActivityDirective): Promise {
+ public activateActivity(activity: Activity): Promise {
if (activity.target === 'activity-panel') {
- return this._router.navigate([{outlets: {[ACTIVITY_OUTLET_NAME]: this.isActive(activity) ? null : activity.commands}}], {
+ return this._router.navigate([{outlets: {[ACTIVITY_OUTLET_NAME]: activity.active ? null : activity.commands}}], {
queryParamsHandling: 'preserve'
});
- }
- else if (activity.target === 'view') {
+ } else if (activity.target === 'view') {
return this._wbRouter.navigate(activity.commands);
}
- throw Error('Illegal activity target; must be \'activity-panel\' or \'view\'');
+ throw Error('[IllegalActivityTargetError] Target must be \'activity-panel\' or \'view\'');
}
- public ngOnDestroy(): void {
- this._destroy$.next();
+ /**
+ * Adds an activity to the activity bar.
+ */
+ public addActivity(activity: Activity): void {
+ this._activities.push(activity);
+ this._activities.sort((a1, a2) => (a1.position || 0) - (a2.position || 0));
}
- private parseActivityPathElseNull(url: string): string | null {
- const activitySegmentGroup: UrlSegmentGroup = this._router
- .parseUrl(url)
- .root.children[ACTIVITY_OUTLET_NAME];
+ /**
+ * Removes an activity from the activity bar.
+ */
+ public removeActivity(activity: Activity): void {
+ const index = this._activities.findIndex(it => it === activity);
+ if (index === -1) {
+ throw Error('[IllegalStateError] Activity not registered');
+ }
+ this._activities.splice(index, 1);
+ }
- if (!activitySegmentGroup) {
- return null; // no activity selected
+ /**
+ * Returns the activity of the current routing context, or `null` if not in the routing context of an activity.
+ */
+ public getActivityFromRoutingContext(route: ActivatedRouteSnapshot): Activity {
+ for (let testee = route; testee !== null; testee = testee.parent) {
+ const activity = testee.data[ACTIVITY_DATA_KEY];
+ if (activity) {
+ return activity;
+ }
}
- return activitySegmentGroup.segments.map((it: UrlSegment) => it.path).join('/');
+ return null;
}
}
diff --git a/projects/scion/workbench/src/lib/disposable.ts b/projects/scion/workbench/src/lib/disposable.ts
new file mode 100644
index 000000000..f7a84ded1
--- /dev/null
+++ b/projects/scion/workbench/src/lib/disposable.ts
@@ -0,0 +1,6 @@
+/**
+ * To dispose a resource.
+ */
+export interface Disposable {
+ dispose(): void;
+}
diff --git a/projects/scion/workbench/src/lib/routing/activity.resolver.ts b/projects/scion/workbench/src/lib/routing/activity.resolver.ts
new file mode 100644
index 000000000..f45ecce54
--- /dev/null
+++ b/projects/scion/workbench/src/lib/routing/activity.resolver.ts
@@ -0,0 +1,66 @@
+import { Injectable, OnDestroy } from '@angular/core';
+import { ActivatedRouteSnapshot, NavigationEnd, Resolve, Router, RouterStateSnapshot, UrlSegment } from '@angular/router';
+import { Activity, InternalActivity } from '../activity-part/activity';
+import { Subject } from 'rxjs';
+import { WorkbenchActivityPartService } from '../activity-part/workbench-activity-part.service';
+import { ACTIVITY_OUTLET_NAME } from '../workbench.constants';
+import { filter, takeUntil } from 'rxjs/operators';
+
+/**
+ * Resolves the {Activity} of an activity route and makes it available to the routing context.
+ */
+@Injectable()
+export class ActivityResolver implements Resolve, OnDestroy {
+
+ private _destroy$ = new Subject();
+
+ constructor(private _activityPartService: WorkbenchActivityPartService, private _router: Router) {
+ this.listenForActivityDeactivation();
+ }
+
+ public resolve(activityRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Activity {
+ // Read activity auxiliary route
+ const activitySegmentGroup = this._router.parseUrl(state.url).root.children[ACTIVITY_OUTLET_NAME];
+ if (!activitySegmentGroup) {
+ throw Error('[ActivityContextError] Route not in the context of an activity');
+ }
+
+ // Resolve the activity
+ const activityPath = activitySegmentGroup.segments.map((it: UrlSegment) => it.path).join('/');
+ const activity = this._activityPartService.activities.find(it => it.path === activityPath);
+ if (!activity) {
+ throw Error(`[NullActivityError] Cannot find activity that matches the path '${activityPath}'. Did you provide it as '' or register it in 'WorkbenchActivityPartService'?`);
+ }
+
+ this.activateActivity(activity);
+ return activity;
+ }
+
+ public ngOnDestroy(): void {
+ this._destroy$.next();
+ }
+
+ private activateActivity(activity: Activity): void {
+ this.deactivateActivity();
+ (activity as InternalActivity).active = true;
+ }
+
+ private deactivateActivity(): void {
+ const activeActivity = this._activityPartService.activeActivity;
+ if (activeActivity) {
+ (activeActivity as InternalActivity).active = false;
+ }
+ }
+
+ private listenForActivityDeactivation(): void {
+ this._router.events
+ .pipe(
+ filter(event => event instanceof NavigationEnd),
+ filter(() => !this._router.routerState.snapshot.root.children.some(it => it.outlet === ACTIVITY_OUTLET_NAME)),
+ takeUntil(this._destroy$)
+ )
+ .subscribe(() => {
+ this.deactivateActivity();
+ });
+ }
+}
diff --git a/projects/scion/workbench/src/lib/routing/workbench-auxiliary-routes-registrator.service.ts b/projects/scion/workbench/src/lib/routing/workbench-auxiliary-routes-registrator.service.ts
new file mode 100644
index 000000000..0861ee5bb
--- /dev/null
+++ b/projects/scion/workbench/src/lib/routing/workbench-auxiliary-routes-registrator.service.ts
@@ -0,0 +1,69 @@
+import { Injectable } from '@angular/core';
+import { Data, PRIMARY_OUTLET, Router, Routes } from '@angular/router';
+import { ActivityResolver } from './activity.resolver';
+import { ACTIVITY_DATA_KEY, ACTIVITY_OUTLET_NAME } from '../workbench.constants';
+import { EmptyOutletComponent } from './empty-outlet.component';
+import { ResolveData } from '@angular/router/src/config';
+
+/**
+ * Registers auxiliary routes for views and activities.
+ */
+@Injectable()
+export class WorkbenchAuxiliaryRoutesRegistrator {
+
+ constructor(private _router: Router) {
+ }
+
+ /**
+ * Registers a named activity auxiliary route for every primary route found in the router config.
+ */
+ public registerActivityAuxiliaryRoutes(): Routes {
+ return this.registerAuxiliaryRoutesFor(ACTIVITY_OUTLET_NAME, {resolve: {[ACTIVITY_DATA_KEY]: ActivityResolver}});
+ }
+
+ /**
+ * Registers a named auxiliary route for every primary route found in the router config.
+ * This allows all primary routes to be used in a named router outlet of the given outlet name.
+ *
+ * @param outlet for which to create named auxiliary routes
+ * @param params optional parametrization of the auxilary route
+ */
+ private registerAuxiliaryRoutesFor(outlet: string, params: AuxiliaryRouteParams = {}): Routes {
+ const primaryRoutes = this._router.config.filter(route => !route.outlet || route.outlet === PRIMARY_OUTLET);
+
+ const auxRoutes: Routes = primaryRoutes.map(it => {
+ return {
+ ...it,
+ outlet: outlet,
+ component: it.component || EmptyOutletComponent, // used for lazy loading of aux routes; see Angular PR #23459
+ canDeactivate: [...(it.canDeactivate || []), ...(params.canDeactivate || [])],
+ data: {...it.data, ...params.data},
+ resolve: {...it.resolve, ...params.resolve},
+ };
+ });
+
+ this.replaceRouterConfig([
+ ...this._router.config.filter(route => route.outlet !== outlet), // all registered routes, except auxiliary routes of the outlet
+ ...auxRoutes
+ ]);
+
+ return auxRoutes;
+ }
+
+ /**
+ * Replaces the router configuration to install or uninstall auxiliary routes.
+ */
+ public replaceRouterConfig(config: Routes): void {
+ // Note: Do not use Router.resetConfig(...) which would destroy any currently routed component because copying all routes.
+ this._router.config = config;
+ }
+}
+
+/**
+ * Controls creation of auxiliary routes for named router outlets.
+ */
+interface AuxiliaryRouteParams {
+ canDeactivate?: any[];
+ data?: Data;
+ resolve?: ResolveData;
+}
diff --git a/projects/scion/workbench/src/lib/spec/activity-guard.spec.ts b/projects/scion/workbench/src/lib/spec/activity-guard.spec.ts
index 04c9590e7..e91846651 100644
--- a/projects/scion/workbench/src/lib/spec/activity-guard.spec.ts
+++ b/projects/scion/workbench/src/lib/spec/activity-guard.spec.ts
@@ -19,7 +19,7 @@ import { expect, jasmineCustomMatchers } from './util/jasmine-custom-matchers.sp
import { ActivityPartComponent } from '../activity-part/activity-part.component';
import { By } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
-import { Observable } from 'rxjs/index';
+import { Observable } from 'rxjs';
/**
*
diff --git a/projects/scion/workbench/src/lib/spec/activity.spec.ts b/projects/scion/workbench/src/lib/spec/activity.spec.ts
index c76590594..f383dd6de 100644
--- a/projects/scion/workbench/src/lib/spec/activity.spec.ts
+++ b/projects/scion/workbench/src/lib/spec/activity.spec.ts
@@ -16,7 +16,7 @@ import { ActivatedRoute, ParamMap, Router, RouteReuseStrategy } from '@angular/r
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { advance, clickElement } from './util/util.spec';
import { expect, jasmineCustomMatchers } from './util/jasmine-custom-matchers.spec';
-import { Subject } from 'rxjs/index';
+import { Subject } from 'rxjs';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
import { ActivityPartComponent } from '../activity-part/activity-part.component';
@@ -29,27 +29,24 @@ describe('Activity part', () => {
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)');
-
+ it('does not throw if a hidden activity is given in URL', fakeAsync(inject([Router], (router: Router) => {
expect((): void => {
const fixture = TestBed.createComponent(AppComponent);
advance(fixture);
+ router.navigateByUrl('(activity:activity-debug)');
+ 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);
-
+ router.navigateByUrl('(activity:activity-debug)?debug=true'); // initial navigation with the `debug` query param set to `true` to show the activity
+ advance(fixture);
expect(fixture).toShow(ActivityDebugComponent);
tick();
@@ -96,9 +93,9 @@ describe('Activity part', () => {
@Component({
template: `
-
();
+export class WorkbenchComponent implements AfterViewInit {
@ViewChild('overlay_host', {read: ViewContainerRef})
public host: ViewContainerRef;
- @ContentChildren(WbActivityDirective)
- public set activities(queryList: QueryList) {
- queryList.changes
- .pipe(takeUntil(this._destroy$))
- .subscribe((activities: QueryList) => {
- this._activityPartService.activities = activities.toArray();
- });
- }
-
@HostBinding('class.maximized')
public get maximized(): boolean {
return this._workbenchLayout.maximized;
@@ -49,7 +34,6 @@ export class WorkbenchComponent implements AfterViewInit, OnDestroy {
}
constructor(private _workbenchLayout: WorkbenchLayoutService,
- private _activityPartService: WorkbenchActivityPartService,
private _overlayHostRef: OverlayHostRef,
private _messageBoxService: MessageBoxService) {
}
@@ -57,8 +41,4 @@ export class WorkbenchComponent implements AfterViewInit, OnDestroy {
public ngAfterViewInit(): void {
this._overlayHostRef.set(this.host);
}
-
- public ngOnDestroy(): void {
- this._destroy$.next();
- }
}
diff --git a/projects/scion/workbench/src/lib/workbench.constants.ts b/projects/scion/workbench/src/lib/workbench.constants.ts
index f08dd1688..4b409d27e 100644
--- a/projects/scion/workbench/src/lib/workbench.constants.ts
+++ b/projects/scion/workbench/src/lib/workbench.constants.ts
@@ -35,6 +35,11 @@ export const WORKBENCH_FORROOT_GUARD = new InjectionToken('WORKBENCH_FORRO
*/
export const ACTIVITY_OUTLET_NAME = 'activity';
+/**
+ * Represents the key which the activity is associated in data params.
+ */
+export const ACTIVITY_DATA_KEY = 'wb.activity';
+
/**
* Specifies the drag type to move views.
*/
diff --git a/projects/scion/workbench/src/lib/workbench.module.ts b/projects/scion/workbench/src/lib/workbench.module.ts
index 192bc3292..e909e27ab 100644
--- a/projects/scion/workbench/src/lib/workbench.module.ts
+++ b/projects/scion/workbench/src/lib/workbench.module.ts
@@ -59,6 +59,8 @@ import { WbActivityRouteReuseProvider } from './routing/wb-activity-route-reuse-
import { WbRouteReuseStrategy } from './routing/wb-route-reuse-strategy.service';
import { SciViewportModule } from './ui/viewport/viewport.module';
import { SciDimensionModule } from './ui/dimension/dimension.module';
+import { ActivityResolver } from './routing/activity.resolver';
+import { WorkbenchAuxiliaryRoutesRegistrator } from './routing/workbench-auxiliary-routes-registrator.service';
const CONFIG = new InjectionToken('WORKBENCH_CONFIG');
@@ -113,7 +115,9 @@ const CONFIG = new InjectionToken('WORKBENCH_CONFIG');
export class WorkbenchModule {
// Note: We are injecting ViewRegistrySynchronizer so it gets created eagerly...
- constructor(@Optional() @Inject(WORKBENCH_FORROOT_GUARD) guard: any, viewRegistrySynchronizer: ViewRegistrySynchronizer) {
+ constructor(@Optional() @Inject(WORKBENCH_FORROOT_GUARD) guard: any, viewRegistrySynchronizer: ViewRegistrySynchronizer, auxRoutesRegistrator: WorkbenchAuxiliaryRoutesRegistrator) {
+ // Register an activity auxiliary route for every primary route
+ auxRoutesRegistrator.registerActivityAuxiliaryRoutes();
}
/**
@@ -145,6 +149,8 @@ export class WorkbenchModule {
WorkbenchService,
WorkbenchLayoutService,
WorkbenchActivityPartService,
+ WorkbenchAuxiliaryRoutesRegistrator,
+ ActivityResolver,
NotificationService,
MessageBoxService,
ViewPartGridSerializerService,
diff --git a/projects/scion/workbench/src/public_api.ts b/projects/scion/workbench/src/public_api.ts
index 8892ac0ff..00e173582 100644
--- a/projects/scion/workbench/src/public_api.ts
+++ b/projects/scion/workbench/src/public_api.ts
@@ -14,7 +14,8 @@
export { WorkbenchModule } from './lib/workbench.module';
export { WorkbenchView, WorkbenchViewPart, WbBeforeDestroy } from './lib/workbench.model';
export { WbRouterLinkDirective, WbRouterLinkWithHrefDirective } from './lib/routing/wb-router-link.directive';
-export { WorkbenchRouter } from './lib/routing/workbench-router.service';
+export { WorkbenchRouter, WbNavigationExtras } from './lib/routing/workbench-router.service';
+export { WorkbenchAuxiliaryRoutesRegistrator } from './lib/routing/workbench-auxiliary-routes-registrator.service';
export { WorkbenchComponent } from './lib/workbench.component';
export { SciViewportModule } from './lib/ui/viewport/viewport.module';
export { SciViewportComponent } from './lib/ui/viewport/viewport.component';
@@ -23,6 +24,7 @@ export { SciDimensionDirective, SciDimension } from './lib/ui/dimension/dimensio
export { RemoteSiteComponent } from './lib/remote-site/remote-site.component';
export { OverlayHostRef } from './lib/overlay-host-ref.service';
export { WbActivityDirective } from './lib/activity-part/wb-activity.directive';
+export { Activity } from './lib/activity-part/activity';
export { ActivityPartComponent } from './lib/activity-part/activity-part.component';
export { WorkbenchActivityPartService } from './lib/activity-part/workbench-activity-part.service';
export { WbActivityActionDirective } from './lib/activity-part/wb-activity-action.directive';
@@ -33,3 +35,4 @@ export { MessageBoxService } from './lib/message-box/message-box.service';
export { WB_REMOTE_URL_PARAM, WB_VIEW_TITLE_PARAM, WB_VIEW_HEADING_PARAM } from './lib/routing/routing-params.constants';
export { Severity, NLS_DEFAULTS, ROUTE_REUSE_PROVIDER } from './lib/workbench.constants';
export { WbRouteReuseProvider, WbRouteReuseStrategy } from './lib/routing/wb-route-reuse-strategy.service';
+export { Disposable } from './lib/disposable';