Skip to content

Commit

Permalink
feat(workbench-client/view): provide part identity on view handle
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcarrian committed Jun 12, 2024
1 parent 43d8220 commit 762bec4
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<span class="e2e-view-id">{{view.id}}</span>
</sci-form-field>

<sci-form-field label="Part ID">
<span class="e2e-part-id">{{view.partId$ | async}}</span>
</sci-form-field>

<sci-form-field label="App Instance" title="Unique identifier of this app instance">
<span class="e2e-app-instance-id">{{appInstanceId}}</span>
</sci-form-field>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class ViewPagePO implements MicrofrontendViewPagePO {
public readonly locator: Locator;
public readonly view: ViewPO;
public readonly viewId: Locator;
public readonly partId: Locator;
public readonly outlet: SciRouterOutletPO;
public readonly path: Locator;

Expand All @@ -37,6 +38,7 @@ export class ViewPagePO implements MicrofrontendViewPagePO {
this.outlet = new SciRouterOutletPO(appPO, {name: locateBy?.viewId, cssClass: locateBy?.cssClass});
this.locator = this.outlet.frameLocator.locator('app-view-page');
this.viewId = this.locator.locator('span.e2e-view-id');
this.partId = this.locator.locator('span.e2e-part-id');
this.path = this.locator.locator('span.e2e-path');
}

Expand Down
35 changes: 35 additions & 0 deletions projects/scion/e2e-testing/src/workbench-client/view.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,41 @@ test.describe('Workbench View', () => {
await expect(testee3ViewPage.viewId).toHaveText('view.102');
});

test('should provide the view\'s part identity', async ({appPO, workbenchNavigator, microfrontendNavigator}) => {
await appPO.navigateTo({microfrontendSupport: true});

await microfrontendNavigator.registerCapability('app1', {
type: 'view',
qualifier: {component: 'testee'},
properties: {
path: 'test-view',
title: 'Testee',
},
});

// Add parts on the left and right.
await workbenchNavigator.modifyLayout(layout => {
return layout
.addPart('right', {align: 'right'})
.addPart('left', {align: 'left'});
});

const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'});

// Open view in the right part.
const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1');
await routerPage.navigate({component: 'testee'}, {
target: 'blank',
partId: 'right',
cssClass: 'testee',
});
await expect(testeeViewPage.partId).toHaveText('right');

// Move View to the left part.
await testeeViewPage.view.tab.moveTo('left');
await expect(testeeViewPage.partId).toHaveText('left');
});

test.describe('keystroke bubbling of view context menu items', () => {

test('should propagate `ctrl+k` for closing the current view', async ({appPO, microfrontendNavigator}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ test.describe('View Tabbar', () => {
direction: 'row',
ratio: .5,
}),
activePartId: 'left',
},
});

Expand Down
11 changes: 11 additions & 0 deletions projects/scion/workbench-client/src/lib/view/workbench-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ export abstract class WorkbenchView {
*/
public abstract readonly active$: Observable<boolean>;

/**
* Provides the part identity in which this view is contained in.
*
* Upon subscription, it emits the current part identity of this view, and then emits continuously when it changes. The Observable does not
* complete when navigating to another microfrontend of the same app. It only completes before unloading the web app, e.g., when closing
* the view or navigating to a microfrontend of another app. Consequently, do not forget to unsubscribe from this Observables before displaying
* another microfrontend.
*/
public abstract readonly partId$: Observable<string>;

/**
* Sets the title to be displayed in the view tab.
*/
Expand Down Expand Up @@ -176,6 +186,7 @@ export class ViewClosingEvent {
*/
export interface ViewSnapshot {
params: ReadonlyMap<string, any>;
partId: string | undefined;
}

/**
Expand Down
15 changes: 13 additions & 2 deletions projects/scion/workbench-client/src/lib/view/ɵworkbench-view.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2018-2024 Swiss Federal Railways
*
* This program and the accompanying materials are made
Expand Down Expand Up @@ -36,11 +36,13 @@ export class ɵWorkbenchView implements WorkbenchView, PreDestroy {
private _canCloseSubscription: Subscription | undefined;

public active$: Observable<boolean>;
public partId$: Observable<string>;
public params$: Observable<ReadonlyMap<string, any>>;
public capability$: Observable<WorkbenchViewCapability>;
public whenInitialParams: Promise<void>;
public snapshot: ViewSnapshot = {
params: new Map<string, any>(),
partId: undefined,
};

constructor(public id: ViewId) {
Expand All @@ -66,12 +68,21 @@ export class ɵWorkbenchView implements WorkbenchView, PreDestroy {
this.active$ = Beans.get(MessageClient).observe$<boolean>(ɵWorkbenchCommands.viewActiveTopic(this.id))
.pipe(
mapToBody(),
distinctUntilChanged(),
shareReplay({refCount: false, bufferSize: 1}),
decorateObservable(),
takeUntil(this._beforeUnload$),
);

this.partId$ = Beans.get(MessageClient).observe$<string>(ɵWorkbenchCommands.viewPartIdTopic(this.id))
.pipe(
mapToBody(),
shareReplay({refCount: false, bufferSize: 1}),
decorateObservable(),
takeUntil(this._beforeUnload$),
);
// Update part id snapshot when part changes.
this.partId$.subscribe(partId => this.snapshot.partId = partId);

// Notify when received initial params.
this.whenInitialParams = new Promise(resolve => this.params$.pipe(take(1)).subscribe(() => resolve()));
// Update params snapshot when params change.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ export const ɵWorkbenchCommands = {
*/
viewActiveTopic: (viewId: ViewId) => `ɵworkbench/views/${viewId}/active`,

/**
* Computes the topic for notifying about the part identity in which this view is contained in.
*
* The part identity is published as a retained message.
*/
viewPartIdTopic: (viewId: ViewId) => `ɵworkbench/views/${viewId}/part/id`,

/**
* Computes the topic to request closing confirmation of a view.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ import {Message, MessageClient, MessageHeaders} from '@scion/microfrontend-platf
import {Logger} from '../../logging';
import {ViewId, WorkbenchView} from '../../view/workbench-view.model';
import {WorkbenchViewRegistry} from '../../view/workbench-view.registry';
import {map, switchMap} from 'rxjs/operators';
import {ɵWorkbenchCommands} from '@scion/workbench-client';
import {merge, Subscription} from 'rxjs';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {Subscription} from 'rxjs';
import {MicrofrontendWorkbenchView} from './microfrontend-workbench-view.model';

/**
Expand All @@ -32,30 +30,13 @@ export class MicrofrontendViewCommandHandler implements OnDestroy {
constructor(private _messageClient: MessageClient,
private _viewRegistry: WorkbenchViewRegistry,
private _logger: Logger) {
this.installViewActiveStatePublisher();

this._subscriptions.add(this.installViewTitleCommandHandler());
this._subscriptions.add(this.installViewHeadingCommandHandler());
this._subscriptions.add(this.installViewDirtyCommandHandler());
this._subscriptions.add(this.installViewClosableCommandHandler());
this._subscriptions.add(this.installViewCloseCommandHandler());
}

/**
* Notifies microfrontends about the active state of the embedding view.
*/
private installViewActiveStatePublisher(): void {
this._viewRegistry.views$
.pipe(
switchMap(views => merge(...views.map(view => view.active$.pipe(map(() => view))))),
takeUntilDestroyed(),
)
.subscribe((view: WorkbenchView) => {
const commandTopic = ɵWorkbenchCommands.viewActiveTopic(view.id);
this._messageClient.publish(commandTopic, view.active, {retain: true}).then();
});
}

/**
* Handles commands to update the title of a view.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import {ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, DestroyRef, ElementRef, inject, Inject, Injector, OnDestroy, OnInit, Provider, runInInjectionContext, ViewChild} from '@angular/core';
import {ActivatedRoute, Params} from '@angular/router';
import {combineLatest, firstValueFrom, Observable, of, Subject, switchMap} from 'rxjs';
import {first, map, takeUntil} from 'rxjs/operators';
import {filter, first, map, takeUntil} from 'rxjs/operators';
import {ManifestService, mapToBody, MessageClient, MessageHeaders, MicrofrontendPlatformConfig, OutletRouter, ResponseStatusCodes, SciRouterOutletElement, TopicMessage} from '@scion/microfrontend-platform';
import {ManifestObjectCache} from '../manifest-object-cache.service';
import {WorkbenchViewCapability, ɵMicrofrontendRouteParams, ɵVIEW_ID_CONTEXT_KEY, ɵViewParamsUpdateCommand, ɵWorkbenchCommands} from '@scion/workbench-client';
Expand Down Expand Up @@ -104,6 +104,8 @@ export class MicrofrontendViewComponent implements OnInit, OnDestroy, CanClose {
this.keystrokesToBubble$ = combineLatest([this.viewContextMenuKeystrokes$(), of(this._universalKeystrokes)])
.pipe(map(keystrokes => new Array<string>().concat(...keystrokes)));
this.installWorkbenchDragDetector();
this.installViewActiveUpdater();
this.installPartIdUpdater();
this.splash = inject(MicrofrontendPlatformConfig).splash ?? MicrofrontendSplashComponent;
}

Expand Down Expand Up @@ -239,6 +241,25 @@ export class MicrofrontendViewComponent implements OnInit, OnDestroy, CanClose {
this.view.dirty = false;
}

private installViewActiveUpdater(): void {
this.view.active$.pipe(takeUntilDestroyed()).subscribe(active => {
const commandTopic = ɵWorkbenchCommands.viewActiveTopic(this.view.id);
this._messageClient.publish(commandTopic, active, {retain: true}).then();
});
}

private installPartIdUpdater(): void {
this.view.partId$
.pipe(
filter(Boolean),
takeUntilDestroyed(),
)
.subscribe(partId => {
const commandTopic = ɵWorkbenchCommands.viewPartIdTopic(this.view.id);
this._messageClient.publish(commandTopic, partId, {retain: true}).then();
});
}

/**
* Promise that resolves once params contain the given capability id.
*/
Expand Down Expand Up @@ -317,6 +338,7 @@ export class MicrofrontendViewComponent implements OnInit, OnDestroy, CanClose {
// Delete retained messages to free resources.
this._messageClient.publish(ɵWorkbenchCommands.viewActiveTopic(this.view.id), undefined, {retain: true}).then();
this._messageClient.publish(ɵWorkbenchCommands.viewParamsTopic(this.view.id), undefined, {retain: true}).then();
this._messageClient.publish(ɵWorkbenchCommands.viewPartIdTopic(this.view.id), undefined, {retain: true}).then();
this._outletRouter.navigate(null, {outlet: this.view.id}).then();
this.view.unregisterAdapter(MicrofrontendWorkbenchView);
}
Expand Down
12 changes: 9 additions & 3 deletions projects/scion/workbench/src/lib/view/ɵworkbench-view.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export class ɵWorkbenchView implements WorkbenchView, Blockable {
private _activationInstant: number | undefined;
private _closable = true;

private _part: ɵWorkbenchPart | undefined;
public uid!: UUID;
public alternativeId: string | undefined;
public navigationId: string | undefined;
Expand All @@ -75,6 +74,7 @@ export class ɵWorkbenchView implements WorkbenchView, Blockable {
public scrollTop = 0;
public scrollLeft = 0;

public readonly partId$ = new BehaviorSubject<string | undefined>(undefined);
public readonly active$ = new BehaviorSubject<boolean>(false);
public readonly menuItems$: Observable<WorkbenchMenuItem[]>;
public readonly blockedBy$ = new BehaviorSubject<ɵWorkbenchDialog | null>(null);
Expand Down Expand Up @@ -141,7 +141,10 @@ export class ɵWorkbenchView implements WorkbenchView, Blockable {
this.state = layout.viewState({viewId: this.id});
this.classList.set(mView.cssClass, {scope: 'layout'});
this.classList.set(mView.navigation?.cssClass, {scope: 'navigation'});
this._part = this._partRegistry.get(mPart.id);

if (mPart.id !== this.partId$.value) {
this.partId$.next(mPart.id);
}

const active = mPart.activeViewId === this.id;
if (active !== this.active) {
Expand Down Expand Up @@ -252,7 +255,10 @@ export class ɵWorkbenchView implements WorkbenchView, Blockable {

/** @inheritDoc */
public get part(): WorkbenchPart {
return this._part ?? throwError(`[NullPartError] Part reference missing for view '${this.id}'.`);
if (!this.partId$.value) {
throw Error(`[NullPartError] Part reference missing for view '${this.id}'.`);
}
return this._partRegistry.get(this.partId$.value);
}

/** @inheritDoc */
Expand Down

0 comments on commit 762bec4

Please sign in to comment.