Skip to content

Commit

Permalink
feat: allow adding actions to the viewpart action bar
Browse files Browse the repository at this point in the history
Viewpart actions are added to the viewpart action bar located to the right of the view tabs. If added in the context of a view, the action is local to the containing viewpart and only shown if the view is active. If not in the context of a viewpart, the action is added to every viewpart instead.

closes #104
  • Loading branch information
danielwiehl authored and ReToCode committed Mar 15, 2019
1 parent fdc9218 commit 0b31ca3
Show file tree
Hide file tree
Showing 32 changed files with 505 additions and 20 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ SCION Workbench helps to build multi-view web applications and integrates separa
<a href="https://github.com/SchweizerischeBundesbahnen/scion-workbench/raw/master/resources/site/pics/workbench-large.png">![SCION Workbench](/resources/site/pics/workbench-small.png)</a>

The Workbench provides core features of a modern rich web application.
- Tabbed, movable and stackable views
- Activity panel as application entry point
- Popups
- Global notifications
- Global or view-local message boxes
- tabbed, movable and stackable views
- activity panel as application entry point
- popups
- global notifications
- global or view-local message boxes
- global or view-local viewpart actions
- URL encoded navigational state

<a href="https://github.com/SchweizerischeBundesbahnen/scion-workbench/raw/master/resources/site/pics/workbench-sketch-large.png">![SCION Workbench Features](/resources/site/pics/workbench-sketch-small.png)</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { Activity1a90c8d31Component } from './activity-1a90c8d3/activity-1a90c8d
import { Activity1a90c8d32Component } from './activity-1a90c8d3/activity-1a90c8d3-2.component';
import { WelcomePageComponent } from './welcome-page/welcome-page.component';
import { ViewComponent } from './view/view.component';
import { View4a3a8932Component } from './view-4a3a8932/view-4a3a8932.component';

const routes: Routes = [
{path: '', component: WelcomePageComponent},
{path: 'welcome', component: WelcomePageComponent},
{path: 'activity-1a90c8d31-1', component: Activity1a90c8d31Component},
{path: 'activity-1a90c8d31-2', component: Activity1a90c8d32Component},
{path: 'view', component: ViewComponent},
{path: 'view-4a3a8932', component: View4a3a8932Component},
];

@NgModule({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,10 @@
itemCssClass="material-icons"
routerLink="activity-1a90c8d31-2">
</wb-activity>

<ng-template wbViewPartAction *ngIf="showOpenNewViewTabAction">
<button [wbRouterLink]="'/welcome'" class="material-icons e2e-open-new-tab" [wbRouterLinkExtras]="{activateIfPresent: false}">
add
</button>
</ng-template>
</wb-workbench>
13 changes: 12 additions & 1 deletion projects/app/workbench/workbench-app/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { WorkbenchRouter, WorkbenchService } from '@scion/workbench';

@Component({
selector: 'app-root',
Expand All @@ -14,15 +15,25 @@ export class AppComponent implements OnDestroy {
private _destroy$ = new Subject<void>();

public showActivities = true;
public showOpenNewViewTabAction = true;
public ensureWelcomeView = false;

constructor(route: ActivatedRoute) {
constructor(route: ActivatedRoute, workbench: WorkbenchService, wbRouter: WorkbenchRouter) {
route.queryParamMap
.pipe(takeUntil(this._destroy$))
.subscribe(queryParams => {
this.showActivities = coerceBooleanProperty(queryParams.get('show-activities') || true);
this.showOpenNewViewTabAction = coerceBooleanProperty(queryParams.get('show-open-new-view-tab-action') || true);
this.ensureWelcomeView = coerceBooleanProperty(queryParams.get('ensure-welcome-view') || false);
});

workbench.views$
.pipe(takeUntil(this._destroy$))
.subscribe(views => {
if (this.ensureWelcomeView && views.length === 0) {
wbRouter.navigate(['/welcome']).then();
}
});
}

public ngOnDestroy(): void {
Expand Down
4 changes: 4 additions & 0 deletions projects/app/workbench/workbench-app/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { WorkbenchModule } from '@scion/workbench';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Activity1a90c8d31Component } from './activity-1a90c8d3/activity-1a90c8d3-1.component';
import { Activity1a90c8d32Component } from './activity-1a90c8d3/activity-1a90c8d3-2.component';
import { WelcomePageComponent } from './welcome-page/welcome-page.component';
import { ViewComponent } from './view/view.component';
import { View4a3a8932Component } from './view-4a3a8932/view-4a3a8932.component';

@NgModule({
declarations: [
Expand All @@ -15,6 +18,7 @@ import { Activity1a90c8d32Component } from './activity-1a90c8d3/activity-1a90c8d
Activity1a90c8d32Component,
WelcomePageComponent,
ViewComponent,
View4a3a8932Component,
],
imports: [
BrowserModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ng-template wbViewPartAction align="end">
<button class="material-icons e2e-button-4a3a8932">
send
</button>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { WorkbenchView } from '@scion/workbench';

@Component({
selector: 'app-view-4a3a8932',
templateUrl: './view-4a3a8932.component.html',
})
export class View4a3a8932Component {

constructor(view: WorkbenchView) {
view.title = 'Testcase 4a3a8932';
view.cssClass = 'e2e-view-4a3a8932';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ <h1>Welcome to SCION Workbench</h1>
<a [wbRouterLink]="['/view', { viewTitle: 'Tile view 2', viewCssClass: 'e2e-tile-view-2' }]" class="e2e-tile-view-2">Open view 2</a>
<a [wbRouterLink]="['/view', { viewTitle: 'Tile view 3', viewCssClass: 'e2e-tile-view-3' }]" class="e2e-tile-view-3">Open view 3</a>
<a [wbRouterLink]="['/view', { viewTitle: 'Tile view 4', viewCssClass: 'e2e-tile-view-4' }]" class="e2e-tile-view-4">Open view 4</a>
<a [wbRouterLink]="['/view-4a3a8932']" class="e2e-tile-4a3a8932">Testcase '4a3a8932'</a>
</nav>
62 changes: 62 additions & 0 deletions projects/e2e/workbench/src/view-part-action.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2018 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

import { AppPO } from './page-object/app.po';
import { browser } from 'protractor';
import { WelcomePagePO } from './page-object/welcome-page.po';

describe('ViewPartAction', () => {

const appPO = new AppPO();

beforeEach(async () => {
await browser.get('/');
});

it('should be added to all viewparts', async () => {
const openNewTabActionButtonPO = appPO.findViewPartAction('e2e-open-new-tab');

await expect(appPO.isViewTabBarShowing()).toBeTruthy();
await expect(openNewTabActionButtonPO.isPresent()).toBeTruthy();

await browser.get('/#/?show-open-new-view-tab-action=false');
await expect(appPO.isViewTabBarShowing()).toBeFalsy();
await expect(openNewTabActionButtonPO.isPresent()).toBeFalsy();

await browser.get('/#/?show-open-new-view-tab-action=true');
await expect(appPO.isViewTabBarShowing()).toBeTruthy();
await expect(openNewTabActionButtonPO.isPresent()).toBeTruthy();
});

it('should stick to a view if registered in the context of a view [testcase: 4a3a8932]', async () => {
const welcomePagePO = new WelcomePagePO();
const viewTabPO = appPO.findViewTab('e2e-view-4a3a8932');
const viewLocalActionButtonPO = appPO.findViewPartAction('e2e-button-4a3a8932');

await welcomePagePO.clickTile('e2e-tile-4a3a8932');

// Open a view which contributes a view-local action
await expect(viewTabPO.isActive()).toBeTruthy();
await expect(viewLocalActionButtonPO.isPresent()).toBeTruthy();

// Open a new view tab
await appPO.openNewViewTab();
await expect(viewLocalActionButtonPO.isPresent()).toBeFalsy();

// Activate previous view
await viewTabPO.click();
await expect(viewLocalActionButtonPO.isPresent()).toBeTruthy();

// Close the view
await viewTabPO.close();
await expect(viewTabPO.isPresent()).toBeFalsy();
await expect(viewLocalActionButtonPO.isPresent()).toBeFalsy();
});
});
51 changes: 51 additions & 0 deletions projects/e2e/workbench/src/view-tab-bar.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2018 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

import { AppPO } from './page-object/app.po';
import { browser } from 'protractor';
import { WelcomePagePO } from './page-object/welcome-page.po';

describe('ViewTabBar', () => {

const appPO = new AppPO();
const welcomePagePO = new WelcomePagePO();

beforeEach(async () => {
await browser.get('/');
});

it('should not show if no views are open and no viewpart actions present', async () => {
await browser.get('/#/?show-open-new-view-tab-action=false');

await expect(appPO.getViewTabCount()).toEqual(0);
await expect(appPO.isViewTabBarShowing()).toBeFalsy();

await welcomePagePO.clickTile('e2e-tile-view-1');
await expect(appPO.getViewTabCount()).toEqual(1);
await expect(appPO.isViewTabBarShowing()).toBeTruthy();

await appPO.findViewTab('e2e-tile-view-1').close();
await expect(appPO.getViewTabCount()).toEqual(0);
await expect(appPO.isViewTabBarShowing()).toBeFalsy();

await browser.get('/#/?show-open-new-view-tab-action=true');

await expect(appPO.getViewTabCount()).toEqual(0);
await expect(appPO.isViewTabBarShowing()).toBeTruthy();

await welcomePagePO.clickTile('e2e-tile-view-1');
await expect(appPO.getViewTabCount()).toEqual(1);
await expect(appPO.isViewTabBarShowing()).toBeTruthy();

await appPO.findViewTab('e2e-tile-view-1').close();
await expect(appPO.getViewTabCount()).toEqual(0);
await expect(appPO.isViewTabBarShowing()).toBeTruthy();
});
});
36 changes: 36 additions & 0 deletions projects/e2e/workbench/src/workbench.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2018 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

import { AppPO } from './page-object/app.po';
import { browser } from 'protractor';
import { expectViewToShow } from './util/testing.util';

describe('Workbench', () => {

const appPO = new AppPO();

beforeEach(async () => {
await browser.get('/');
});

it('should allow to always have an entry view open', async () => {
await browser.get('/#/?ensure-welcome-view=true');

await expect(appPO.getViewTabCount()).toEqual(1);
await expect(appPO.isViewTabBarShowing()).toBeTruthy();
await expectViewToShow({viewCssClass: 'e2e-welcome-page', componentSelector: 'app-welcome-page'});

// close the view
await appPO.findViewTab('e2e-welcome-page').close();
await expect(appPO.getViewTabCount()).toEqual(1);
await expect(appPO.isViewTabBarShowing()).toBeTruthy();
await expectViewToShow({viewCssClass: 'e2e-welcome-page', componentSelector: 'app-welcome-page'});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@ $size: 14px;
}

:host-context:not(.visible) {
visibility: hidden;
display: none;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
width: 100%;
height: 100%;

// Sets the viewport client (<ul>) to its smallest height to shrink the overlay when closing views.
--grid-template-rows: min-content;

ul {
list-style: none;
padding: 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<ul class="e2e-view-actions start">
<li *ngFor="let action of startActions$ | async">
<ng-container *ngTemplateOutlet="action_template; context: {$implicit: action}"></ng-container>
</li>
</ul>
<ul class="e2e-view-actions end">
<li *ngFor="let action of endActions$ | async">
<ng-container *ngTemplateOutlet="action_template; context: {$implicit: action}"></ng-container>
</li>
</ul>

<ng-template #action_template let-action>
<ng-container *ngIf="isTemplate(action)">
<ng-container *ngTemplateOutlet="action.templateOrComponent"></ng-container>
</ng-container>
<ng-container *ngIf="!isTemplate(action)">
<ng-container *ngComponentOutlet="action.templateOrComponent.component; injector: addViewPartToInjector(action.templateOrComponent.injector)"></ng-container>
</ng-container>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
:host {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 1em 0 1em;

> ul {
display: flex;
flex-direction: row;
align-items: center;
list-style: none;
margin: 0;
padding: 0;

> li {
flex: none;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2018 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

import { ChangeDetectionStrategy, Component, Injector, TemplateRef } from '@angular/core';
import { WorkbenchViewPartService } from '../workbench-view-part.service';
import { combineLatest, Observable, OperatorFunction } from 'rxjs';
import { map } from 'rxjs/operators';
import { WorkbenchViewPart, WorkbenchViewPartAction } from '../../workbench.model';
import { InternalWorkbenchService } from '../../workbench.service';
import { PortalInjector } from '@angular/cdk/portal';

@Component({
selector: 'wb-view-part-action-bar',
templateUrl: './view-part-action-bar.component.html',
styleUrls: ['./view-part-action-bar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ViewPartActionBarComponent {

public startActions$: Observable<WorkbenchViewPartAction[]>;
public endActions$: Observable<WorkbenchViewPartAction[]>;

constructor(private _viewPart: WorkbenchViewPart, workbenchService: InternalWorkbenchService, viewPartService: WorkbenchViewPartService) {
this.startActions$ = combineLatest(this._viewPart.actions$, workbenchService.viewPartActions$, viewPartService.activeViewRef$).pipe(combineAndFilterViewPartActions('start'));
this.endActions$ = combineLatest(this._viewPart.actions$, workbenchService.viewPartActions$, viewPartService.activeViewRef$).pipe(combineAndFilterViewPartActions('end'));
}

public isTemplate(action: WorkbenchViewPartAction): boolean {
return action.templateOrComponent instanceof TemplateRef;
}

public addViewPartToInjector(injector: Injector): Injector {
const injectionTokens = new WeakMap();
injectionTokens.set(WorkbenchViewPart, this._viewPart);
return new PortalInjector(injector, injectionTokens);
}
}

function combineAndFilterViewPartActions(align: 'start' | 'end'): OperatorFunction<[WorkbenchViewPartAction[], WorkbenchViewPartAction[], string], WorkbenchViewPartAction[]> {
return map(([localActions, globalActions, activeViewRef]: [WorkbenchViewPartAction[], WorkbenchViewPartAction[], string]): WorkbenchViewPartAction[] => {
return [...localActions, ...globalActions]
.filter(action => (action.align || 'start') === align)
.filter(action => !action.viewRef || action.viewRef === activeViewRef);
}
);
}
Loading

0 comments on commit 0b31ca3

Please sign in to comment.