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()">

{{activity.title}}

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';