From 2ee9df378b2ad8d59a13308a92975dbec7229b9a Mon Sep 17 00:00:00 2001 From: Daniel Wiehl Date: Mon, 19 Aug 2019 23:25:42 +0200 Subject: [PATCH] feat: allow dragging views to app instances running in different browser tabs or windows closes: #168 --- .../lib/spec/view-part-grid.component.spec.ts | 22 +++- .../src/lib/view-dnd/view-drag.service.ts | 3 + .../lib/view-dnd/view-drop-zone.directive.ts | 49 +++++++-- .../view-tab-drag-image-renderer.service.ts | 3 + .../view-part-grid.component.ts | 103 ++++++++++++++---- .../view-part-bar/view-part-bar.component.ts | 1 + .../lib/view-part/view-part.component.html | 2 +- .../src/lib/view-part/view-part.component.ts | 7 +- .../view-part/view-tab/view-tab.component.ts | 1 + .../lib/workbench-view-registry.service.ts | 2 +- .../workbench/src/lib/workbench.model.ts | 23 +++- 11 files changed, 182 insertions(+), 34 deletions(-) diff --git a/projects/scion/workbench/src/lib/spec/view-part-grid.component.spec.ts b/projects/scion/workbench/src/lib/spec/view-part-grid.component.spec.ts index 8f12f6c6c..e74adda73 100644 --- a/projects/scion/workbench/src/lib/spec/view-part-grid.component.spec.ts +++ b/projects/scion/workbench/src/lib/spec/view-part-grid.component.spec.ts @@ -12,7 +12,7 @@ import { async, fakeAsync, TestBed } from '@angular/core/testing'; import { expect, jasmineCustomMatchers } from './util/jasmine-custom-matchers.spec'; import { AbstractType, Component, InjectionToken, NgModule, Type } from '@angular/core'; import { ViewPartGridComponent } from '../view-part-grid/view-part-grid.component'; -import { Router } from '@angular/router'; +import { Router, UrlSegment } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { WorkbenchViewPartRegistry } from '../view-part-grid/workbench-view-part-registry.service'; import { WorkbenchRouter } from '../routing/workbench-router.service'; @@ -58,6 +58,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -102,6 +103,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -146,6 +148,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -190,6 +193,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -234,6 +238,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -276,6 +281,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.3', + viewUrlSegments: [new UrlSegment('view-3', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -291,6 +297,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -317,6 +324,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.1', + viewUrlSegments: [new UrlSegment('view-1', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -356,6 +364,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -380,6 +389,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.2', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -423,6 +433,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -447,6 +458,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.2', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -490,6 +502,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -514,6 +527,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.2', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -557,6 +571,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -581,6 +596,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.2', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -632,6 +648,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.3', + viewUrlSegments: [new UrlSegment('view-3', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -660,6 +677,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.1', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -691,6 +709,7 @@ describe('ViewPartGridComponent', () => { appInstanceId: workbench.appInstanceId, viewPartRef: 'viewpart.3', viewRef: 'view.2', + viewUrlSegments: [new UrlSegment('view-2', {})], }, target: { appInstanceId: workbench.appInstanceId, @@ -803,3 +822,4 @@ class AppTestModule { function getService(token: Type | AbstractType | InjectionToken): T { return TestBed.get(token as Type); } + diff --git a/projects/scion/workbench/src/lib/view-dnd/view-drag.service.ts b/projects/scion/workbench/src/lib/view-dnd/view-drag.service.ts index 9c852bb11..ece05024e 100644 --- a/projects/scion/workbench/src/lib/view-dnd/view-drag.service.ts +++ b/projects/scion/workbench/src/lib/view-dnd/view-drag.service.ts @@ -14,6 +14,7 @@ import { coerceArray } from '@angular/cdk/coercion'; import { filter, take, takeUntil } from 'rxjs/operators'; import { BroadcastChannelService } from '../broadcast-channel.service'; import { Defined } from '../defined.util'; +import { UrlSegment } from '@angular/router'; /** * Events fired during view drag and drop operation. @@ -211,6 +212,7 @@ export interface ViewDragData { viewTabPointerOffsetY: number; viewRef: string; viewTitle: string; + viewUrlSegments: UrlSegment[]; viewHeading: string; viewClosable: boolean; viewDirty: boolean; @@ -227,6 +229,7 @@ export interface ViewMoveEvent { source: { viewRef: string; viewPartRef: string; + viewUrlSegments: UrlSegment[], appInstanceId: string; }; target: { diff --git a/projects/scion/workbench/src/lib/view-dnd/view-drop-zone.directive.ts b/projects/scion/workbench/src/lib/view-dnd/view-drop-zone.directive.ts index b791b0c2b..75e29756d 100644 --- a/projects/scion/workbench/src/lib/view-dnd/view-drop-zone.directive.ts +++ b/projects/scion/workbench/src/lib/view-dnd/view-drop-zone.directive.ts @@ -8,8 +8,8 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; -import { Subject } from 'rxjs'; +import { Directive, ElementRef, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core'; +import { asapScheduler, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { createElement, ElementCreateOptions, setStyle } from '../dom.util'; import { ViewDragData, ViewDragService } from './view-drag.service'; @@ -22,6 +22,9 @@ const NULL_BOUNDS: Bounds = null; /** * Adds a view drop zone to the host element allowing the view to be dropped either in the north, * east, south, west or in the center. + * + * The drop zone is aligned with the target's bounds, thus requires the element to define a positioning context. + * If not positioned, the element is changed to be positioned relative. */ @Directive({ selector: '[wbViewDropZone]', @@ -37,6 +40,12 @@ export class ViewDropZoneDirective implements OnInit, OnDestroy { private _dropRegion: Region; + /** + * Specifies which drop regions to allow. If not specified, all regions are allowed. + */ + @Input() + public wbViewDropZoneRegions: Region[]; + /** * Emits upon a view drop action. */ @@ -45,6 +54,9 @@ export class ViewDropZoneDirective implements OnInit, OnDestroy { constructor(host: ElementRef, private _viewDragService: ViewDragService, private _zone: NgZone) { this._host = host.nativeElement; + + // Ensure the host element to define a positioning context (after element creation) + asapScheduler.schedule(() => ensureHostElementPositioned(this._host)); } public ngOnInit(): void { @@ -54,9 +66,14 @@ export class ViewDropZoneDirective implements OnInit, OnDestroy { private onDragOver(event: DragEvent): void { NgZone.assertNotInAngularZone(); - event.preventDefault(); // allow view drop const dropRegion = this.computeDropRegion(event); + if (dropRegion === undefined) { + this.renderDropRegions(NULL_BOUNDS, NULL_BOUNDS); + return; + } + + event.preventDefault(); // allow view drop if (dropRegion === this._dropRegion) { return; // drop region did not change } @@ -234,25 +251,33 @@ export class ViewDropZoneDirective implements OnInit, OnDestroy { } } - private computeDropRegion(event: DragEvent): Region { + private computeDropRegion(event: DragEvent): Region | undefined { const horizontalDropZoneWidth = Math.min(DROP_REGION_MAX_SIZE, this._host.clientWidth / 3); const verticalDropZoneHeight = Math.min(DROP_REGION_MAX_SIZE, this._host.clientHeight / 3); const offsetX = event.pageX - this._host.getBoundingClientRect().left; const offsetY = event.pageY - this._host.getBoundingClientRect().top; - if (offsetX < horizontalDropZoneWidth) { + if (this.supportsRegion('west') && offsetX < horizontalDropZoneWidth) { return 'west'; } - if (offsetX > this._host.clientWidth - horizontalDropZoneWidth) { + if (this.supportsRegion('east') && offsetX > this._host.clientWidth - horizontalDropZoneWidth) { return 'east'; } - if (offsetY < verticalDropZoneHeight) { + if (this.supportsRegion('north') && offsetY < verticalDropZoneHeight) { return 'north'; } - if (offsetY > this._host.clientHeight - verticalDropZoneHeight) { + if (this.supportsRegion('south') && offsetY > this._host.clientHeight - verticalDropZoneHeight) { return 'south'; } - return 'center'; + if (this.supportsRegion('center')) { + return 'center'; + } + + return undefined; + } + + private supportsRegion(region: Region): boolean { + return !this.wbViewDropZoneRegions || this.wbViewDropZoneRegions.includes(region); } public ngOnDestroy(): void { @@ -260,6 +285,12 @@ export class ViewDropZoneDirective implements OnInit, OnDestroy { } } +function ensureHostElementPositioned(element: HTMLElement): void { + if (getComputedStyle(element).position === 'static') { + element.style.position = 'relative'; + } +} + interface Bounds { top: string; right: string; diff --git a/projects/scion/workbench/src/lib/view-dnd/view-tab-drag-image-renderer.service.ts b/projects/scion/workbench/src/lib/view-dnd/view-tab-drag-image-renderer.service.ts index e1cea168c..217124375 100644 --- a/projects/scion/workbench/src/lib/view-dnd/view-tab-drag-image-renderer.service.ts +++ b/projects/scion/workbench/src/lib/view-dnd/view-tab-drag-image-renderer.service.ts @@ -18,6 +18,7 @@ import { ViewTabContentComponent } from '../view-part/view-tab-content/view-tab- import { WorkbenchView } from '../workbench.model'; import { WorkbenchConfig } from '../workbench.config'; import { VIEW_TAB_CONTEXT } from '../workbench.constants'; +import { UrlSegment } from '@angular/router'; export type ConstrainFn = (rect: ViewDragImageRect) => ViewDragImageRect; @@ -191,6 +192,7 @@ class DragImageWorkbenchView implements WorkbenchView { public readonly active = true; public readonly blocked = false; public readonly cssClasses = []; + public readonly urlSegments: UrlSegment[]; constructor(dragData: ViewDragData) { this.viewRef = dragData.viewRef; @@ -198,6 +200,7 @@ class DragImageWorkbenchView implements WorkbenchView { this.heading = dragData.viewHeading; this.closable = dragData.viewClosable; this.dirty = dragData.viewDirty; + this.urlSegments = dragData.viewUrlSegments; } public close(): Promise { diff --git a/projects/scion/workbench/src/lib/view-part-grid/view-part-grid.component.ts b/projects/scion/workbench/src/lib/view-part-grid/view-part-grid.component.ts index 30660e0d4..6e2d2bd29 100644 --- a/projects/scion/workbench/src/lib/view-part-grid/view-part-grid.component.ts +++ b/projects/scion/workbench/src/lib/view-part-grid/view-part-grid.component.ts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { ChangeDetectorRef, Component, isDevMode, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { WbComponentPortal } from '../portal/wb-component-portal'; import { WorkbenchViewPartRegistry } from './workbench-view-part-registry.service'; import { VIEW_PART_REF_INDEX, ViewPartInfoArray, ViewPartSashBox } from './view-part-grid-serializer.service'; @@ -17,7 +17,10 @@ import { noop, Subject } from 'rxjs'; import { pairwise, startWith, takeUntil } from 'rxjs/operators'; import { ViewPartGrid, ViewPartGridNode } from './view-part-grid.model'; import { ViewDragService, ViewMoveEvent } from '../view-dnd/view-drag.service'; +import { InternalWorkbenchService } from '../workbench.service'; +import { UrlSegment } from '@angular/router'; import { ViewOutletNavigator } from '../routing/view-outlet-navigator.service'; +import { WorkbenchViewRegistry } from '../workbench-view-registry.service'; /** * Allows the arrangement of viewparts in a grid. @@ -52,7 +55,9 @@ export class ViewPartGridComponent implements OnInit, OnDestroy { */ public sashBox: ViewPartSashBox; - constructor(private _viewOutletNavigator: ViewOutletNavigator, + constructor(private _workbench: InternalWorkbenchService, + private _viewOutletNavigator: ViewOutletNavigator, + private _viewRegistry: WorkbenchViewRegistry, private _viewPartRegistry: WorkbenchViewPartRegistry, private _viewDragService: ViewDragService, private _cd: ChangeDetectorRef) { @@ -130,36 +135,94 @@ export class ViewPartGridComponent implements OnInit, OnDestroy { } private installViewMoveListener(): void { + const appInstanceId = this._workbench.appInstanceId; + this._viewDragService.viewMove$ .pipe(takeUntil(this._destroy$)) .subscribe((event: ViewMoveEvent) => { - if (event.source.appInstanceId !== event.target.appInstanceId) { - isDevMode() && console && console.warn && console.warn('[UnsupportedOperationError] Dragging views between different browsing contexts not supported yet'); + // Check if this app instance takes part in the view drag operation. If not, do nothing. + if (event.source.appInstanceId !== appInstanceId && event.target.appInstanceId !== appInstanceId) { return; } - if (event.source.viewPartRef === event.target.viewPartRef && event.target.viewPartRegion === 'center') { + const crossAppInstanceViewDrag = (event.source.appInstanceId !== event.target.appInstanceId); + + // Check if the user dropped the viewtab at the same location. If so, do nothing. + if (!crossAppInstanceViewDrag && event.source.viewPartRef === event.target.viewPartRef && event.target.viewPartRegion === 'center') { return; } - if (!event.target.viewPartRegion || event.target.viewPartRegion === 'center') { - this._viewOutletNavigator.navigate({ - viewGrid: this._viewPartRegistry.grid - .moveView(event.source.viewRef, event.target.viewPartRef, event.target.insertionIndex) - .serialize(), - }).then(); + // Check if to remove the view from this app instance if being moved to another app instance. + if (crossAppInstanceViewDrag && event.source.appInstanceId === appInstanceId) { + this.removeView(event); } + // Check if to add the view to this app instance if being moved from another app instance to this app instance. + else if (crossAppInstanceViewDrag && event.target.appInstanceId === appInstanceId) { + this.addView(event); + } + // Move the view within the same app instance. else { - const grid = this._viewPartRegistry.grid; - const newViewPartRef = grid.computeNextViewPartIdentity(); - - this._viewOutletNavigator.navigate({ - viewGrid: grid - .addSiblingViewPart(event.target.viewPartRegion, event.target.viewPartRef, newViewPartRef) - .moveView(event.source.viewRef, newViewPartRef) - .serialize(), - }).then(); + this.moveView(event); } }); } + + private addView(event: ViewMoveEvent): void { + const addToNewViewPart = (event.target.viewPartRegion || 'center') !== 'center'; + + // Transform URL segments into an array of commands. + const commands = event.source.viewUrlSegments.reduce((acc: any[], segment: UrlSegment) => { + return acc.concat( + segment.path || [], + segment.parameters && Object.keys(segment.parameters).length ? segment.parameters : [], + ); + }, []); + + if (addToNewViewPart) { + const newViewRef = this._viewRegistry.computeNextViewOutletIdentity(); + const newViewPartRef = this._viewPartRegistry.grid.computeNextViewPartIdentity(); + this._viewOutletNavigator.navigate({ + viewOutlet: {name: newViewRef, commands}, + viewGrid: this._viewPartRegistry.grid + .addSiblingViewPart(event.target.viewPartRegion, event.target.viewPartRef, newViewPartRef) + .addView(newViewPartRef, newViewRef) + .serialize(), + }).then(); + } + else { + const newViewRef = this._viewRegistry.computeNextViewOutletIdentity(); + this._viewOutletNavigator.navigate({ + viewOutlet: {name: newViewRef, commands}, + viewGrid: this._viewPartRegistry.grid + .addView(event.target.viewPartRef, newViewRef, event.target.insertionIndex) + .serialize(), + }).then(); + } + } + + private removeView(event: ViewMoveEvent): void { + this._workbench.destroyView(event.source.viewRef).then(); + } + + private moveView(event: ViewMoveEvent): void { + const addToNewViewPart = (event.target.viewPartRegion || 'center') !== 'center'; + const grid = this._viewPartRegistry.grid; + + if (addToNewViewPart) { + const newViewPartRef = grid.computeNextViewPartIdentity(); + this._viewOutletNavigator.navigate({ + viewGrid: grid + .addSiblingViewPart(event.target.viewPartRegion, event.target.viewPartRef, newViewPartRef) + .moveView(event.source.viewRef, newViewPartRef) + .serialize(), + }).then(); + } + else { + this._viewOutletNavigator.navigate({ + viewGrid: grid + .moveView(event.source.viewRef, event.target.viewPartRef, event.target.insertionIndex) + .serialize(), + }).then(); + } + } } diff --git a/projects/scion/workbench/src/lib/view-part/view-part-bar/view-part-bar.component.ts b/projects/scion/workbench/src/lib/view-part/view-part-bar/view-part-bar.component.ts index fa093e48d..3c8315c56 100644 --- a/projects/scion/workbench/src/lib/view-part/view-part-bar/view-part-bar.component.ts +++ b/projects/scion/workbench/src/lib/view-part/view-part-bar/view-part-bar.component.ts @@ -234,6 +234,7 @@ export class ViewPartBarComponent implements OnInit, OnDestroy { appInstanceId: this.dragData.appInstanceId, viewPartRef: this.dragData.viewPartRef, viewRef: this.dragData.viewRef, + viewUrlSegments: this.dragData.viewUrlSegments, }, target: { appInstanceId: this._workbench.appInstanceId, diff --git a/projects/scion/workbench/src/lib/view-part/view-part.component.html b/projects/scion/workbench/src/lib/view-part/view-part.component.html index 140c72c8d..f8afef372 100644 --- a/projects/scion/workbench/src/lib/view-part/view-part.component.html +++ b/projects/scion/workbench/src/lib/view-part/view-part.component.html @@ -5,7 +5,7 @@ -
+
diff --git a/projects/scion/workbench/src/lib/view-part/view-part.component.ts b/projects/scion/workbench/src/lib/view-part/view-part.component.ts index d69421e14..531526444 100644 --- a/projects/scion/workbench/src/lib/view-part/view-part.component.ts +++ b/projects/scion/workbench/src/lib/view-part/view-part.component.ts @@ -16,6 +16,7 @@ import { InternalWorkbenchService } from '../workbench.service'; import { WorkbenchViewPart } from '../workbench.model'; import { takeUntil } from 'rxjs/operators'; import { ViewDragService } from '../view-dnd/view-drag.service'; +import { WorkbenchViewRegistry } from '../workbench-view-registry.service'; @Component({ selector: 'wb-view-part', @@ -44,6 +45,7 @@ export class ViewPartComponent implements OnDestroy { constructor(private _workbench: InternalWorkbenchService, private _viewDragService: ViewDragService, private _viewPart: WorkbenchViewPart, + private _viewRegistry: WorkbenchViewRegistry, public viewPartService: WorkbenchViewPartService) { combineLatest([this._workbench.viewPartActions$, this._viewPart.actions$, this._viewPart.viewRefs$]) .pipe(takeUntil(this._destroy$)) @@ -73,11 +75,13 @@ export class ViewPartComponent implements OnDestroy { return; } + const activeView = this._viewRegistry.getElseThrow(this._viewPart.activeViewRef); this._viewDragService.dispatchViewMoveEvent({ source: { appInstanceId: this._workbench.appInstanceId, viewPartRef: this._viewPart.viewPartRef, - viewRef: this._viewPart.activeViewRef, + viewRef: activeView.viewRef, + viewUrlSegments: activeView.urlSegments, }, target: { appInstanceId: this._workbench.appInstanceId, @@ -104,6 +108,7 @@ export class ViewPartComponent implements OnDestroy { appInstanceId: event.dragData.appInstanceId, viewPartRef: event.dragData.viewPartRef, viewRef: event.dragData.viewRef, + viewUrlSegments: event.dragData.viewUrlSegments, }, target: { appInstanceId: this._workbench.appInstanceId, diff --git a/projects/scion/workbench/src/lib/view-part/view-tab/view-tab.component.ts b/projects/scion/workbench/src/lib/view-part/view-tab/view-tab.component.ts index 3f85692f0..b37cbe0cb 100644 --- a/projects/scion/workbench/src/lib/view-part/view-tab/view-tab.component.ts +++ b/projects/scion/workbench/src/lib/view-part/view-tab/view-tab.component.ts @@ -107,6 +107,7 @@ export class ViewTabComponent implements OnDestroy { viewHeading: this.view.heading, viewClosable: this.view.closable, viewDirty: this.view.dirty, + viewUrlSegments: this.view.urlSegments, viewPartRef: this.view.viewPart.viewPartRef, viewTabPointerOffsetX: event.offsetX, viewTabPointerOffsetY: event.offsetY, diff --git a/projects/scion/workbench/src/lib/workbench-view-registry.service.ts b/projects/scion/workbench/src/lib/workbench-view-registry.service.ts index 22bbd692a..25050cb65 100644 --- a/projects/scion/workbench/src/lib/workbench-view-registry.service.ts +++ b/projects/scion/workbench/src/lib/workbench-view-registry.service.ts @@ -98,7 +98,7 @@ export class WorkbenchViewRegistry implements OnDestroy { private createWorkbenchView(viewRef: string, active: boolean): InternalWorkbenchView { const portal = new WbComponentPortal(this._componentFactoryResolver, this._injector.get(VIEW_COMPONENT_TYPE)); - const view = new InternalWorkbenchView(viewRef, active, this._injector.get(WORKBENCH), portal, this._viewActivationInstantProvider); + const view = new InternalWorkbenchView(viewRef, active, this._injector.get(WORKBENCH), portal, this._viewActivationInstantProvider, this._router); portal.init({ injectorTokens: new WeakMap() diff --git a/projects/scion/workbench/src/lib/workbench.model.ts b/projects/scion/workbench/src/lib/workbench.model.ts index b4aeb0c45..e27b66afb 100644 --- a/projects/scion/workbench/src/lib/workbench.model.ts +++ b/projects/scion/workbench/src/lib/workbench.model.ts @@ -18,6 +18,7 @@ import { Injector, TemplateRef, Type } from '@angular/core'; import { Disposable } from './disposable'; import { ComponentType } from '@angular/cdk/portal'; import { ViewActivationInstantProvider } from './view-activation-instant-provider.service'; +import { Router, UrlSegment } from '@angular/router'; /** * A view is a visual component within the Workbench to present content, @@ -95,6 +96,13 @@ export abstract class WorkbenchView { * Note: This instruction runs asynchronously via URL routing. */ public abstract close(): Promise; + + /** + * Returns the URL segments of this view. + * + * A {@link UrlSegment} is a part of a URL between the two slashes. It contains a path and the matrix parameters associated with the segment. + */ + public abstract get urlSegments(): UrlSegment[]; } export class InternalWorkbenchView implements WorkbenchView { @@ -118,7 +126,8 @@ export class InternalWorkbenchView implements WorkbenchView { active: boolean, public workbench: WorkbenchService, public readonly portal: WbComponentPortal, - private _viewActivationInstantProvider: ViewActivationInstantProvider) { + private _viewActivationInstantProvider: ViewActivationInstantProvider, + private _router: Router) { this.active$ = new BehaviorSubject(active); this.cssClasses$ = new BehaviorSubject([]); this.title = viewRef; @@ -154,6 +163,18 @@ export class InternalWorkbenchView implements WorkbenchView { return this.workbench.destroyView(this.viewRef); } + public get urlSegments(): UrlSegment[] { + const urlTree = this._router.parseUrl(this._router.url); + const urlSegmentGroups = urlTree.root.children; + + const viewOutlet = urlSegmentGroups[this.viewRef]; + if (!viewOutlet) { + throw Error(`[ViewOutletNotFoundError] View outlet not part of the URL [outlet=${this.viewRef}]`); + } + + return viewOutlet.segments; + } + public get destroyed(): boolean { return this.portal.isDestroyed; }