Skip to content

Commit

Permalink
fix(workbench/perspective): support browser back navigation after swi…
Browse files Browse the repository at this point in the history
…tching perspective

Switching perspectives now creates a session history entry, allowing for back/forward browser navigation.

closes #579

BREAKING CHANGE: The active perspective is now set after navigation completes (previously before navigation), so it is unavailable during route resolution/activation. Route guards (like `canMatch`) should use the `canMatchWorkbenchPerspective` function instead of `WorkbenchService` or `WorkbenchPerspective` to determine the perspective’s activation state.

**Migration Example:**

**Before:**
```ts
import {Route} from '@angular/router';
import {inject} from '@angular/core';
import {WorkbenchService} from '@scion/workbench';

const route: Route = {
  canMatch: [() => inject(WorkbenchService).activePerspective()?.id === 'perspective'],
  // or
  canMatch: [() => inject(WorkbenchService).perspectives().find(perspective => perspective.id === 'perspective')?.active()],
};```

**After:**
```ts
import {Route} from '@angular/router';
import {canMatchWorkbenchPerspective} from '@scion/workbench';

const route: Route = {
  canMatch: [canMatchWorkbenchPerspective('perspective')],
};
```
  • Loading branch information
danielwiehl authored and Marcarrian committed Sep 2, 2024
1 parent 4c8412c commit 5777728
Show file tree
Hide file tree
Showing 24 changed files with 184 additions and 89 deletions.
2 changes: 1 addition & 1 deletion apps/workbench-testing-app/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@if (workbenchStartup.whenStarted | async) {
@if (workbenchStartup.isStarted()) {
<app-header/>
}
<main>
Expand Down
3 changes: 1 addition & 2 deletions apps/workbench-testing-app/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {filter} from 'rxjs/operators';
import {NavigationCancel, NavigationEnd, NavigationError, Router, RouterOutlet} from '@angular/router';
import {UUID} from '@scion/toolkit/uuid';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {AsyncPipe, DOCUMENT} from '@angular/common';
import {DOCUMENT} from '@angular/common';
import {WORKBENCH_ID, WorkbenchService, WorkbenchStartup, WorkbenchViewMenuItemDirective} from '@scion/workbench';
import {HeaderComponent} from './header/header.component';
import {fromEvent} from 'rxjs';
Expand All @@ -26,7 +26,6 @@ import {SettingsService} from './settings.service';
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [
AsyncPipe,
RouterOutlet,
HeaderComponent,
WorkbenchViewMenuItemDirective,
Expand Down
13 changes: 6 additions & 7 deletions docs/site/howto/how-to-configure-start-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,27 @@ bootstrapApplication(AppComponent, {
```

### How to configure a start page per perspective

If working with perspectives, configure a different start page per perspective by testing for the active perspective in the `canMatch` route handler.
Different perspectives can have a different start page. Use the `canMatchWorkbenchPerspective` guard to match a route only if the specified perspective is active.

```ts
import {bootstrapApplication} from '@angular/platform-browser';
import {provideRouter} from '@angular/router';
import {WorkbenchService} from '@scion/workbench';
import {canMatchWorkbenchPerspective} from '@scion/workbench';

bootstrapApplication(AppComponent, {
providers: [
provideRouter([
// Match this route only if 'perspective A' is active.
// Match this route only if 'perspective-a' is active.
{
path: '',
loadComponent: () => import('./perspective-a/start-page.component'),
canMatch: [() => inject(WorkbenchService).getPerspective('perspective-a')?.active],
canMatch: [canMatchWorkbenchPerspective('perspective-a')],
},
// Match this route only if 'perspective B' is active.
// Match this route only if 'perspective-b' is active.
{
path: '',
loadComponent: () => import('./perspective-b/start-page.component'),
canMatch: [() => inject(WorkbenchService).getPerspective('perspective-b')?.active],
canMatch: [canMatchWorkbenchPerspective('perspective-b')],
},
]),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {rejectWhenAttached, waitUntilAttached} from '../../../helper/testing.uti
import {Locator} from '@playwright/test';
import {SciCheckboxPO} from '../../../@scion/components.internal/checkbox.po';
import {SciKeyValueFieldPO} from '../../../@scion/components.internal/key-value-field.po';
import {WorkbenchLayout, WorkbenchLayoutFactory} from '@scion/workbench';
import {WorkbenchLayoutFn} from '@scion/workbench';
import {LayoutPages} from './layout-pages.po';
import {ɵWorkbenchLayout, ɵWorkbenchLayoutFactory} from './layout.model';

Expand All @@ -31,7 +31,7 @@ export class CreatePerspectivePagePO {
await this.enterData(definition.data);

// Enter the layout.
const {parts, views, viewNavigations} = definition.layout(new ɵWorkbenchLayoutFactory()) as ɵWorkbenchLayout;
const {parts, views, viewNavigations} = await definition.layout(new ɵWorkbenchLayoutFactory()) as ɵWorkbenchLayout;
await LayoutPages.enterParts(this.locator.locator('app-add-parts'), parts);
await LayoutPages.enterViews(this.locator.locator('app-add-views'), views);
await LayoutPages.enterViewNavigations(this.locator.locator('app-navigate-views'), viewNavigations);
Expand All @@ -54,7 +54,7 @@ export class CreatePerspectivePagePO {
}

export interface PerspectiveDefinition {
layout: (factory: WorkbenchLayoutFactory) => WorkbenchLayout;
layout: WorkbenchLayoutFn;
data?: {[key: string]: any};
transient?: true;
}
16 changes: 10 additions & 6 deletions projects/scion/e2e-testing/src/workbench/workbench-navigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {RouterPagePO} from './page-object/router-page.po';
import {ViewPagePO} from './page-object/view-page.po';
import {LayoutPagePO} from './page-object/layout-page/layout-page.po';
import {DialogOpenerPagePO} from './page-object/dialog-opener-page.po';
import {WorkbenchLayout, WorkbenchLayoutFactory} from '@scion/workbench';
import {WorkbenchLayout, WorkbenchLayoutFn} from '@scion/workbench';

export interface Type<T> extends Function { // eslint-disable-line @typescript-eslint/ban-types
new(...args: any[]): T;
Expand Down Expand Up @@ -104,10 +104,14 @@ export class WorkbenchNavigator {
* @see WorkbenchService.registerPerspective
* @see WorkbenchService.switchPerspective
*/
public async createPerspective(defineLayoutFn: (factory: WorkbenchLayoutFactory) => WorkbenchLayout): Promise<string> {
const id = crypto.randomUUID();
public async createPerspective(id: string, layoutFn: WorkbenchLayoutFn): Promise<string>;
public async createPerspective(layoutFn: WorkbenchLayoutFn): Promise<string>;
public async createPerspective(arg1: string | WorkbenchLayoutFn, arg2?: WorkbenchLayoutFn): Promise<string> {
const id = typeof arg1 === 'string' ? arg1 : crypto.randomUUID();
const layoutFn = typeof arg1 === 'function' ? arg1 : arg2!;

const layoutPage = await this.openInNewTab(LayoutPagePO);
await layoutPage.createPerspective(id, {layout: defineLayoutFn});
await layoutPage.createPerspective(id, {layout: layoutFn});
await layoutPage.view.tab.close();
await this._appPO.switchPerspective(id);
return id;
Expand All @@ -118,9 +122,9 @@ export class WorkbenchNavigator {
*
* @see WorkbenchRouter.navigate
*/
public async modifyLayout(modifyLayoutFn: (layout: WorkbenchLayout, activePartId: string) => WorkbenchLayout): Promise<void> {
public async modifyLayout(layoutFn: (layout: WorkbenchLayout, activePartId: string) => WorkbenchLayout): Promise<void> {
const layoutPage = await this.openInNewTab(LayoutPagePO);
await layoutPage.modifyLayout(modifyLayoutFn);
await layoutPage.modifyLayout(layoutFn);
await layoutPage.view.tab.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2018-2024 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 {expect} from '@playwright/test';
import {test} from '../fixtures';
import {MPart} from '../matcher/to-equal-workbench-layout.matcher';

test.describe('Workbench Perspective', () => {

test('should support back/forward browser navigation after switching perspective', async ({appPO, workbenchNavigator}) => {
await appPO.navigateTo({microfrontendSupport: false});

// Create perspective.
await workbenchNavigator.createPerspective('testee-1', factory => factory.addPart('part-1').addView('view.101', {partId: 'part-1'}));
await workbenchNavigator.createPerspective('testee-2', factory => factory.addPart('part-2').addView('view.102', {partId: 'part-2'}));

// Switch to perspective 1.
await appPO.switchPerspective('testee-1');

await expect.poll(() => appPO.getActivePerspectiveId()).toEqual('testee-1');
await expect(appPO.workbench).toEqualWorkbenchLayout({
workbenchGrid: {root: new MPart({id: 'part-1', views: [{id: 'view.101'}]})},
});

// Switch to perspective 2.
await appPO.switchPerspective('testee-2');

await expect.poll(() => appPO.getActivePerspectiveId()).toEqual('testee-2');
await expect(appPO.workbench).toEqualWorkbenchLayout({
workbenchGrid: {root: new MPart({id: 'part-2', views: [{id: 'view.102'}]})},
});

// Perform browser history back.
await appPO.navigateBack();

// Expect perspective 1 to be active.
await expect.poll(() => appPO.getActivePerspectiveId()).toEqual('testee-1');
await expect(appPO.workbench).toEqualWorkbenchLayout({
workbenchGrid: {root: new MPart({id: 'part-1', views: [{id: 'view.101'}]})},
});

// Perform browser history forward.
await appPO.navigateForward();

// Expect perspective 2 to be active.
await expect.poll(() => appPO.getActivePerspectiveId()).toEqual('testee-2');
await expect(appPO.workbench).toEqualWorkbenchLayout({
workbenchGrid: {root: new MPart({id: 'part-2', views: [{id: 'view.102'}]})},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ export class ɵWorkbenchLayoutFactory implements WorkbenchLayoutFactory {
* To control the identity of the initial part, pass an injector and set the DI token {@link MAIN_AREA_INITIAL_PART_ID}.
* - Grids and outlets can be passed in serialized or deserialized form.
*/
public create(options?: {workbenchGrid?: string | MPartGrid | null; mainAreaGrid?: string | MPartGrid | null; viewOutlets?: ViewOutlets | string; navigationStates?: NavigationStates; injector?: Injector; maximized?: boolean}): ɵWorkbenchLayout {
public create(options?: {workbenchGrid?: string | MPartGrid | null; mainAreaGrid?: string | MPartGrid | null; perspectiveId?: string; viewOutlets?: ViewOutlets | string; navigationStates?: NavigationStates; injector?: Injector; maximized?: boolean}): ɵWorkbenchLayout {
return runInInjectionContext(options?.injector ?? this._environmentInjector, () => new ɵWorkbenchLayout({
workbenchGrid: options?.workbenchGrid,
mainAreaGrid: options?.mainAreaGrid,
perspectiveId: options?.perspectiveId,
maximized: options?.maximized,
viewOutlets: options?.viewOutlets,
navigationStates: options?.navigationStates,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ export class ɵWorkbenchLayout implements WorkbenchLayout {

private _maximized: boolean;

/** Identifies the perspective of this layout, if any. */
public readonly perspectiveId: string | undefined;

/** @internal **/
constructor(config: {workbenchGrid?: string | MPartGrid | null; mainAreaGrid?: string | MPartGrid | null; viewOutlets?: string | ViewOutlets | null; navigationStates?: NavigationStates | null; maximized?: boolean}) {
constructor(config: {workbenchGrid?: string | MPartGrid | null; mainAreaGrid?: string | MPartGrid | null; perspectiveId?: string; viewOutlets?: string | ViewOutlets | null; navigationStates?: NavigationStates | null; maximized?: boolean}) {
this._grids = {
workbench: coerceMPartGrid(config.workbenchGrid, {default: createDefaultWorkbenchGrid}),
};
Expand All @@ -64,6 +67,7 @@ export class ɵWorkbenchLayout implements WorkbenchLayout {
this._viewOutlets = new Map<ViewId, UrlSegment[]>(Objects.entries(coerceViewOutlets(config.viewOutlets)));
this._navigationStates = new Map<ViewId, NavigationState>(Objects.entries(config.navigationStates ?? {}));
this.parts().forEach(part => assertType(part, {toBeOneOf: [MTreeNode, MPart]}));
this.perspectiveId = config.perspectiveId;
}

/**
Expand Down Expand Up @@ -817,6 +821,7 @@ export class ɵWorkbenchLayout implements WorkbenchLayout {
return runInInjectionContext(this._injector, () => new ɵWorkbenchLayout({
workbenchGrid: this._serializer.serializeGrid(this.workbenchGrid),
mainAreaGrid: this._serializer.serializeGrid(this._grids.mainArea),
perspectiveId: this.perspectiveId,
viewOutlets: Object.fromEntries(this._viewOutlets),
navigationStates: Object.fromEntries(this._navigationStates),
maximized: this._maximized,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {MicrofrontendDialogCapabilityValidator} from './microfrontend-dialog/mic
import {MicrofrontendMessageBoxIntentHandler} from './microfrontend-message-box/microfrontend-message-box-intent-handler.interceptor';
import {MicrofrontendMessageBoxCapabilityValidator} from './microfrontend-message-box/microfrontend-message-box-capability-validator.interceptor';
import {Defined} from '@scion/toolkit/util';
import {canMatchWorkbenchView} from '../view/workbench-view-route-guards';
import {canMatchWorkbenchView} from '../routing/workbench-route-guards';
import {WORKBENCH_AUXILIARY_ROUTE_OUTLET} from '../routing/workbench-auxiliary-route-installer.service';
import {Routing} from '../routing/routing.util';
import {TEXT_MESSAGE_BOX_CAPABILITY_ROUTE} from './microfrontend-host-message-box/text-message/text-message.component';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {WorkbenchLayoutFactory} from '../layout/workbench-layout.factory';
import {WorkbenchRouter} from '../routing/workbench-router.service';
import {provideRouter} from '@angular/router';
import {provideWorkbenchForTest} from '../testing/workbench.provider';
import {canMatchWorkbenchView} from '../view/workbench-view-route-guards';
import {canMatchWorkbenchPerspective, canMatchWorkbenchView} from '../routing/workbench-route-guards';
import {WorkbenchPerspective} from './workbench-perspective.model';
import {WORKBENCH_STARTUP} from '../startup/workbench-initializer';
import {WORKBENCH_PERSPECTIVE_REGISTRY} from './workbench-perspective.registry';
Expand Down Expand Up @@ -342,6 +342,7 @@ describe('Workbench Perspective', () => {
perspectives: [
{id: 'perspective-1', layout: (factory: WorkbenchLayoutFactory) => factory.addPart(MAIN_AREA)},
{id: 'perspective-2', layout: (factory: WorkbenchLayoutFactory) => factory.addPart(MAIN_AREA)},
{id: 'perspective-3', layout: (factory: WorkbenchLayoutFactory) => factory.addPart(MAIN_AREA)},
],
},
}),
Expand All @@ -350,13 +351,18 @@ describe('Workbench Perspective', () => {
path: '',
loadComponent: () => import('../testing/test.component'),
providers: [withComponentContent('Start Page Perspective 1')],
canMatch: [() => inject(WorkbenchService).activePerspective()?.id === 'perspective-1'],
canMatch: [canMatchWorkbenchPerspective('perspective-1')],
},
{
path: '',
loadComponent: () => import('../testing/test.component'),
providers: [withComponentContent('Start Page Perspective 2')],
canMatch: [() => inject(WorkbenchService).activePerspective()?.id === 'perspective-2'],
canMatch: [canMatchWorkbenchPerspective('perspective-2')],
},
{
path: '',
loadComponent: () => import('../testing/test.component'),
providers: [withComponentContent('Start Page')],
},
]),
],
Expand All @@ -371,6 +377,10 @@ describe('Workbench Perspective', () => {
await workbenchService.switchPerspective('perspective-2');
expect(fixture.debugElement.query(By.css('router-outlet + spec-test-component')).nativeElement.innerText).toEqual('Start Page Perspective 2');

// Switch to perspective-3
await workbenchService.switchPerspective('perspective-3');
expect(fixture.debugElement.query(By.css('router-outlet + spec-test-component')).nativeElement.innerText).toEqual('Start Page');

// Switch to perspective-1
await workbenchService.switchPerspective('perspective-1');
expect(fixture.debugElement.query(By.css('router-outlet + spec-test-component')).nativeElement.innerText).toEqual('Start Page Perspective 1');
Expand Down
Loading

0 comments on commit 5777728

Please sign in to comment.