UNPKG

@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
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