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,255 lines (1,226 loc) 71.6 kB
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