Skip to content

Commit

Permalink
feat: provide a context menu on view tabs
Browse files Browse the repository at this point in the history
closes: #174
  • Loading branch information
danielwiehl committed Aug 29, 2019
1 parent 2ee9df3 commit 1bdb195
Show file tree
Hide file tree
Showing 33 changed files with 1,044 additions and 136 deletions.
14 changes: 14 additions & 0 deletions projects/scion/workbench/src/lib/array.util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,18 @@ describe('Arrays', () => {
expect(Arrays.last([], () => true)).toBeUndefined();
});
});

describe('Arrays.remove', () => {

it('should remove the specified element', () => {
const array = ['a', 'b', 'c', 'd', 'e'];
expect(Arrays.remove(array, 'c')).toEqual(['a', 'b', 'd', 'e']);
});

it('should not modify the original array', () => {
const array = ['a', 'b', 'c', 'd', 'e'];
expect(Arrays.remove(array, 'c')).toEqual(['a', 'b', 'd', 'e']);
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
});
});
});
11 changes: 11 additions & 0 deletions projects/scion/workbench/src/lib/array.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,15 @@ export class Arrays {
}
return [...array].reverse().find(predicate);
}

/**
* Removes given item from the array. The original array will not be modified.
*/
public static remove<T>(items: T[], item: T): T[] {
const result = [...items];
for (let index = result.indexOf(item); index !== -1; index = result.indexOf(item)) {
result.splice(index, 1);
}
return result;
}
}
16 changes: 16 additions & 0 deletions projects/scion/workbench/src/lib/operators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { map } from 'rxjs/operators';
import { MonoTypeOperatorFunction, OperatorFunction } from 'rxjs';

/**
* Filters items in the source array and emits an array with items satisfying the given predicate.
*/
export function filterArray<T>(predicate?: (item: T) => boolean): MonoTypeOperatorFunction<T[]> {
return map((items: T[]): T[] => items.filter(item => !predicate || predicate(item)));
}

/**
* Maps each element in the source array to its mapped value.
*/
export function mapArray<I, P>(fn: (item: I) => P): OperatorFunction<I[], P[]> {
return map((items: I[]): P[] => items.map(item => fn(item)));
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
import { ActivatedRoute, NavigationExtras, PRIMARY_OUTLET, Router, UrlSegment } from '@angular/router';
import { ActivatedRoute, NavigationExtras, PRIMARY_OUTLET, Router, UrlSegment, UrlTree } from '@angular/router';
import { ACTIVITY_OUTLET_NAME, VIEW_GRID_QUERY_PARAM, VIEW_REF_PREFIX } from '../workbench.constants';
import { Injectable } from '@angular/core';
import { Arrays } from '../array.util';
import { coerceArray } from '@angular/cdk/coercion';

/**
* Allows navigating to auxiliary routes in view outlets.
Expand All @@ -25,10 +26,18 @@ export class ViewOutletNavigator {
* Navigates based on the provided array of commands, if any, and updates the URL with the given view grid.
*/
public navigate(params: { viewOutlet?: { name: string, commands: any[] }, viewGrid: string, extras?: NavigationExtras }): Promise<boolean> {
const urlTree = this.createUrlTree(params);
return this._router.navigateByUrl(urlTree);
}

/**
* Applies the provided commands and viewgrid to the current URL tree and creates a new URL tree.
*/
public createUrlTree(params: { viewOutlet?: Outlet | Outlet[], viewGrid: string, extras?: NavigationExtras }): UrlTree {
const {viewOutlet, viewGrid, extras = {}} = params;
const commands: any[] = (viewOutlet ? [{outlets: {[viewOutlet.name]: viewOutlet.commands}}] : []);
const commands: any[] = createOutletCommands(viewOutlet);

return this._router.navigate(commands, {
return this._router.createUrlTree(commands, {
...extras,
queryParams: {...extras.queryParams, [VIEW_GRID_QUERY_PARAM]: viewGrid},
queryParamsHandling: 'merge',
Expand Down Expand Up @@ -99,3 +108,16 @@ export class ViewOutletNavigator {
return serializedCommands;
}
}

function createOutletCommands(viewOutlets?: Outlet | Outlet[]): any[] {
if (!viewOutlets) {
return [];
}
const outletsObject = coerceArray(viewOutlets).reduce((acc, viewOutlet) => ({...acc, [viewOutlet.name]: viewOutlet.commands}), {});
return [{outlets: outletsObject}];
}

export interface Outlet {
name: string;
commands: any[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { NavigationExtras, Router } from '@angular/router';
import { InternalWorkbenchService } from '../workbench.service';
import { WorkbenchViewRegistry } from '../workbench-view-registry.service';
import { Defined } from '../defined.util';
import { WorkbenchViewPartRegistry } from '../view-part-grid/workbench-view-part-registry.service';
import { ViewPartGrid } from '../view-part-grid/view-part-grid.model';
import { ViewOutletNavigator } from './view-outlet-navigator.service';
import { Injectable } from '@angular/core';
import { ViewPartGridProvider } from '../view-part-grid/view-part-grid-provider.service';

/**
* Provides workbench view navigation capabilities based on Angular Router.
Expand All @@ -27,7 +27,7 @@ export class WorkbenchRouter {
private _viewOutletNavigator: ViewOutletNavigator,
private _workbench: InternalWorkbenchService,
private _viewRegistry: WorkbenchViewRegistry,
private _viewPartRegistry: WorkbenchViewPartRegistry) {
private _viewPartGridProvider: ViewPartGridProvider) {
}

/**
Expand Down Expand Up @@ -71,7 +71,7 @@ export class WorkbenchRouter {

switch (extras.target || 'blank') {
case 'blank': {
const viewPartGrid = this._viewPartRegistry.grid;
const viewPartGrid = this._viewPartGridProvider.grid;
const newViewRef = this._viewRegistry.computeNextViewOutletIdentity();
const viewPartRef = extras.blankViewPartRef || (this._workbench.activeViewPartService && this._workbench.activeViewPartService.viewPartRef) || viewPartGrid.viewPartRefs()[0];
const viewInsertionIndex = this.coerceViewInsertionIndex(extras.blankInsertionIndex, viewPartRef, viewPartGrid);
Expand All @@ -97,7 +97,7 @@ export class WorkbenchRouter {

return this._viewOutletNavigator.navigate({
viewOutlet: {name: extras.selfViewRef, commands},
viewGrid: this._viewPartRegistry.grid.serialize(),
viewGrid: this._viewPartGridProvider.grid.serialize(),
extras: {
...extras,
relativeTo: null, // commands are absolute because normalized
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export interface ViewMoveEvent {
viewPartRef: string;
viewPartRegion?: 'north' | 'east' | 'south' | 'west' | 'center';
insertionIndex?: number;
appInstanceId: string;
appInstanceId: string | 'new';
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@
*/

import { ApplicationRef, ComponentFactoryResolver, Injectable, Injector, OnDestroy } from '@angular/core';
import { of, Subject } from 'rxjs';
import { EMPTY, of, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { createElement, setStyle } from '../dom.util';
import { ViewDragData, ViewDragService } from './view-drag.service';
import { ComponentPortal, DomPortalOutlet, PortalInjector } from '@angular/cdk/portal';
import { ViewTabContentComponent } from '../view-part/view-tab-content/view-tab-content.component';
import { WorkbenchView } from '../workbench.model';
import { WorkbenchMenuItem, WorkbenchView } from '../workbench.model';
import { WorkbenchConfig } from '../workbench.config';
import { VIEW_TAB_CONTEXT } from '../workbench.constants';
import { UrlSegment } from '@angular/router';
import { Disposable } from '../disposable';

export type ConstrainFn = (rect: ViewDragImageRect) => ViewDragImageRect;

Expand Down Expand Up @@ -193,6 +194,10 @@ class DragImageWorkbenchView implements WorkbenchView {
public readonly blocked = false;
public readonly cssClasses = [];
public readonly urlSegments: UrlSegment[];
public readonly viewMenuItems$ = EMPTY;
public readonly first = true;
public readonly last = true;
public readonly position = 0;

constructor(dragData: ViewDragData) {
this.viewRef = dragData.viewRef;
Expand All @@ -207,7 +212,15 @@ class DragImageWorkbenchView implements WorkbenchView {
throw Error('[UnsupportedOperationError]');
}

public move(region: 'north' | 'south' | 'west' | 'east'): Promise<boolean> {
throw Error('[UnsupportedOperationError]');
}

public set cssClass(cssClass: string | string[]) {
throw Error('[UnsupportedOperationError]');
}

public registerMenuItem(menuItem: WorkbenchMenuItem): Disposable {
throw Error('[UnsupportedOperationError]');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Injectable } from '@angular/core';
import { ViewPartGrid } from './view-part-grid.model';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter } from 'rxjs/operators';

/**
* Provides access to the viewpart grid which represents the visual arrangement of the viewparts.
*/
@Injectable()
export class ViewPartGridProvider {

private readonly _grid$ = new BehaviorSubject<ViewPartGrid>(null);

/**
* Sets the given viewpart grid.
*/
public setGrid(grid: ViewPartGrid): void {
this._grid$.next(grid);
}

/**
* Returns a reference to the viewpart grid, if any. Is `null` until the initial navigation is performed.
*/
public get grid(): ViewPartGrid {
return this._grid$.value;
}

/**
* Emits the viewpart grid.
*
* Upon subscription, the current grid is emitted, if any, and then emits continuously when the grid changes. It never completes.
*/
public get grid$(): Observable<ViewPartGrid> {
return this._grid$.pipe(filter(Boolean));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ 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 { Router, UrlSegment } from '@angular/router';
import { ViewOutletNavigator } from '../routing/view-outlet-navigator.service';
import { WorkbenchViewRegistry } from '../workbench-view-registry.service';
import { ViewPartGridProvider } from './view-part-grid-provider.service';
import { LocationStrategy } from '@angular/common';

/**
* Allows the arrangement of viewparts in a grid.
Expand Down Expand Up @@ -59,13 +61,16 @@ export class ViewPartGridComponent implements OnInit, OnDestroy {
private _viewOutletNavigator: ViewOutletNavigator,
private _viewRegistry: WorkbenchViewRegistry,
private _viewPartRegistry: WorkbenchViewPartRegistry,
private _viewPartGridProvider: ViewPartGridProvider,
private _viewDragService: ViewDragService,
private _locationStrategy: LocationStrategy,
private _router: Router,
private _cd: ChangeDetectorRef) {
this.installViewMoveListener();
}

public ngOnInit(): void {
this._viewPartRegistry.grid$
this._viewPartGridProvider.grid$
.pipe(
startWith(null as ViewPartGrid), // start with a null grid to initialize the 'pairwise' operator, so it emits once the grid is set.
pairwise(),
Expand Down Expand Up @@ -155,6 +160,10 @@ export class ViewPartGridComponent implements OnInit, OnDestroy {
// 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 a new browser window.
if (event.target.appInstanceId === 'new') {
this.addViewToNewWindow(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) {
Expand All @@ -180,10 +189,10 @@ export class ViewPartGridComponent implements OnInit, OnDestroy {

if (addToNewViewPart) {
const newViewRef = this._viewRegistry.computeNextViewOutletIdentity();
const newViewPartRef = this._viewPartRegistry.grid.computeNextViewPartIdentity();
const newViewPartRef = this._viewPartGridProvider.grid.computeNextViewPartIdentity();
this._viewOutletNavigator.navigate({
viewOutlet: {name: newViewRef, commands},
viewGrid: this._viewPartRegistry.grid
viewGrid: this._viewPartGridProvider.grid
.addSiblingViewPart(event.target.viewPartRegion, event.target.viewPartRef, newViewPartRef)
.addView(newViewPartRef, newViewRef)
.serialize(),
Expand All @@ -193,20 +202,34 @@ export class ViewPartGridComponent implements OnInit, OnDestroy {
const newViewRef = this._viewRegistry.computeNextViewOutletIdentity();
this._viewOutletNavigator.navigate({
viewOutlet: {name: newViewRef, commands},
viewGrid: this._viewPartRegistry.grid
viewGrid: this._viewPartGridProvider.grid
.addView(event.target.viewPartRef, newViewRef, event.target.insertionIndex)
.serialize(),
}).then();
}
}

private addViewToNewWindow(event: ViewMoveEvent): void {
const urlTree = this._viewOutletNavigator.createUrlTree({
viewOutlet: this._viewRegistry.viewRefs
.filter(viewRef => viewRef !== event.source.viewRef) // retain the source view outlet
.map(viewRef => ({name: viewRef, commands: null})), // remove all other view outlets
viewGrid: this._viewPartGridProvider.grid
.clear()
.addView(event.target.viewPartRef, event.source.viewRef)
.serialize(),
});

window.open(this._locationStrategy.prepareExternalUrl(this._router.serializeUrl(urlTree)));
}

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;
const grid = this._viewPartGridProvider.grid;

if (addToNewViewPart) {
const newViewPartRef = grid.computeNextViewPartIdentity();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ export class ViewPartGrid {
return new ViewPartGrid(serializedGrid, this._serializer, this._viewRegistry);
}

/**
*
* Returns a copy of this grid with all views removed.
*/
public clear(): ViewPartGrid {
return new ViewPartGrid(null, this._serializer, this._viewRegistry);
}

/**
* Adds a view to the specified viewpart, and activates it.
* Control the insertion position by providing an insertion index. If not specified, the view is added at the end.
Expand Down
Loading

0 comments on commit 1bdb195

Please sign in to comment.