@scion/workbench-client
Version:
SCION Workbench Client provides core API for a web app to interact with SCION Workbench and other microfrontends. It is a pure TypeScript library based on the framework-agnostic `@scion/microfrontend-platform` library and can be used with any web stack.
1,255 lines (1,226 loc) • 71.6 kB
JavaScript
import { Beans } from '@scion/toolkit/bean-manager';
import { IntentClient, mapToBody, RequestError, MessageClient, ObservableDecorator, MicrofrontendPlatformClient, ManifestService, ContextService, OUTLET_CONTEXT, MicrofrontendPlatform, PlatformState, APP_IDENTITY, IS_PLATFORM_HOST, ACTIVATION_CONTEXT } from '@scion/microfrontend-platform';
import { lastValueFrom, Subject, merge, firstValueFrom, combineLatest, pipe, concat, NEVER, catchError, throwError, Subscription, of, timeout, share, ReplaySubject, first, switchMap as switchMap$1 } from 'rxjs';
import { map, shareReplay, takeUntil, distinctUntilChanged, skip, mergeMap, filter, tap, switchMap, finalize } from 'rxjs/operators';
import { Maps, Dictionaries, Observables, Defined, Arrays } from '@scion/toolkit/util';
import { fromBoundingClientRect$ } from '@scion/toolkit/observable';
import { UUID } from '@scion/toolkit/uuid';
/*
* Copyright (c) 2018-2022 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
*/
/**
* A view is a visual workbench element for displaying content stacked or side-by-side in the workbench layout.
*
* Users can drag views from one part to another, even across windows, or place them side-by-side, horizontally and vertically.
*
* The view microfrontend can inject this handle to interact with the view.
*
* @category View
* @see WorkbenchViewCapability
* @see WorkbenchRouter
*/
class WorkbenchView {
}
/*
* 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
*/
/**
* Defines command endpoints for the communication between SCION Workbench and SCION Workbench Client.
*
* @docs-private Not public API, intended for internal use only.
*/
const ɵWorkbenchCommands = {
/**
* Computes the topic via which the title of a workbench view tab can be set.
*/
viewTitleTopic: (viewId) => `ɵworkbench/views/${viewId}/title`,
/**
* Computes the topic via which the heading of a workbench view tab can be set.
*/
viewHeadingTopic: (viewId) => `ɵworkbench/views/${viewId}/heading`,
/**
* Computes the topic via which a view tab can be marked dirty or pristine.
*/
viewDirtyTopic: (viewId) => `ɵworkbench/views/${viewId}/dirty`,
/**
* Computes the topic via which a view tab can be made closable.
*/
viewClosableTopic: (viewId) => `ɵworkbench/views/${viewId}/closable`,
/**
* Computes the topic via which a view can be closed.
*/
viewCloseTopic: (viewId) => `ɵworkbench/views/${viewId}/close`,
/**
* Computes the topic to notify the active state of a view.
*
* The active state is published as a retained message.
*/
viewActiveTopic: (viewId) => `ɵworkbench/views/${viewId}/active`,
/**
* Computes the topic to notify the focused state of a view.
*
* The focused state is published as a retained message.
*/
viewFocusedTopic: (viewId) => `ɵworkbench/views/${viewId}/focused`,
/**
* Computes the topic to notify the part of a view.
*
* The part identity is published as a retained message.
*/
viewPartIdTopic: (viewId) => `ɵworkbench/views/${viewId}/part/id`,
/**
* Computes the topic to request closing confirmation of a view.
*
* When closing a view and if the microfrontend has subscribed to this topic, the workbench requests closing confirmation
* via this topic. By sending a `true` reply, the workbench continues with closing the view, by sending a `false` reply,
* closing is prevented.
*/
canCloseTopic: (viewId) => `ɵworkbench/views/${viewId}/canClose`,
/**
* Computes the topic for signaling that a microfrontend is about to be replaced by a microfrontend of another app.
*/
viewUnloadingTopic: (viewId) => `ɵworkbench/views/${viewId}/unloading`,
/**
* Computes the topic for updating params of a microfrontend view.
*/
viewParamsUpdateTopic: (viewId, viewCapabilityId) => `ɵworkbench/views/${viewId}/capabilities/${viewCapabilityId}/params/update`,
/**
* Computes the topic for providing params to a view microfrontend.
*
* Params include the {@link ɵMicrofrontendRouteParams#ɵVIEW_CAPABILITY_ID capability id}, params as passed in {@link WorkbenchNavigationExtras.params},
* and the view qualifier.
*
* Params are published as a retained message.
*/
viewParamsTopic: (viewId) => `ɵworkbench/views/${viewId}/params`,
/**
* Computes the topic for observing the popup origin.
*/
popupOriginTopic: (popupId) => `ɵworkbench/popups/${popupId}/origin`,
/**
* Computes the topic to notify the focused state of a popup.
*
* The focused state is published as a retained message.
*/
popupFocusedTopic: (popupId) => `ɵworkbench/popups/${popupId}/focused`,
/**
* Computes the topic via which a popup can be closed.
*/
popupCloseTopic: (popupId) => `ɵworkbench/popups/${popupId}/close`,
/**
* Computes the topic via which to set a result
*/
popupResultTopic: (popupId) => `ɵworkbench/popups/${popupId}/result`,
/**
* Computes the topic via which the title of a dialog can be set.
*/
dialogTitleTopic: (dialogId) => `ɵworkbench/dialogs/${dialogId}/title`,
/**
* Computes the topic to notify the focused state of a dialog.
*
* The focused state is published as a retained message.
*/
dialogFocusedTopic: (dialogId) => `ɵworkbench/dialogs/${dialogId}/focused`,
/**
* Computes the topic via which a dialog can be closed.
*/
dialogCloseTopic: (dialogId) => `ɵworkbench/dialogs/${dialogId}/close`,
};
/*
* 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
*/
/**
* Built-in workbench capabilities.
*/
var WorkbenchCapabilities;
(function (WorkbenchCapabilities) {
/**
* Contributes a microfrontend for display in workbench view.
*
* A view is a visual workbench element for displaying content stacked or side-by-side.
*/
WorkbenchCapabilities["View"] = "view";
/**
* Contributes a perspective to the workbench.
*
* A perspective is a named arrangement of views. Different perspectives provide a different perspective on the application.
*/
WorkbenchCapabilities["Perspective"] = "perspective";
/**
* Contributes a microfrontend for display in workbench popup.
*
* A popup is a visual workbench component for displaying content above other content.
*/
WorkbenchCapabilities["Popup"] = "popup";
/**
* Contributes a microfrontend for display in workbench dialog.
*
* A dialog is a visual element for focused interaction with the user, such as prompting the user for input or confirming actions.
*/
WorkbenchCapabilities["Dialog"] = "dialog";
/**
* Contributes a message box in the host app.
*
* A message box is a standardized dialog for presenting a message to the user, such as an info, warning or alert,
* or for prompting the user for confirmation.
*/
WorkbenchCapabilities["MessageBox"] = "messagebox";
/**
* Contributes a notification in the host app.
*
* A notification appears in the upper-right corner and disappears automatically after a few seconds.
* It informs the user of a system event, e.g., that a task has been completed or an error has occurred.
*/
WorkbenchCapabilities["Notification"] = "notification";
/**
* Provides texts to the SCION Workbench and micro apps.
*/
WorkbenchCapabilities["TextProvider"] = "text-provider";
})(WorkbenchCapabilities || (WorkbenchCapabilities = {}));
/*
* Copyright (c) 2018-2022 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
*/
/**
* Enables navigation of workbench views.
*
* A view is a visual workbench element for displaying content side-by-side or stacked.
*
* A microfrontend provided as a view capability can be opened in a view. The qualifier differentiates between different
* view capabilities. An application can open the public view capabilities of other applications if it manifests a respective
* intention.
*
* @category Router
* @category View
*/
class WorkbenchRouter {
/**
* Navigates to a microfrontend of a view capability based on the given qualifier and extras.
*
* By default, the router opens a new view if no view is found that matches the specified qualifier and required params. Optional parameters do not affect view resolution.
* If one or more views match the qualifier and required params, they will be navigated instead of opening the microfrontend in a new view tab.
* This behavior can be changed by setting an explicit navigation target in navigation extras.
*
* @param qualifier - Identifies the view capability that provides the microfrontend to display in a view.
* Passing an empty qualifier (`{}`) allows the microfrontend to update its parameters, restoring updated parameters when the page reloads.
* Parameter handling can be controlled using the {@link WorkbenchNavigationExtras#paramsHandling} option.
* @param extras - Options to control navigation.
* @return Promise that resolves to `true` on successful navigation, or `false` otherwise.
*/
async navigate(qualifier, extras) {
if (this.isSelfNavigation(qualifier)) {
return this.updateViewParams(extras);
}
else {
return this.issueViewIntent(qualifier, extras);
}
}
async issueViewIntent(qualifier, extras) {
const navigationExtras = {
...extras,
params: undefined, // included in the intent
paramsHandling: undefined, // only applicable for self-navigation
};
const navigate$ = Beans.get(IntentClient).request$({ type: WorkbenchCapabilities.View, qualifier, params: Maps.coerce(extras?.params) }, navigationExtras);
try {
return await lastValueFrom(navigate$.pipe(mapToBody()));
}
catch (error) {
throw (error instanceof RequestError ? error.message : error);
}
}
async updateViewParams(extras) {
const viewCapabilityId = Beans.get(WorkbenchView).snapshot.params.get(ɵMicrofrontendRouteParams.ɵVIEW_CAPABILITY_ID);
if (viewCapabilityId === undefined) {
return false; // Params cannot be updated until the loading of the view is completed
}
const command = {
params: Dictionaries.coerce(extras?.params),
paramsHandling: extras?.paramsHandling,
};
const updateParams$ = Beans.get(MessageClient).request$(ɵWorkbenchCommands.viewParamsUpdateTopic(Beans.get(WorkbenchView).id, viewCapabilityId), command);
try {
return await lastValueFrom(updateParams$.pipe(mapToBody()));
}
catch (error) {
throw (error instanceof RequestError ? error.message : error);
}
}
isSelfNavigation(qualifier) {
if (Object.keys(qualifier).length === 0) {
if (!Beans.opt(WorkbenchView)) {
throw Error('[NavigateError] Self-navigation is supported only if in the context of a view.');
}
return true;
}
return false;
}
}
/**
* Named parameters used in microfrontend routes.
*
* @docs-private Not public API, intended for internal use only.
* @ignore
*/
var ɵMicrofrontendRouteParams;
(function (ɵMicrofrontendRouteParams) {
/**
* Named path segment in the microfrontend route representing the view capability for which to embed its microfrontend.
*/
ɵMicrofrontendRouteParams["\u0275VIEW_CAPABILITY_ID"] = "\u0275ViewCapabilityId";
})(ɵMicrofrontendRouteParams || (ɵMicrofrontendRouteParams = {}));
/*
* Copyright (c) 2018-2022 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
*/
/**
* Decorates the source with registered {@link ObservableDecorator}, if any.
*
* @internal
*/
function decorateObservable() {
return (source$) => Beans.opt(ObservableDecorator)?.decorate$(source$) ?? source$;
}
/*
* 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
*/
class ɵWorkbenchView {
id;
_propertyChange$ = new Subject();
_destroy$ = new Subject();
/**
* Observable that emits when the application loaded into the current view receives an unloading event,
* i.e., is just about to be replaced by a microfrontend of another application.
*/
_beforeUnload$;
/**
* Observable that emits before navigating to a different microfrontend of the same app.
*/
_beforeInAppNavigation$ = new Subject();
_canCloseFn;
_canCloseSubscription;
active$;
focused$;
partId$;
params$;
capability$;
whenProperties;
snapshot = {
params: new Map(),
partId: undefined,
active: false,
focused: false,
};
constructor(id) {
this.id = id;
this._beforeUnload$ = Beans.get(MessageClient).observe$(ɵWorkbenchCommands.viewUnloadingTopic(this.id))
.pipe(map(() => undefined), shareReplay({ refCount: false, bufferSize: 1 }));
this.params$ = Beans.get(MessageClient).observe$(ɵWorkbenchCommands.viewParamsTopic(this.id))
.pipe(mapToBody(), shareReplay({ refCount: false, bufferSize: 1 }), decorateObservable(), takeUntil(merge(this._beforeUnload$, this._destroy$)));
this.capability$ = this.params$
.pipe(map(params => params.get(ɵMicrofrontendRouteParams.ɵVIEW_CAPABILITY_ID)), lookupViewCapabilityAndShareReplay(), decorateObservable(), takeUntil(this._beforeUnload$));
this.active$ = Beans.get(MessageClient).observe$(ɵWorkbenchCommands.viewActiveTopic(this.id))
.pipe(mapToBody(), shareReplay({ refCount: false, bufferSize: 1 }), decorateObservable(), takeUntil(this._beforeUnload$));
this.focused$ = Beans.get(MessageClient).observe$(ɵWorkbenchCommands.viewFocusedTopic(this.id))
.pipe(mapToBody(), shareReplay({ refCount: false, bufferSize: 1 }), decorateObservable(), takeUntil(this._beforeUnload$));
this.partId$ = Beans.get(MessageClient).observe$(ɵWorkbenchCommands.viewPartIdTopic(this.id))
.pipe(mapToBody(), shareReplay({ refCount: false, bufferSize: 1 }), decorateObservable(), takeUntil(this._beforeUnload$));
// Update part id snapshot when part changes.
this.partId$
.pipe(takeUntil(this._destroy$))
.subscribe(partId => this.snapshot.partId = partId);
// Update params snapshot when params change.
this.params$
.pipe(takeUntil(this._destroy$))
.subscribe(params => this.snapshot.params = new Map(params));
// Update active snapshot when the active state changes.
this.active$
.pipe(takeUntil(this._destroy$))
.subscribe(active => this.snapshot.active = active);
// Update active snapshot when the active state changes.
this.focused$
.pipe(takeUntil(this._destroy$))
.subscribe(focused => this.snapshot.focused = focused);
// Detect navigation to a different view capability of the same app.
// Do NOT use `capability$` observable to detect capability change, as its lookup is asynchronous.
this.params$
.pipe(map(params => params.get(ɵMicrofrontendRouteParams.ɵVIEW_CAPABILITY_ID)), distinctUntilChanged(), skip(1), // skip the initial navigation
takeUntil(merge(this._beforeUnload$, this._destroy$)))
.subscribe(() => {
this._beforeInAppNavigation$.next();
this._canCloseFn = undefined;
this._canCloseSubscription?.unsubscribe();
this._canCloseSubscription = undefined;
});
// Signal view properties available.
this.whenProperties = firstValueFrom(combineLatest([this.partId$, this.params$])).then();
}
/** @inheritDoc */
signalReady() {
MicrofrontendPlatformClient.signalReady();
}
/** @inheritDoc */
setTitle(title) {
this._propertyChange$.next('title');
Observables.coerce(title)
.pipe(mergeMap(it => Beans.get(MessageClient).publish(ɵWorkbenchCommands.viewTitleTopic(this.id), it)), takeUntil(merge(this._propertyChange$.pipe(filter(prop => prop === 'title')), this._beforeInAppNavigation$, this._beforeUnload$, this._destroy$)))
.subscribe();
}
/** @inheritDoc */
setHeading(heading) {
this._propertyChange$.next('heading');
Observables.coerce(heading)
.pipe(mergeMap(it => Beans.get(MessageClient).publish(ɵWorkbenchCommands.viewHeadingTopic(this.id), it)), takeUntil(merge(this._propertyChange$.pipe(filter(prop => prop === 'heading')), this._beforeInAppNavigation$, this._beforeUnload$, this._destroy$)))
.subscribe();
}
/** @inheritDoc */
markDirty(dirty) {
this._propertyChange$.next('dirty');
Observables.coerce(dirty ?? true)
.pipe(mergeMap(it => Beans.get(MessageClient).publish(ɵWorkbenchCommands.viewDirtyTopic(this.id), it)), takeUntil(merge(this._propertyChange$.pipe(filter(prop => prop === 'dirty')), this._beforeInAppNavigation$, this._beforeUnload$, this._destroy$)))
.subscribe();
}
/** @inheritDoc */
setClosable(closable) {
this._propertyChange$.next('closable');
Observables.coerce(closable)
.pipe(mergeMap(it => Beans.get(MessageClient).publish(ɵWorkbenchCommands.viewClosableTopic(this.id), it)), takeUntil(merge(this._propertyChange$.pipe(filter(prop => prop === 'closable')), this._beforeInAppNavigation$, this._beforeUnload$, this._destroy$)))
.subscribe();
}
/** @inheritDoc */
close() {
void Beans.get(MessageClient).publish(ɵWorkbenchCommands.viewCloseTopic(this.id));
}
/** @inheritDoc */
canClose(canClose) {
this._canCloseFn = canClose;
this._canCloseSubscription ??= Beans.get(MessageClient).onMessage(ɵWorkbenchCommands.canCloseTopic(this.id), () => this._canCloseFn?.() ?? true);
return {
dispose: () => {
if (canClose === this._canCloseFn) {
this._canCloseSubscription.unsubscribe();
this._canCloseSubscription = undefined;
this._canCloseFn = undefined;
}
},
};
}
preDestroy() {
this._canCloseSubscription?.unsubscribe();
this._canCloseSubscription = undefined;
this._destroy$.next();
}
}
/**
* Context key to retrieve the view ID for microfrontends embedded in the context of a workbench view.
*
* @docs-private Not public API, intended for internal use only.
* @ignore
* @see {@link ContextService}
*/
const ɵVIEW_ID_CONTEXT_KEY = 'ɵworkbench.view.id';
/**
* Looks up the corresponding view capability for each capability id emitted by the source Observable.
*
* For new subscribers, the most recently looked up capability is replayed. It is guaranteed that no stale capability
* is replayed, that is, that the replayed capability always corresponds to the most recent emitted capability id of
* the source Observable.
*/
function lookupViewCapabilityAndShareReplay() {
let latestViewCapabilityId;
return pipe(distinctUntilChanged(), tap(viewCapabilityId => latestViewCapabilityId = viewCapabilityId), switchMap(viewCapabilityId => Beans.get(ManifestService).lookupCapabilities$({ id: viewCapabilityId })), // async call; long-living
map(viewCapabilities => viewCapabilities[0]),
// Replay the latest looked up capability for new subscribers.
shareReplay({ refCount: false, bufferSize: 1 }),
// Ensure not to replay a stale capability upon the subscription of new subscribers. For this reason, we install a filter to filter them out.
// The 'shareReplay' operator would replay a stale capability if the source has emitted a new capability id, but the lookup for it did not complete yet.
filter(viewCapability => latestViewCapabilityId === viewCapability.metadata.id));
}
/*
* Copyright (c) 2018-2022 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
*/
/**
* Registers {@link WorkbenchView} in the bean manager if in the context of a workbench view.
*
* @internal
*/
class WorkbenchViewInitializer {
async init() {
const viewId = await Beans.get(ContextService).lookup(ɵVIEW_ID_CONTEXT_KEY);
if (viewId !== null) {
const workbenchView = new ɵWorkbenchView(viewId);
Beans.register(WorkbenchView, { useValue: workbenchView });
// Wait until initialized the view, supporting synchronous access to view properties in microfrontend constructor.
await workbenchView.whenProperties;
}
}
}
/*
* Copyright (c) 2018-2025 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
*/
/**
* Computes a unique popup id.
*/
function computePopupId() {
return `popup.${UUID.randomUUID().substring(0, 8)}`;
}
/*
* Copyright (c) 2018-2022 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
*/
/**
* Displays a microfrontend in a popup.
*
* A popup is a visual workbench component for displaying content above other content. It is positioned relative to an anchor and
* moves when the anchor moves. Unlike a dialog, the popup closes on focus loss.
*
* A microfrontend provided as a `popup` capability can be opened in a popup. The qualifier differentiates between different
* popup capabilities. An application can open the public popup capabilities of other applications if it manifests a respective
* intention.
*
* @category Popup
* @see WorkbenchPopupCapability
*/
class WorkbenchPopupService {
/**
* Displays a microfrontend in a workbench popup based on the given qualifier.
*
* The qualifier identifies the microfrontend to display in the popup.
*
* The anchor is used to position the popup based on its preferred alignment:
* - Using an element: The popup opens and sticks to the element.
* - Using coordinates: The popup opens and sticks relative to the view or page bounds.
*
* If the popup is opened within a view, it only displays if the view is active and closes when the view is closed.
*
* By default, the popup closes on focus loss or when pressing the escape key.
*
* Pass data to the popup using parameters. Only declared parameters are allowed. Refer to the capability documentation for details.
*
* @param qualifier - Identifies the popup capability that provides the microfrontend for display as popup.
* @param config - Controls popup behavior.
* @returns Promise that resolves to the popup result, if any, or that rejects if the popup was closed with an error or couldn't be opened,
* e.g., because of missing the intention or because no `popup` capability matching the qualifier and visible to the application
* was found.
*/
async open(qualifier, config) {
const popupCommand = {
popupId: computePopupId(),
align: config.align,
closeStrategy: config.closeStrategy,
cssClass: config.cssClass,
context: {
viewId: Defined.orElse(config.context?.viewId, () => Beans.opt(WorkbenchView)?.id),
},
};
const popupOriginReporter = this.observePopupOrigin$(config)
.pipe(finalize(() => void Beans.get(MessageClient).publish(ɵWorkbenchCommands.popupOriginTopic(popupCommand.popupId), undefined, { retain: true })))
.subscribe(origin => void Beans.get(MessageClient).publish(ɵWorkbenchCommands.popupOriginTopic(popupCommand.popupId), origin, { retain: true }));
try {
const params = Maps.coerce(config.params);
const openPopup$ = Beans.get(IntentClient).request$({ type: WorkbenchCapabilities.Popup, qualifier, params }, popupCommand).pipe(mapToBody());
return await firstValueFrom(openPopup$, { defaultValue: undefined });
}
catch (error) {
throw (error instanceof RequestError ? error.message : error);
}
finally {
popupOriginReporter.unsubscribe();
}
}
/**
* Observes the position of the popup anchor.
*
* The Observable emits the anchor's initial position, and each time its position changes.
* The Observable never completes.
*/
observePopupOrigin$(config) {
if (config.anchor instanceof Element) {
return fromBoundingClientRect$(config.anchor);
}
else {
return concat(Observables.coerce(config.anchor), NEVER);
}
}
}
/*
* 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
*/
/**
* A popup is a visual workbench component for displaying content above other content.
*
* If a microfrontend lives in the context of a workbench popup, regardless of its embedding level, it can inject an instance
* of this class to interact with the workbench popup, such as reading passed parameters or closing the popup.
*
* #### Preferred Size
* You can report preferred popup size using {@link @scion/microfrontend-platform!PreferredSizeService}. Typically, you would
* subscribe to size changes of the microfrontend's primary content and report it. As a convenience, {@link @scion/microfrontend-platform!PreferredSizeService}
* provides API to pass an element for automatic dimension monitoring. If your content can grow and shrink, e.g., if using expandable
* panels, consider positioning primary content out of the document flow, that is, setting its position to `absolute`. This way,
* you give it infinite space so that it can always be rendered at its preferred size.
*
* ```typescript
* Beans.get(PreferredSizeService).fromDimension(<HTMLElement>);
* ```
*
* Note that the microfrontend may take some time to load, causing the popup to flicker when opened. Therefore, for fixed-sized
* popups, consider declaring the popup size in the popup capability.
*
* @category Popup
*/
class WorkbenchPopup {
}
/**
* @ignore
*/
class ɵWorkbenchPopup {
_context;
id;
params;
capability;
referrer;
focused$;
constructor(_context) {
this._context = _context;
this.id = this._context.popupId;
this.capability = this._context.capability;
this.params = this._context.params;
this.referrer = this._context.referrer;
void this.requestFocus();
this.focused$ = Beans.get(MessageClient).observe$(ɵWorkbenchCommands.popupFocusedTopic(this.id))
.pipe(mapToBody(), shareReplay({ refCount: false, bufferSize: 1 }), decorateObservable());
}
/**
* @inheritDoc
*/
signalReady() {
MicrofrontendPlatformClient.signalReady();
}
/**
* @inheritDoc
*/
setResult(result) {
void Beans.get(MessageClient).publish(ɵWorkbenchCommands.popupResultTopic(this._context.popupId), result);
}
/**
* @inheritDoc
*/
close(result) {
if (result instanceof Error) {
const headers = new Map().set(ɵWorkbenchPopupMessageHeaders.CLOSE_WITH_ERROR, true);
void Beans.get(MessageClient).publish(ɵWorkbenchCommands.popupCloseTopic(this._context.popupId), result.message, { headers });
}
else {
void Beans.get(MessageClient).publish(ɵWorkbenchCommands.popupCloseTopic(this._context.popupId), result);
}
}
/**
* If the document is not yet focused, make it focusable and request the focus.
*
* In order to close the popup on focus loss, microfrontend content must gain the focus first.
*/
async requestFocus() {
// Request focus only if this microfrontend is the actual popup microfrontend,
// i.e. not nested microfrontends in the popup.
const contexts = await Beans.get(ContextService).lookup(OUTLET_CONTEXT, { collect: true });
if (contexts.length > 1) {
return;
}
// Do nothing if an element of this microfrontend already has the focus.
if (document.activeElement !== document.body) {
return;
}
// Ensure the body element to be focusable.
if (document.body.getAttribute('tabindex') === null) {
document.body.style.outline = 'none';
document.body.setAttribute('tabindex', '-1');
}
// Request the focus.
document.body.focus();
}
}
/**
* Message headers to interact with the workbench popup.
*
* @docs-private Not public API, intended for internal use only.
* @ignore
*/
var ɵWorkbenchPopupMessageHeaders;
(function (ɵWorkbenchPopupMessageHeaders) {
ɵWorkbenchPopupMessageHeaders["CLOSE_WITH_ERROR"] = "\u0275WORKBENCH-POPUP:CLOSE_WITH_ERROR";
})(ɵWorkbenchPopupMessageHeaders || (ɵWorkbenchPopupMessageHeaders = {}));
/*
* Copyright (c) 2018-2022 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
*/
/**
* Key for obtaining the current popup context using {@link ContextService}.
*
* The popup context is only available to microfrontends loaded in a workbench popup.
*
* @docs-private Not public API, intended for internal use only.
* @ignore
* @see {@link ContextService}
* @see {@link ɵPopupContext}
*/
const ɵPOPUP_CONTEXT = 'ɵworkbench.popup';
/*
* Copyright (c) 2018-2022 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
*/
/**
* Registers {@link WorkbenchPopup} in the bean manager if in the context of a workbench popup.
*
* @internal
*/
class WorkbenchPopupInitializer {
async init() {
const popupContext = await Beans.get(ContextService).lookup(ɵPOPUP_CONTEXT);
if (popupContext !== null) {
Beans.register(WorkbenchPopup, { useValue: new ɵWorkbenchPopup(popupContext) });
}
}
}
/*
* 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
*/
/**
* Displays a microfrontend in a message box.
*
* A message box is a standardized dialog for presenting a message to the user, such as an info, warning or alert,
* or for prompting the user for confirmation.
*
* A microfrontend provided as a `messagebox` capability can be opened in a message box. The qualifier differentiates between different
* message box capabilities. An application can open the public message box capabilities of other applications if it manifests a respective
* intention.
*
* Displayed on top of other content, a message box blocks interaction with other parts of the application. Multiple message boxes are stacked,
* and only the topmost message box in each modality stack can be interacted with.
*
* A message box can be view-modal or application-modal. A view-modal message box blocks only a specific view, allowing the user to interact
* with other views. An application-modal message box blocks the workbench, or the browser's viewport if configured in the workbench
* host application.
*
* @category MessageBox
* @see WorkbenchMessageBoxCapability
*/
class WorkbenchMessageBoxService {
}
/*
* Copyright (c) 2018-2022 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
*/
/**
* Allows displaying a notification to the user.
*
* A notification is a closable message that appears in the upper-right corner and disappears automatically after a few seconds.
* It informs the user of a system event, e.g., that a task has been completed or an error has occurred.
*
* Multiple notifications are stacked vertically. Notifications can be grouped. For each group, only the last notification is
* displayed at any given time.
*
* The built-in notification supports the display of a plain text message and is available as 'notification' capability without a qualifier.
* Other notification capabilities can be contributed in the host app, e.g., to display structured content or to provide out-of-the-box
* notification templates. The use of a qualifier distinguishes different notification providers.
*
* Applications need to declare an intention in their application manifest for displaying a notification to the user, as illustrated below:
*
* ```json
* {
* "intentions": [
* { "type": "notification" }
* ]
* }
* ```
*
* @see WorkbenchNotificationCapability
* @category Notification
*/
class WorkbenchNotificationService {
/**
* Presents the user with a notification that is displayed in the upper-right corner based on the given qualifier.
*
* The qualifier identifies the provider to display the notification. The build-in notification to display a plain text message requires
* no qualifier.
*
* @param notification - Configures the content and appearance of the notification.
* @param qualifier - Identifies the notification provider.
*
* @return Promise that resolves when displaying the notification, or that rejects if displaying the notification failed, e.g., if missing
* the notification intention, or because no notification provider could be found that provides a notification under the specified
* qualifier.
*/
async show(notification, qualifier) {
const config = typeof notification === 'string' ? { content: notification } : notification;
const params = Maps.coerce(config.params);
const showNotification$ = Beans.get(IntentClient).request$({ type: WorkbenchCapabilities.Notification, qualifier, params }, config);
try {
await lastValueFrom(showNotification$, { defaultValue: undefined });
}
catch (error) {
throw (error instanceof RequestError ? error.message : error);
}
}
}
/*
* Copyright (c) 2018-2023 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
*/
/**
* Enables an application to monitor the workbench theme.
*/
class WorkbenchThemeMonitor {
}
/*
* Copyright (c) 2018-2023 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
*/
/**
* @inheritDoc
*/
class ɵWorkbenchThemeMonitor {
/**
* @inheritDoc
*/
theme$ = Beans.get(ContextService).observe$(ɵTHEME_CONTEXT_KEY);
}
/**
* Context key to retrieve information about the current workbench theme.
*
* @docs-private Not public API, intended for internal use only.
* @ignore
* @see {@link ContextService}
*/
const ɵTHEME_CONTEXT_KEY = 'ɵworkbench.theme';
/*
* 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
*/
/**
* Key for obtaining the current dialog context using {@link ContextService}.
*
* The dialog context is only available to microfrontends loaded in a workbench dialog.
*
* @docs-private Not public API, intended for internal use only.
* @ignore
* @see {@link ContextService}
* @see {@link ɵDialogContext}
*/
const ɵDIALOG_CONTEXT = 'ɵworkbench.dialog';
/*
* 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
*/
/**
* Handle to interact with a dialog opened via {@link WorkbenchDialogService}.
*
* The dialog microfrontend can inject this handle to interact with the dialog, such as setting the title,
* reading parameters, or closing it.
*
* @category Dialog
* @see WorkbenchDialogCapability
* @see WorkbenchDialogService
*/
class WorkbenchDialog {
}
/*
* 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
*/
/**
* @ignore
* @docs-private Not public API, intended for internal use only.
*/
class ɵWorkbenchDialog {
_context;
_destroy$ = new Subject();
_titleChange$ = new Subject();
id;
capability;
params;
focused$;
constructor(_context) {
this._context = _context;
this.id = this._context.dialogId;
this.capability = this._context.capability;
this.params = this._context.params;
this.focused$ = Beans.get(MessageClient).observe$(ɵWorkbenchCommands.dialogFocusedTopic(this.id))
.pipe(mapToBody(), shareReplay({ refCount: false, bufferSize: 1 }), decorateObservable());
}
/**
* @inheritDoc
*/
setTitle(title) {
this._titleChange$.next();
Observables.coerce(title)
.pipe(takeUntil(merge(this._destroy$, this._titleChange$)))
.subscribe(value => void Beans.get(MessageClient).publish(ɵWorkbenchCommands.dialogTitleTopic(this._context.dialogId), value));
}
/**
* @inheritDoc
*/
close(result) {
this._destroy$.next();
if (result instanceof Error) {
const headers = new Map().set(ɵWorkbenchDialogMessageHeaders.CLOSE_WITH_ERROR, true);
void Beans.get(MessageClient).publish(ɵWorkbenchCommands.dialogCloseTopic(this._context.dialogId), result.message, { headers });
}
else {
void Beans.get(MessageClient).publish(ɵWorkbenchCommands.dialogCloseTopic(this._context.dialogId), result);
}
}
/**
* @inheritDoc
*/
signalReady() {
MicrofrontendPlatformClient.signalReady();
}
}
/**
* Message headers to interact with the workbench dialog.
*
* @docs-private Not public API, intended for internal use only.
* @ignore
*/
var ɵWorkbenchDialogMessageHeaders;
(function (ɵWorkbenchDialogMessageHeaders) {
ɵWorkbenchDialogMessageHeaders["CLOSE_WITH_ERROR"] = "\u0275WORKBENCH-DIALOG:CLOSE_WITH_ERROR";
})(ɵWorkbenchDialogMessageHeaders || (ɵWorkbenchDialogMessageHeaders = {}));
/*
* 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
*/
/**
* Registers {@link WorkbenchDialog} in the bean manager if in the context of a workbench dialog.
*
* @internal
*/
class WorkbenchDialogInitializer {
async init() {
const dialogContext = await Beans.get(ContextService).lookup(ɵDIALOG_CONTEXT);
if (dialogContext !== null) {
Beans.register(WorkbenchDialog, { useValue: new ɵWorkbenchDialog(dialogContext) });
}
}
}
/*
* 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
*/
/**
* Displays a microfrontend in a modal dialog.
*
* A dialog is a visual element for focused interaction with the user, such as prompting the user for input or confirming actions.
* The user can move or resize a dialog.
*
* A microfrontend provided as a `dialog` capability can be opened in a dialog. The qualifier differentiates between different
* dialog capabilities. An application can open the public dialog capabilities of other applications if it manifests a respective
* intention.
*
* Displayed on top of other content, a dialog blocks interaction with other parts of the application. Multiple dialogs are stacked,
* and only the topmost dialog in each modality stack can be interacted with.
*
* A dialog can be view-modal or application-modal. A view-modal dialog blocks only a specific view, allowing the user to interact
* with other views. An application-modal dialog blocks the workbench, or the browser's viewport if configured in the workbench
* host application.
*
* @category Dialog
* @see WorkbenchDialogCapability
*/
class WorkbenchDialogService {
}
/*
* 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
*/
/**
* @ignore
* @docs-private Not public API, intended for internal use only.
*/
class ɵWorkbenchDialogService {
/** @inheritDoc */
open(qualifier, options) {
const intent = { type: WorkbenchCapabilities.Dialog, qualifier, params: Maps.coerce(options?.params) };
const body = {
...options,
context: { viewId: options?.context?.viewId ?? Beans.opt(WorkbenchView)?.id },
params: undefined, // passed via intent
};
const closeResult$ = Beans.get(IntentClient).request$(intent, body)
.pipe(mapToBody(), catchError((error) => throwError(() => error instanceof RequestError ? error.message : error)));
return firstValueFrom(closeResult$, { defaultValue: undefined });
}
}
/*
* 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
*/
/**
* @ignore
* @docs-private Not public API, intended for internal use only.
*/
class ɵWorkbenchMessageBox {
_context;
id;
capability;
params;
referrer;
focused$;
constructor(_context) {
this._context = _context;
this.id = this._context.dialogId;
this.capability = this._context.capability;
this.params = this._context.params;
this.referrer = this._context.referrer;
this.focused$ = Beans.get(MessageClient).observe$(ɵWorkbenchCommands.dialogFocusedTopic(this.id))
.pipe(mapToBody(), shareReplay({ refCount: false, bufferSize: 1 }), decorateObservable());
}
/** @inheritDoc */
signalReady() {
MicrofrontendPlatformClient.signalReady();
}
}
/*
* 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
*/
/**
* Handle to interact with a message box opened via {@link WorkbenchMessageBoxService}.
*
* The message box microfrontend can inject this handle to interact with the message box,
* such as reading parameters or signaling readiness.
*
* @category MessageBox
* @see WorkbenchMessageBoxCapability
* @see WorkbenchMessageBoxService
*/
class WorkbenchMessageBox {
}
/*
* 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
*/
/**
* Key for obtaining the current message box context using {@link ContextService}.
*
* The message box context is only available to microfrontends loaded in a workbench message box.
*
* @docs-private Not public API, intended for internal use only.
* @ignore
* @see {@link ContextService}
* @see {@link ɵMessageBoxContext}
*/
const ɵMESSAGE_BOX_CONTEXT = 'ɵworkbench.message-box';
/*
* 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
*/
/**
* Registers {@link WorkbenchMessageBox} in the bean manager if in the context of a workbench message box.
*
* @internal
*/
class WorkbenchMessageBoxInitializer {
async init() {
const messageBoxContext = await Beans.get(ContextService).lookup(ɵMESSAGE_BOX_CONTEXT);
if (messageBoxContext !== null) {
Beans.register(WorkbenchMessageBox, { useValue: new ɵWorkbenchMessageBox(messageBoxContext) });
}
}
}
/*
* 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
*/
/**
* Parameter name for the message displayed in the built-in text {@link WorkbenchMessageBoxCapability}.
*/
const eMESSAGE_BOX_MESSAGE_PARAM = 'message';
/*
* 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
*/
/**
* @ignore
* @docs-private Not public API, intended for internal use only.
*/
class ɵWorkbenchMessageBoxService {
/** @inheritDoc */
open(message, options) {
const intent = (() => {
if (typeof message === 'string' || message === null) {
return { type: WorkbenchCapabilities.MessageBox, qualifier: {}, params: new Map().set(eMESSAGE_BOX_MESSAGE_PARAM, message ?? undefined) };
}
else {
return { type: WorkbenchCapabilities.MessageBox, qualifier: message, params: Maps.coerce(options?.params) };
}
})();
const body = {
...options,
context: { viewId: options?.context?.viewId ?? Beans.opt(WorkbenchView)?.id },
params: undefined, // passed via intent
};
const action$ = Beans.get(IntentClient).request$(intent, body)
.pipe(mapToBody(), catchError((error) => throwError(() => error instanceof RequestError ? error.message : error)));
return firstValueFrom(action$);
}
}
/**
* Installs a CSS stylesheet with styles required by the workbench.
*/
class StyleSheetInstaller {
_styleSheet = new CSSStyleSheet({});
constructor() {
// Declare styles for the document root element (`<html>`) in a CSS layer.
// CSS layers have lower priority than "regular" CSS declarations, and the layer name indicates the styles are from @scion/workbench.
// Applies the following styles:
// - Ensures the document root element is positioned to support `@scion/toolkit/observable/fromBoundingClientRect$` for observing element bounding boxes.
// - Aligns the document root with the page viewport so the top-level positioning context fills the page viewport (as expected b