Skip to content

Commit

Permalink
fix(workbench): update model properties at the right time (after dest…
Browse files Browse the repository at this point in the history
…ruction of previous component and before creation of new component)
  • Loading branch information
danielwiehl committed Jul 12, 2024
1 parent 94fa086 commit a9df03d
Show file tree
Hide file tree
Showing 33 changed files with 2,588 additions and 825 deletions.
30 changes: 24 additions & 6 deletions projects/scion/workbench/src/lib/common/operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
* SPDX-License-Identifier: EPL-2.0
*/

import {audit, MonoTypeOperatorFunction, Observable, OperatorFunction} from 'rxjs';
import {audit, MonoTypeOperatorFunction, Observable, of, OperatorFunction} from 'rxjs';
import {filter, mergeMap} from 'rxjs/operators';
import {inject} from '@angular/core';
import {WorkbenchLayoutService} from '../layout/workbench-layout.service';
import {ɵDestroyRef} from './ɵdestroy-ref';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';

/**
* Serializes the execution of elements emitted by the source Observable.
Expand All @@ -32,10 +34,26 @@ export function filterNull<T>(): OperatorFunction<T | null, T> {
}

/**
* Buffers the most recent value from the source Observable until the next layout change.
* Use this operator to avoid emitting a partially updated layout.
* Mirrors the source Observable if the layout is stable. If the layout is not stable,
* retains the most recent value and emits it once the layout becomes stable.
*
* Use this operator to prevent emitting values while the layout is unstable, ensuring
* that only the most recent value is emitted once the layout stabilizes.
*/
export function bufferLatestUntilLayoutChange<T>(): MonoTypeOperatorFunction<T> {
const onLayoutChange$ = inject(WorkbenchLayoutService).onLayoutChange$;
return audit(() => onLayoutChange$);
export function bufferLatestUntilLayoutStable<T>(): MonoTypeOperatorFunction<T> {
const layoutService = inject(WorkbenchLayoutService);

return (source$: Observable<T>): Observable<T> => {
return new Observable(observer => {
const destroyRef = new ɵDestroyRef();
source$
.pipe(
audit(() => layoutService.stable ? of(null) : layoutService.onLayoutChange$),
takeUntilDestroyed(destroyRef),
)
.subscribe(observer);

return () => destroyRef.destroy();
});
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {MPartGridV2, MPartV2, MTreeNodeV2, MViewV2} from './model/workbench-layo
import {MPartGridV3, MPartV3, MTreeNodeV3, MViewV3, VIEW_ID_PREFIX_V3, ViewIdV3} from './model/workbench-layout-migration-v3.model';
import {Router, UrlTree} from '@angular/router';
import {WorkbenchMigration} from '../../migration/workbench-migration';
import {RouterUtils} from '../../routing/router.util';
import {Routing} from '../../routing/routing.util';

/**
* Migrates the workbench layout from version 2 to version 3.
Expand All @@ -33,7 +33,7 @@ export class WorkbenchLayoutMigrationV3 implements WorkbenchMigration {
// Otherwise, when migrating the main area and using a view id already present in the perspective,
// the view outlet would not be removed from the URL, resulting the migrated view to display
// "Not Found" page or incorrect content.
const viewOutlets = RouterUtils.parseViewOutlets(this.getCurrentUrl());
const viewOutlets = Routing.parseViewOutlets(this.getCurrentUrl());
const usedViewIds = new Set<ViewIdV3>([...viewOutlets.keys(), ...collectViewIds(partGridV2.root)]);

// Migrate the grid.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export class WorkbenchLayoutService {
*/
public readonly dragging$: Observable<'start' | 'end'>;

/**
* Indicates whether the layout is stable., i.e., is not currently changing.
*/
public stable = true;

constructor(viewDragService: ViewDragService) {
this.dragging$ = merge(
merge(this._dragStart$, viewDragService.viewDragStart$).pipe(map((): 'start' => 'start')),
Expand All @@ -67,10 +72,15 @@ export class WorkbenchLayoutService {
}

/**
* Sets the given {@link WorkbenchLayout}.
* Sets the given {@link WorkbenchLayout}, but only if a different layout instance.
*/
public setLayout(layout: ɵWorkbenchLayout): void {
public setLayout(layout: ɵWorkbenchLayout): boolean {
if (layout === this._layout$.value) {
return false;
}

this._layout$.next(layout);
return true;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions projects/scion/workbench/src/lib/layout/ɵworkbench-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {GridSerializationFlags, WorkbenchLayoutSerializer} from './workench-layo
import {WorkbenchViewRegistry} from '../view/workbench-view.registry';
import {WorkbenchPartRegistry} from '../part/workbench-part.registry';
import {inject, Injectable, InjectionToken, Injector, Predicate, runInInjectionContext} from '@angular/core';
import {RouterUtils} from '../routing/router.util';
import {Commands, ViewOutlets, ViewState, ViewStates} from '../routing/routing.model';
import {Routing} from '../routing/routing.util';
import {ActivatedRoute, UrlSegment} from '@angular/router';
import {ViewId} from '../view/workbench-view.model';
import {Arrays} from '@scion/toolkit/util';
Expand Down Expand Up @@ -558,7 +558,7 @@ export class ɵWorkbenchLayout implements WorkbenchLayout {
throw Error('[NavigateError] Commands, relativeTo or hint must be set.');
}

const urlSegments = runInInjectionContext(this._injector, () => RouterUtils.commandsToSegments(commands, {relativeTo: extras?.relativeTo}));
const urlSegments = runInInjectionContext(this._injector, () => Routing.commandsToSegments(commands, {relativeTo: extras?.relativeTo}));
if (urlSegments.length) {
this._viewOutlets.set(view.id, urlSegments);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import {Component, inject, Injector, Input, OnDestroy, OnInit, runInInjectionContext, StaticProvider} from '@angular/core';
import {WorkbenchDialog as WorkbenchClientDialog, WorkbenchDialogCapability} from '@scion/workbench-client';
import {RouterUtils} from '../../routing/router.util';
import {Routing} from '../../routing/routing.util';
import {Commands} from '../../routing/routing.model';
import {Router, RouterOutlet} from '@angular/router';
import {ɵWorkbenchDialog} from '../../dialog/ɵworkbench-dialog';
Expand Down Expand Up @@ -75,7 +75,7 @@ export class MicrofrontendHostDialogComponent implements OnDestroy, OnInit {
private navigate(path: string | null, extras?: {params?: Map<string, any>}): Promise<boolean> {
path = Microfrontends.substituteNamedParameters(path, extras?.params);

const outletCommands: Commands | null = (path !== null ? runInInjectionContext(this._injector, () => RouterUtils.pathToCommands(path!)) : null);
const outletCommands: Commands | null = (path !== null ? runInInjectionContext(this._injector, () => Routing.pathToCommands(path!)) : null);
const commands: Commands = [{outlets: {[this.outletName]: outletCommands}}];
return this._singleNavigationExecutor.submit(() => this._router.navigate(commands, {skipLocationChange: true, queryParamsHandling: 'preserve'}));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import {Component, ElementRef, inject, Injector, Input, OnDestroy, OnInit, runInInjectionContext, StaticProvider} from '@angular/core';
import {WorkbenchMessageBox, WorkbenchMessageBoxCapability} from '@scion/workbench-client';
import {RouterUtils} from '../../routing/router.util';
import {Routing} from '../../routing/routing.util';
import {Commands} from '../../routing/routing.model';
import {Router, RouterOutlet} from '@angular/router';
import {NgTemplateOutlet} from '@angular/common';
Expand Down Expand Up @@ -75,7 +75,7 @@ export class MicrofrontendHostMessageBoxComponent implements OnDestroy, OnInit {
private navigate(path: string | null, extras?: {params?: Map<string, any>}): Promise<boolean> {
path = Microfrontends.substituteNamedParameters(path, extras?.params);

const outletCommands: Commands | null = (path !== null ? runInInjectionContext(this._injector, () => RouterUtils.pathToCommands(path!)) : null);
const outletCommands: Commands | null = (path !== null ? runInInjectionContext(this._injector, () => Routing.pathToCommands(path!)) : null);
const commands: Commands = [{outlets: {[this.outletName]: outletCommands}}];
return this._singleNavigationExecutor.submit(() => this._router.navigate(commands, {skipLocationChange: true, queryParamsHandling: 'preserve'}));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import {Component, inject, Injector, OnDestroy, runInInjectionContext, StaticProvider} from '@angular/core';
import {WorkbenchPopup, ɵPopupContext} from '@scion/workbench-client';
import {RouterUtils} from '../../routing/router.util';
import {Routing} from '../../routing/routing.util';
import {Commands} from '../../routing/routing.model';
import {Router, RouterOutlet} from '@angular/router';
import {Popup, ɵPopup} from '../../popup/popup.config';
Expand Down Expand Up @@ -71,7 +71,7 @@ export class MicrofrontendHostPopupComponent implements OnDestroy {
private navigate(path: string | null, extras: {outletName: string; params?: Map<string, any>}): Promise<boolean> {
path = Microfrontends.substituteNamedParameters(path, extras.params);

const outletCommands: Commands | null = (path !== null ? runInInjectionContext(this._injector, () => RouterUtils.pathToCommands(path!)) : null);
const outletCommands: Commands | null = (path !== null ? runInInjectionContext(this._injector, () => Routing.pathToCommands(path!)) : null);
const commands: Commands = [{outlets: {[extras.outletName]: outletCommands}}];
return this._singleNavigationExecutor.submit(() => this._router.navigate(commands, {skipLocationChange: true, queryParamsHandling: 'preserve'}));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {MicrofrontendMessageBoxCapabilityValidator} from './microfrontend-messag
import {Defined} from '@scion/toolkit/util';
import {canMatchWorkbenchView} from '../view/workbench-view-route-guards';
import {WORKBENCH_AUXILIARY_ROUTE_OUTLET} from '../routing/workbench-auxiliary-route-installer.service';
import {RouterUtils} from '../routing/router.util';
import {Routing} from '../routing/routing.util';
import {TEXT_MESSAGE_BOX_CAPABILITY_ROUTE} from './microfrontend-host-message-box/text-message/text-message.component';
import {MicrofrontendMessageBoxLegacyIntentTranslator} from './microfrontend-message-box/microfrontend-message-box-legacy-intent-translator.interceptor';
import {MicrofrontendPerspectiveCapabilityValidator} from './microfrontend-perspective/microfrontend-perspective-capability-validator.interceptor';
Expand Down Expand Up @@ -190,6 +190,6 @@ function provideBuiltInTextMessageBoxCapabilityRoute(): EnvironmentProviders {
function canMatchWorkbenchMessageBox(): CanMatchFn {
return () => {
const outlet = inject(WORKBENCH_AUXILIARY_ROUTE_OUTLET, {optional: true});
return RouterUtils.isMessageBoxOutlet(outlet);
return Routing.isMessageBoxOutlet(outlet);
};
}
2 changes: 2 additions & 0 deletions projects/scion/workbench/src/lib/part/part.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {WorkbenchPortalOutletDirective} from '../portal/workbench-portal-outlet.
import {ViewPortalPipe} from '../view/view-portal.pipe';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {WORKBENCH_ID} from '../workbench-id';
import {bufferLatestUntilLayoutStable} from '../common/operators';

@Component({
selector: 'wb-part',
Expand Down Expand Up @@ -112,6 +113,7 @@ export class PartComponent implements OnInit, OnDestroy {
private constructInactiveViewComponents(): void {
this.part.viewIds$
.pipe(
bufferLatestUntilLayoutStable(),
mapArray(viewId => this._viewRegistry.get(viewId)),
filterArray(view => !view.active && !view.portal.isConstructed),
mergeMap(views => from(views)),
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@

import {Injectable, OnDestroy} from '@angular/core';
import {ɵWorkbenchPart} from './ɵworkbench-part.model';
import {concat, Observable} from 'rxjs';
import {Observable} from 'rxjs';
import {WorkbenchObjectRegistry} from '../registry/workbench-object-registry';
import {take} from 'rxjs/operators';
import {bufferLatestUntilLayoutChange} from '../common/operators';

/**
* Registry for {@link WorkbenchPart} model objects.
Expand All @@ -26,10 +24,7 @@ export class WorkbenchPartRegistry implements OnDestroy {
nullObjectErrorFn: partId => Error(`[NullPartError] Part '${partId}' not found.`),
});

public parts$: Observable<readonly ɵWorkbenchPart[]> = concat(
this._registry.objects$.pipe(take(1)), // immediate emission upon subscription
this._registry.objects$.pipe(bufferLatestUntilLayoutChange()),
);
public parts$: Observable<readonly ɵWorkbenchPart[]> = this._registry.objects$;

/**
* Registers given part.
Expand Down
Loading

0 comments on commit a9df03d

Please sign in to comment.