@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,327 lines (1,294 loc) • 85 kB
JavaScript
import { Beans } from '@scion/toolkit/bean-manager';
import { ObservableDecorator, MessageClient, mapToBody, MicrofrontendPlatformClient, ManifestService, ContextService, OUTLET_CONTEXT, IntentClient, RequestError, MicrofrontendPlatform, PlatformState, APP_IDENTITY, IS_PLATFORM_HOST, ACTIVATION_CONTEXT } from '@scion/microfrontend-platform';
import { Subject, merge, firstValueFrom, combineLatest, pipe, catchError, throwError, Subscription, of, concatWith, NEVER, share, timer, animationFrameScheduler, ReplaySubject, first, timeout, switchMap as switchMap$1, concat, lastValueFrom, fromEvent } from 'rxjs';
import { map, shareReplay, takeUntil, distinctUntilChanged, skip, mergeMap, filter, tap, switchMap, finalize } from 'rxjs/operators';
import { Observables, Maps, Defined, Arrays, Dictionaries } from '@scion/toolkit/util';
import { fromBoundingClientRect$ } from '@scion/toolkit/observable';
import { UUID } from '@scion/toolkit/uuid';
/*
* 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
*/
/**
* 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
*/
class WorkbenchView {
}
/*
* Copyright (c) 2018-2026 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. 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 active state of a part.
*
* The active state is published as a retained message.
*/
partActiveTopic: (partId) => `ɵworkbench/parts/${partId}/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 focused state of a part.
*
* The focused state is published as a retained message.
*/
partFocusedTopic: (partId) => `ɵworkbench/parts/${partId}/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} and params as passed in {@link WorkbenchNavigationExtras.params}.
*
* 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`,
/**
* Computes the topic to notify the focused state of a notification.
*
* The focused state is published as a retained message.
*/
notificationFocusedTopic: (notificationId) => `ɵworkbench/notifications/${notificationId}/focused`,
/**
* Computes the topic via which a notification can be closed.
*/
notificationCloseTopic: (notificationId) => `ɵworkbench/notifications/${notificationId}/close`,
};
/*
* 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-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
*/
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,
capability: undefined,
};
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(ɵVIEW_CAPABILITY_ID_PARAM_NAME)), 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 focus state changes.
this.focused$
.pipe(takeUntil(this._destroy$))
.subscribe(focused => this.snapshot.focused = focused);
// Update capability snapshot when the capability changes.
this.capability$
.pipe(takeUntil(this._destroy$))
.subscribe(capability => this.snapshot.capability = capability);
// 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(ɵVIEW_CAPABILITY_ID_PARAM_NAME)), 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$, this.capability$])).then();
}
/** @inheritDoc */
signalReady() {
MicrofrontendPlatformClient.signalReady();
}
/** @inheritDoc */
setTitle(title) {
void Beans.get(MessageClient).publish(ɵWorkbenchCommands.viewTitleTopic(this.id), title);
}
/** @inheritDoc */
setHeading(heading) {
void Beans.get(MessageClient).publish(ɵWorkbenchCommands.viewHeadingTopic(this.id), heading);
}
/** @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 for obtaining the view ID using {@link ContextService}.
*
* @docs-private Not public API. For internal use only.
* @ignore
* @see {@link ContextService}
*/
const ɵVIEW_ID_CONTEXT_KEY = 'ɵworkbench.view.id';
/**
* Parameter name for obtaining the capability id in a microfrontend view.
*
* @docs-private Not public API. For internal use only.
* @ignore
*/
const ɵVIEW_CAPABILITY_ID_PARAM_NAME = 'ɵViewCapabilityId';
/**
* 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-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
*/
/**
* Symbol to inject the workbench element available in the current context.
*
* @see WorkbenchElement
*/
const WORKBENCH_ELEMENT = Symbol('WORKBENCH_ELEMENT');
/*
* 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 });
Beans.register(WORKBENCH_ELEMENT, { useExisting: WorkbenchView });
// Wait until initialized the view, supporting synchronous access to view properties in microfrontend constructor.
await workbenchView.whenProperties;
}
}
}
/*
* 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. Declaring an intention allows for opening public view capabilities of other applications.
*
* @category Router
* @category View
*/
class WorkbenchRouter {
}
/*
* 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 element for displaying content above other content. It is positioned relative to an anchor,
* which can be an element or a coordinate. The popup moves with the anchor. By default, the popup closes on focus loss or
* when pressing the escape key.
*
* A microfrontend provided as a `popup` capability can be opened in a popup. The qualifier differentiates between different
* popup capabilities. Declaring an intention allows for opening public popup capabilities of other applications.
*
* A popup can be bound to a context (e.g., part or view), displaying the popup only if the context is visible and closing
* it when the context is disposed. Defaults to the calling context.
*
* @category Popup
* @see WorkbenchPopupCapability
*/
class WorkbenchPopupService {
}
/*
* 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 element for displaying content above other content. The popup is positioned relative
* to an anchor based on its preferred alignment. The anchor can be an element or a coordinate.
*
* The microfrontend can inject this handle to interact with the popup.
*
* #### Popup Size
* Configure the popup with a fixed size in {@link WorkbenchPopupCapability.properties.size}, or report its intrinsic content
* size using {@link @scion/microfrontend-platform!PreferredSizeService}.
*
* @example - Reporting the size of a microfrontend
* ```ts
* Beans.get(PreferredSizeService).fromDimension(<Microfrontend HTMLElement>);
* ```
*
* If the content can grow and shrink, e.g., if using expandable panels, position the microfrontend `absolute` to allow infinite
* space for rendering at its preferred size.
*
* Since loading a microfrontend may take time, prefer setting the popup size in the popup capability to avoid flickering when
* opening the popup.
*
* @category Popup
*/
class WorkbenchPopup {
}
/*
* 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. For internal use only.
* @ignore
* @see {@link ContextService}
* @see {@link ɵPopupContext}
*/
const ɵPOPUP_CONTEXT = 'ɵworkbench.popup';
/*
* 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. For internal use only.
*/
class ɵWorkbenchPopup {
id;
params;
capability;
referrer;
focused$;
constructor(context) {
this.id = context.popupId;
this.capability = context.capability;
this.params = context.params;
this.referrer = 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.id), 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.id), result.message, { headers });
}
else {
void Beans.get(MessageClient).publish(ɵWorkbenchCommands.popupCloseTopic(this.id), 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. 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
*/
/**
* 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) });
Beans.register(WORKBENCH_ELEMENT, { useExisting: WorkbenchPopup });
}
}
}
/*
* 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. Declaring an intention allows for opening public message box capabilities of other applications.
*
* Displayed on top of other content, a modal message box blocks interaction with other parts of the application.
*
* ## Modality
* A message box can be context-modal or application-modal. Context-modal blocks a specific part of the application, as specified by the context;
* application-modal blocks the workbench or browser viewport, based on global workbench settings.
*
* ## Context
* A message box can be bound to a context (e.g., part or view), defaulting to the calling context.
* The message box is displayed only if the context is visible and closes when the context is disposed.
*
* ## Positioning
* A message box is opened in the center of its context, if any, unless opened from the peripheral area.
*
* ## Stacking
* Message boxes are stacked per modality, with only the topmost message box in each stack being interactive.
*
* @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
*/
/**
* Shows a notification.
*
* A notification is a closable message displayed in the upper-right corner that disappears after a few seconds unless hovered or focused.
* It informs about system events, task completion, or errors. Severity indicates importance or urgency.
*
* Notifications can be grouped. Only the most recent notification within a group is displayed.
*
* A microfrontend provided as a `notification` capability can be opened in a notification. The qualifier differentiates between different
* notification capabilities. Declaring an intention allows for opening public notification capabilities of other applications.
*
* @see WorkbenchNotificationCapability
* @category Notification
*/
class WorkbenchNotificationService {
}
/*
* 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. 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. For internal use only.
* @ignore
* @see {@link ContextService}
* @see {@link ɵDialogContext}
*/
const ɵDIALOG_CONTEXT = 'ɵworkbench.dialog';
/*
* 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
*/
/**
* 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-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
*/
/**
* @ignore
* @docs-private Not public API. For internal use only.
*/
class ɵWorkbenchDialog {
_context;
_destroy$ = 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) {
void Beans.get(MessageClient).publish(ɵWorkbenchCommands.dialogTitleTopic(this._context.dialogId), title);
}
/**
* @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. 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) });
Beans.register(WORKBENCH_ELEMENT, { useExisting: 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
*/
/**
* Displays a microfrontend in a 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 and resize a dialog.
*
* A microfrontend provided as a `dialog` capability can be opened in a dialog. The qualifier differentiates between different
* dialog capabilities. Declaring an intention allows for opening public dialog capabilities of other applications.
*
* Displayed on top of other content, a modal dialog blocks interaction with other parts of the application.
*
* ## Modality
* A dialog can be context-modal or application-modal. Context-modal blocks a specific part of the application, as specified by the context;
* application-modal blocks the workbench or browser viewport, based on global workbench settings.
*
* ## Context
* A dialog can be bound to a context (e.g., part or view), defaulting to the calling context.
* The dialog is displayed only if the context is visible and closes when the context is disposed.
*
* ## Positioning
* A dialog is opened in the center of its context, if any, unless opened from the peripheral area.
*
* ## Stacking
* Dialogs are stacked per modality, with only the topmost dialog in each stack being interactive.
*
* @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
*/
/**
* Built-in workbench capabilities.
*/
var WorkbenchCapabilities;
(function (WorkbenchCapabilities) {
/**
* Contributes a workbench part.
*
* A part is a visual element of the workbench layout. Parts can be docked to the side or
* positioned relative to each other. A part can display content or stack views.
*/
WorkbenchCapabilities["Part"] = "part";
/**
* Contributes a workbench view.
*
* A view is a visual element of the workbench layout for displaying content stacked or side-by-side.
*/
WorkbenchCapabilities["View"] = "view";
/**
* Contributes a workbench perspective.
*
* A perspective defines an arrangement of parts and views. Users can switch between perspectives. Perspectives share the same main area, if any.
*/
WorkbenchCapabilities["Perspective"] = "perspective";
/**
* Contributes a workbench popup.
*
* A popup is a visual workbench element for displaying content above other content.
*/
WorkbenchCapabilities["Popup"] = "popup";
/**
* Contributes a 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 workbench 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.
*/
WorkbenchCapabilities["MessageBox"] = "messagebox";
/**
* Contributes a workbench notification.
*
* A notification is a closable message displayed in the upper-right corner that disappears after a few seconds unless hovered or focused.
* It informs about system events, task completion, or errors. Severity indicates importance or urgency.
*/
WorkbenchCapabilities["Notification"] = "notification";
/**
* Provides texts to the SCION Workbench and micro apps.
*/
WorkbenchCapabilities["TextProvider"] = "text-provider";
})(WorkbenchCapabilities || (WorkbenchCapabilities = {}));
/*
* Copyright (c) 2018-2026 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. For internal use only.
*/
class ɵWorkbenchDialogService {
_context;
constructor(_context) {
this._context = _context;
}
/** @inheritDoc */
open(qualifier, options) {
const intent = { type: WorkbenchCapabilities.Dialog, qualifier, params: Maps.coerce(options?.params) };
const command = {
modality: options?.modality,
animate: options?.animate,
cssClass: options?.cssClass,
context: (() => {
// TODO [Angular 22] Remove backward compatiblity.
const context = options?.context && (typeof options.context === 'object' ? options.context.viewId : options.context);
return Defined.orElse(context, this._context);
})(),
};
const closeResult$ = Beans.get(IntentClient).request$(intent, command)
.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. For internal use only.
*/
class ɵWorkbenchMessageBox {
id;
capability;
params;
referrer;
focused$;
constructor(context) {
this.id = context.dialogId;
this.capability = context.capability;
this.params = context.params;
this.referrer = 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. 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) });
Beans.register(WORKBENCH_ELEMENT, { useExisting: WorkbenchMessageBox });
}
}
}
/*
* Copyright (c) 2018-2026 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}.
*
* @docs-private Not public API. For internal use only.
* @ignore
*/
const eMESSAGE_BOX_MESSAGE_PARAM = 'message';
/*
* Copyright (c) 2018-2026 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. For internal use only.
*/
class ɵWorkbenchMessageBoxService {
_context;
constructor(_context) {
this._context = _context;
}
/** @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 command = {
title: options?.title,
actions: options?.actions,
severity: options?.severity,
modality: options?.modality,
contentSelectable: options?.contentSelectable,
cssClass: options?.cssClass,
context: (() => {
// TODO [Angular 22] Remove backward compatiblity.
const context = options?.context && (typeof options.context === 'object' ? options.context.viewId : options.context);
return Defined.orElse(context, this._context);
})(),
};
const action$ = Beans.get(IntentClient).request$(intent, command)
.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 by applications).
this._styleSheet.insertRule(`
@layer sci-workbench {
:root {
position: absolute;
inset: 0;
}
}`);
document.adoptedStyleSheets.push(this._styleSheet);
}
preDestroy() {
Arrays.remove(document.adoptedStyleSheets, this._styleSheet);
}
}
/*
* 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
*/
/**
* Provides texts to the SCION Workbench and micro apps.
*
* A text provider is a function that returns the text for a translation key.
*
* Texts starting with the percent symbol (`%`) are passed to the text provider for translation, with the percent symbol omitted.
*
* This function must be called in an Activator.
*
* @param textProvider - Function to provide the text for a translation key.
* @return Object to unregister the text provider.
*/
function registerTextProvider(textProvider) {
const resources = new Subscription();
// Wait until starting or started the platform.
Promise.race([MicrofrontendPlatform.whenState(PlatformState.Starting), MicrofrontendPlatform.whenState(PlatformState.Started)])
.then(async () => {
await assertInHostOrActivator();
await throwIfAlreadyRegistered();
// Register 'text-provider' capability.
const appSymbolicName = Beans.get(APP_IDENTITY);
const capability = {
type: WorkbenchCapabilities.TextProvider,
qualifier: { provider: appSymbolicName },
private: false,
description: `Provides texts of '${appSymbolicName}' application.`,
params: [
{
name: 'key',
required: true,
description: '{string} - Translation key of the text.',
},
{
name: 'params',
required: false,
description: '{dictionary} - Parameters used for text interpolation.',
},
],
};
const manifestService = Beans.get(ManifestService);
const capabilityId = await manifestService.registerCapability(capability);
if (capabilityId === null) {
return;
}
resources.add(() => void manifestService.unregisterCapabilities({ id: capabilityId }));
// Install intent handler.
const intentSubscription = Beans.get(IntentClient).onIntent({ type: capability.type, qualifier: capability.qualifier }, ({ intent }) => {
const key = intent.params.get('key');
const params = intent.params.get('params') ?? {};
return textProvider(key, params);
});
resources.add(intentSubscription);
})
.catch((error) => {
console.error(`[WorkbenchClientError] Failed to register text provider for application '${Beans.opt(APP_IDENTITY)}'. Caused by: `, error);
resources.unsubscribe();
});
// Unregister text provider when stopping the platform, e.g., during hot code replacement.
void MicrofrontendPlatform.whenState(PlatformState.Stopping).then(() => resources.unsubscribe());
return {
dispose: () => resources.unsubscribe(),
};
}
/**
* Throws if the application has already registered a text provider capability.
*/
async function throwIfAlreadyRegistered() {
const capabilities = await firstValueFrom(Beans.get(ManifestService).lookupCapabilities$({ type: WorkbenchCapabilities.TextProvider, qualifier: { provider: Beans.get(APP_IDENTITY) } }));
if (capabilities.length > 0) {
throw Error('[TextProviderError] Text Provider already registered.');
}
}
/**
* Throws if not in the context of the host app or an activator.
*/
async function assertInHostOrActivator() {
if (Beans.get(IS_PLATFORM_HOST)) {
return;
}
if (!await Beans.get(ContextService).isPresent(ACTIVATION_CONTEXT)) {
throw Error('[TextProviderError] Text Provider must be registered in an Activator.');
}
}
/*
* 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
*/
/**
* Provides texts from micro applications.
*
* Applications can register a text provider using {@link WorkbenchClient.registerTextProvider} to provide texts to other applications.
*
* To get texts from another application, the application must declare an intention:
*
* ```json
* {
* "type": "text-provider",
* "qualifier": {
* "provider": "<APP_SYMBOLIC_NAME>" // Replace with the symbolic name of the providing application
* }
* },
* ```
*
* @category Localization
*/
class WorkbenchTextService {
}
/*
* 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
*/
/** @inheritDoc */
class ɵWorkbenchTextService {
_cache = new Map();
/** @inheritDoc */
text$(translatable, options) {
if (!translatable) {
return of(undefined);
}
if (!translatable.startsWith('%') || translatable === '%') {
return of(translatable);
}
// Parse translatable into key and params.
const { key, params } = parseTranslatable(translatable