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 lines 120 kB
{"version":3,"file":"scion-workbench-client.mjs","sources":["../../../../projects/scion/workbench-client/src/lib/view/workbench-view.ts","../../../../projects/scion/workbench-client/src/lib/ɵworkbench-commands.ts","../../../../projects/scion/workbench-client/src/lib/workbench-capabilities.enum.ts","../../../../projects/scion/workbench-client/src/lib/routing/workbench-router.ts","../../../../projects/scion/workbench-client/src/lib/observable-decorator.ts","../../../../projects/scion/workbench-client/src/lib/view/ɵworkbench-view.ts","../../../../projects/scion/workbench-client/src/lib/view/workbench-view-initializer.ts","../../../../projects/scion/workbench-client/src/lib/popup/workbench-popup-service.ts","../../../../projects/scion/workbench-client/src/lib/popup/workbench-popup.ts","../../../../projects/scion/workbench-client/src/lib/popup/workbench-popup-context.ts","../../../../projects/scion/workbench-client/src/lib/popup/workbench-popup-initializer.ts","../../../../projects/scion/workbench-client/src/lib/message-box/workbench-message-box-service.ts","../../../../projects/scion/workbench-client/src/lib/notification/workbench-notification-service.ts","../../../../projects/scion/workbench-client/src/lib/theme/workbench-theme-monitor.ts","../../../../projects/scion/workbench-client/src/lib/theme/ɵworkbench-theme-monitor.ts","../../../../projects/scion/workbench-client/src/lib/dialog/ɵworkbench-dialog-context.ts","../../../../projects/scion/workbench-client/src/lib/dialog/workbench-dialog.ts","../../../../projects/scion/workbench-client/src/lib/dialog/ɵworkbench-dialog.ts","../../../../projects/scion/workbench-client/src/lib/dialog/workbench-dialog-initializer.ts","../../../../projects/scion/workbench-client/src/lib/dialog/workbench-dialog-service.ts","../../../../projects/scion/workbench-client/src/lib/dialog/ɵworkbench-dialog-service.ts","../../../../projects/scion/workbench-client/src/lib/message-box/ɵworkbench-message-box.ts","../../../../projects/scion/workbench-client/src/lib/message-box/workbench-message-box.ts","../../../../projects/scion/workbench-client/src/lib/message-box/ɵworkbench-message-box-context.ts","../../../../projects/scion/workbench-client/src/lib/message-box/workbench-message-box-initializer.ts","../../../../projects/scion/workbench-client/src/lib/message-box/workbench-message-box-capability.ts","../../../../projects/scion/workbench-client/src/lib/message-box/ɵworkbench-message-box-service.ts","../../../../projects/scion/workbench-client/src/lib/style-sheet-installer.ts","../../../../projects/scion/workbench-client/src/lib/workbench-client.ts","../../../../projects/scion/workbench-client/src/lib/perspective/workbench-perspective-capability.ts","../../../../projects/scion/workbench-client/src/public-api.ts","../../../../projects/scion/workbench-client/src/scion-workbench-client.ts"],"sourcesContent":["/*\n * Copyright (c) 2018-2022 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\nimport {Observable} from 'rxjs';\nimport {WorkbenchViewCapability} from './workbench-view-capability';\n\n/**\n * A view is a visual workbench element for displaying content stacked or side-by-side in the workbench layout.\n *\n * Users can drag views from one part to another, even across windows, or place them side-by-side, horizontally and vertically.\n *\n * The view microfrontend can inject this handle to interact with the view.\n *\n * @category View\n * @see WorkbenchViewCapability\n * @see WorkbenchRouter\n */\nexport abstract class WorkbenchView {\n\n /**\n * Represents the identity of this view.\n */\n public abstract readonly id: ViewId;\n\n /**\n * Signals readiness, notifying the workbench that this view has completed initialization.\n *\n * If `showSplash` is set to `true` on the view capability, the workbench displays a splash until the view microfrontend signals readiness.\n *\n * @see WorkbenchViewCapability.properties.showSplash\n */\n public abstract signalReady(): void;\n\n /**\n * Provides the capability of the microfrontend loaded into the view.\n *\n * Upon subscription, emits the microfrontend's capability, and then emits continuously when navigating to a different microfrontend\n * of the same application. It completes when navigating to a microfrontend of another application.\n */\n public abstract readonly capability$: Observable<WorkbenchViewCapability>;\n\n /**\n * Provides the parameters of the microfrontend loaded into the view.\n *\n * Upon subscription, emits the microfrontend's parameters, and then emits continuously when the parameters change.\n * The Observable completes when navigating to a microfrontend of another application, but not when navigating to a different microfrontend\n * of the same application.\n */\n public abstract readonly params$: Observable<ReadonlyMap<string, any>>;\n\n /**\n * The current snapshot of this view.\n */\n public abstract readonly snapshot: ViewSnapshot;\n\n /**\n * Indicates whether this view is active.\n *\n * Upon subscription, emits the active state of this view, and then emits continuously when it changes.\n * The Observable completes when navigating to a microfrontend of another application, but not when navigating to a different microfrontend\n * of the same application.\n */\n public abstract readonly active$: Observable<boolean>;\n\n /**\n * Provides the identity of the part that contains this view.\n *\n * Upon subscription, emits the identity of this view's part, and then emits continuously when it changes.\n * The Observable completes when navigating to a microfrontend of another application, but not when navigating to a different microfrontend\n * of the same application.\n */\n public abstract readonly partId$: Observable<string>;\n\n /**\n * Sets the title to be displayed in the view tab.\n */\n public abstract setTitle(title: string | Observable<string>): void;\n\n /**\n * Sets the subtitle to be displayed in the view tab.\n */\n public abstract setHeading(heading: string | Observable<string>): void;\n\n /**\n * Sets whether this view is dirty or pristine. When navigating to another microfrontend, the view's dirty state is set to pristine.\n *\n * You can provide the dirty/pristine state either as a boolean or as Observable. If you pass an Observable, it will be unsubscribed when\n * navigating to another microfrontend, whether from the same app or a different one.\n *\n * If not passing an argument, the view is marked as dirty. To mark it as pristine, you need to pass `false`.\n */\n public abstract markDirty(dirty?: boolean | Observable<boolean>): void;\n\n /**\n * Controls whether the user should be allowed to close this workbench view.\n *\n * You can provide either a boolean or Observable. If you pass an Observable, it will be unsubscribed when navigating to another microfrontend,\n * whether from the same app or a different one.\n */\n public abstract setClosable(closable: boolean | Observable<boolean>): void;\n\n /**\n * Initiates the closing of this workbench view.\n */\n public abstract close(): void;\n\n /**\n * Registers a guard to confirm closing the view, replacing any previous guard.\n *\n * Example:\n * ```ts\n * Beans.get(WorkbenchView).canClose(async () => {\n * const action = await Beans.get(WorkbenchMessageBoxService).open('Do you want to save changes?', {\n * actions: {\n * yes: 'Yes',\n * no: 'No',\n * cancel: 'Cancel'\n * }\n * });\n *\n * switch (action) {\n * case 'yes':\n * // Store changes ...\n * return true;\n * case 'no':\n * return true;\n * default:\n * return false;\n * }\n * });\n * ```\n *\n * @param canClose - Callback to confirm closing the view.\n * @returns Reference to the `CanClose` guard, which can be used to unregister the guard.\n */\n public abstract canClose(canClose: CanCloseFn): CanCloseRef;\n}\n\n/**\n * The signature of a function to confirm closing a view., e.g., if dirty.\n */\nexport type CanCloseFn = () => Observable<boolean> | Promise<boolean> | boolean;\n\n/**\n * Reference to the `CanClose` guard registered on a view.\n */\nexport interface CanCloseRef {\n\n /**\n * Removes the `CanClose` guard from the view.\n *\n * Has no effect if another guard was registered in the meantime.\n */\n dispose(): void;\n}\n\n/**\n * Provides information about a view displayed at a particular moment in time.\n *\n * @category View\n */\nexport interface ViewSnapshot {\n /**\n * Parameters of the microfrontend loaded into the view.\n */\n params: ReadonlyMap<string, any>;\n /**\n * The identity of the part that contains the view.\n */\n partId: string;\n}\n\n/**\n * Format of a view identifier.\n *\n * Each view is assigned a unique identifier (e.g., `view.1`, `view.2`, etc.).\n *\n * @category View\n */\nexport type ViewId = `view.${number}`;\n","/*\n * Copyright (c) 2018-2024 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\nimport {ViewId} from './view/workbench-view';\n\n/**\n * Defines command endpoints for the communication between SCION Workbench and SCION Workbench Client.\n *\n * @docs-private Not public API, intended for internal use only.\n */\nexport const ɵWorkbenchCommands = {\n\n /**\n * Computes the topic via which the title of a workbench view tab can be set.\n */\n viewTitleTopic: (viewId: ViewId | ':viewId') => `ɵworkbench/views/${viewId}/title`,\n\n /**\n * Computes the topic via which the heading of a workbench view tab can be set.\n */\n viewHeadingTopic: (viewId: ViewId | ':viewId') => `ɵworkbench/views/${viewId}/heading`,\n\n /**\n * Computes the topic via which a view tab can be marked dirty or pristine.\n */\n viewDirtyTopic: (viewId: ViewId | ':viewId') => `ɵworkbench/views/${viewId}/dirty`,\n\n /**\n * Computes the topic via which a view tab can be made closable.\n */\n viewClosableTopic: (viewId: ViewId | ':viewId') => `ɵworkbench/views/${viewId}/closable`,\n\n /**\n * Computes the topic via which a view can be closed.\n */\n viewCloseTopic: (viewId: ViewId | ':viewId') => `ɵworkbench/views/${viewId}/close`,\n\n /**\n * Computes the topic to notify the active state of a view.\n *\n * The active state is published as a retained message.\n */\n viewActiveTopic: (viewId: ViewId) => `ɵworkbench/views/${viewId}/active`,\n\n /**\n * Computes the topic to notify the part of a view.\n *\n * The part identity is published as a retained message.\n */\n viewPartIdTopic: (viewId: ViewId) => `ɵworkbench/views/${viewId}/part/id`,\n\n /**\n * Computes the topic to request closing confirmation of a view.\n *\n * When closing a view and if the microfrontend has subscribed to this topic, the workbench requests closing confirmation\n * via this topic. By sending a `true` reply, the workbench continues with closing the view, by sending a `false` reply,\n * closing is prevented.\n */\n canCloseTopic: (viewId: ViewId) => `ɵworkbench/views/${viewId}/canClose`,\n\n /**\n * Computes the topic for signaling that a microfrontend is about to be replaced by a microfrontend of another app.\n */\n viewUnloadingTopic: (viewId: ViewId) => `ɵworkbench/views/${viewId}/unloading`,\n\n /**\n * Computes the topic for updating params of a microfrontend view.\n */\n viewParamsUpdateTopic: (viewId: ViewId, viewCapabilityId: string) => `ɵworkbench/views/${viewId}/capabilities/${viewCapabilityId}/params/update`,\n\n /**\n * Computes the topic for providing params to a view microfrontend.\n *\n * Params include the {@link ɵMicrofrontendRouteParams#ɵVIEW_CAPABILITY_ID capability id}, params as passed in {@link WorkbenchNavigationExtras.params},\n * and the view qualifier.\n *\n * Params are published as a retained message.\n */\n viewParamsTopic: (viewId: ViewId) => `ɵworkbench/views/${viewId}/params`,\n\n /**\n * Computes the topic for observing the popup origin.\n */\n popupOriginTopic: (popupId: string) => `ɵworkbench/popups/${popupId}/origin`,\n\n /**\n * Computes the topic via which a popup can be closed.\n */\n popupCloseTopic: (popupId: string) => `ɵworkbench/popups/${popupId}/close`,\n\n /**\n * Computes the topic via which to set a result\n */\n popupResultTopic: (popupId: string) => `ɵworkbench/popups/${popupId}/result`,\n\n /**\n * Computes the topic via which the title of a dialog can be set.\n */\n dialogTitleTopic: (dialogId: string) => `ɵworkbench/dialogs/${dialogId}/title`,\n\n /**\n * Computes the topic via which a dialog can be closed.\n */\n dialogCloseTopic: (dialogId: string) => `ɵworkbench/dialogs/${dialogId}/close`,\n} as const;\n","/*\n * Copyright (c) 2018-2024 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\n/**\n * Built-in workbench capabilities.\n */\nexport enum WorkbenchCapabilities {\n /**\n * Contributes a microfrontend for display in workbench view.\n *\n * A view is a visual workbench element for displaying content stacked or side-by-side.\n */\n View = 'view',\n /**\n * Contributes a perspective to the workbench.\n *\n * A perspective is a named arrangement of views. Different perspectives provide a different perspective on the application.\n */\n Perspective = 'perspective',\n /**\n * Contributes a microfrontend for display in workbench popup.\n *\n * A popup is a visual workbench component for displaying content above other content.\n */\n Popup = 'popup',\n /**\n * Contributes a microfrontend for display in workbench dialog.\n *\n * A dialog is a visual element for focused interaction with the user, such as prompting the user for input or confirming actions.\n */\n Dialog = 'dialog',\n /**\n * Contributes a message box in the host app.\n *\n * A message box is a standardized dialog for presenting a message to the user, such as an info, warning or alert,\n * or for prompting the user for confirmation.\n */\n MessageBox = 'messagebox',\n /**\n * Contributes a notification in the host app.\n *\n * A notification appears in the upper-right corner and disappears automatically after a few seconds.\n * It informs the user of a system event, e.g., that a task has been completed or an error has occurred.\n */\n Notification = 'notification',\n}\n","/*\n * Copyright (c) 2018-2022 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\nimport {IntentClient, mapToBody, MessageClient, Qualifier, RequestError} from '@scion/microfrontend-platform';\nimport {Beans} from '@scion/toolkit/bean-manager';\nimport {WorkbenchView} from '../view/workbench-view';\nimport {WorkbenchCapabilities} from '../workbench-capabilities.enum';\nimport {Dictionaries, Dictionary, Maps} from '@scion/toolkit/util';\nimport {ɵWorkbenchCommands} from '../ɵworkbench-commands';\nimport {lastValueFrom} from 'rxjs';\nimport {Empty} from '../utility-types';\n\n/**\n * Enables navigation of workbench views.\n *\n * A view is a visual workbench element for displaying content side-by-side or stacked.\n *\n * A microfrontend provided as a view capability can be opened in a view. The qualifier differentiates between different\n * view capabilities. An application can open the public view capabilities of other applications if it manifests a respective\n * intention.\n *\n * @category Router\n * @category View\n */\nexport class WorkbenchRouter {\n\n /**\n * Navigates to a microfrontend of a view capability based on the given qualifier and extras.\n *\n * 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.\n * 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.\n * This behavior can be changed by setting an explicit navigation target in navigation extras.\n *\n * @param qualifier - Identifies the view capability that provides the microfrontend to display in a view.\n * Passing an empty qualifier (`{}`) allows the microfrontend to update its parameters, restoring updated parameters when the page reloads.\n * Parameter handling can be controlled using the {@link WorkbenchNavigationExtras#paramsHandling} option.\n * @param extras - Options to control navigation.\n * @return Promise that resolves to `true` on successful navigation, or `false` otherwise.\n */\n public async navigate(qualifier: Qualifier | Empty<Qualifier>, extras?: WorkbenchNavigationExtras): Promise<boolean> {\n if (this.isSelfNavigation(qualifier)) {\n return this.updateViewParams(extras);\n }\n else {\n return this.issueViewIntent(qualifier, extras);\n }\n }\n\n private async issueViewIntent(qualifier: Qualifier, extras?: WorkbenchNavigationExtras): Promise<boolean> {\n const navigationExtras: WorkbenchNavigationExtras = {\n ...extras,\n params: undefined, // included in the intent\n paramsHandling: undefined, // only applicable for self-navigation\n };\n const navigate$ = Beans.get(IntentClient).request$<boolean>({type: WorkbenchCapabilities.View, qualifier, params: Maps.coerce(extras?.params)}, navigationExtras);\n try {\n return await lastValueFrom(navigate$.pipe(mapToBody()));\n }\n catch (error) {\n throw (error instanceof RequestError ? error.message : error);\n }\n }\n\n private async updateViewParams(extras?: WorkbenchNavigationExtras): Promise<boolean> {\n const viewCapabilityId = Beans.get(WorkbenchView).snapshot.params.get(ɵMicrofrontendRouteParams.ɵVIEW_CAPABILITY_ID) as string | undefined;\n if (viewCapabilityId === undefined) {\n return false; // Params cannot be updated until the loading of the view is completed\n }\n\n const command: ɵViewParamsUpdateCommand = {\n params: Dictionaries.coerce(extras?.params),\n paramsHandling: extras?.paramsHandling,\n };\n const updateParams$ = Beans.get(MessageClient).request$<boolean>(ɵWorkbenchCommands.viewParamsUpdateTopic(Beans.get(WorkbenchView).id, viewCapabilityId), command);\n try {\n return await lastValueFrom(updateParams$.pipe(mapToBody()));\n }\n catch (error) {\n throw (error instanceof RequestError ? error.message : error);\n }\n }\n\n private isSelfNavigation(qualifier: Qualifier | Empty<Qualifier>): boolean {\n if (Object.keys(qualifier).length === 0) {\n if (!Beans.opt(WorkbenchView)) {\n throw Error('[NavigateError] Self-navigation is supported only if in the context of a view.');\n }\n return true;\n }\n return false;\n }\n}\n\n/**\n * Options to control the navigation.\n *\n * @category Router\n * @category View\n */\nexport interface WorkbenchNavigationExtras {\n /**\n * Passes data to the view.\n *\n * The view can declare mandatory and optional parameters. No additional parameters are allowed. Refer to the documentation of the capability for more information.\n */\n params?: Map<string, any> | Dictionary;\n /**\n * Instructs the workbench router how to handle params in self-navigation.\n *\n * Self-navigation allows the microfrontend to update its parameters, restoring updated parameters when the page reloads.\n * Setting a `paramsHandling` strategy has no effect on navigations other than self-navigation. A self-navigation is\n * initiated by passing an empty qualifier.\n *\n * One of:\n * * `replace`: Replaces current parameters (default).\n * * `merge`: Merges new parameters with current parameters, with new parameters of equal name overwriting existing parameters.\n * A parameter can be removed by passing `undefined` as its value.\n */\n paramsHandling?: 'merge' | 'replace';\n /**\n * Controls where to open the view. Defaults to `auto`.\n *\n * One of:\n * - 'auto': Navigates existing views that match the qualifier and required params, or opens a new view otherwise. Optional parameters do not affect view resolution.\n * - 'blank': Opens a new view and navigates it.\n * - <viewId>: Navigates the specified view. If already opened, replaces it, or opens a new view otherwise.\n */\n target?: string | 'blank' | 'auto';\n /**\n * Controls which part to navigate views in.\n *\n * If target is `blank`, opens the view in the specified part.\n * If target is `auto`, navigates matching views in the specified part, or opens a new view in that part otherwise.\n *\n * If the specified part is not in the layout, opens the view in the active part, with the active part of the main area taking precedence.\n */\n partId?: string;\n /**\n * Instructs the router to activate the view. Defaults to `true`.\n */\n activate?: boolean;\n /**\n * Closes views that match the specified qualifier and required parameters. Optional parameters do not affect view resolution.\n *\n * The parameters support the asterisk wildcard value (`*`) to match views with any value for a parameter.\n *\n * Only views for which the application has an intention can be closed.\n */\n close?: boolean;\n /**\n * Specifies where to insert the view into the tab bar. Has no effect if navigating an existing view. Defaults to after the active view.\n */\n position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view';\n /**\n * Specifies CSS class(es) to add to the view, e.g., to locate the view in tests.\n */\n cssClass?: string | string[];\n}\n\n/**\n * Command object for instructing the Workbench Router to update view params in self-navigation.\n *\n * @docs-private Not public API, intended for internal use only.\n * @ignore\n */\nexport interface ɵViewParamsUpdateCommand {\n /**\n * @see WorkbenchNavigationExtras#params\n */\n params: Dictionary;\n /**\n * @see WorkbenchNavigationExtras#paramsHandling\n */\n paramsHandling?: 'merge' | 'replace';\n}\n\n/**\n * Named parameters used in microfrontend routes.\n *\n * @docs-private Not public API, intended for internal use only.\n * @ignore\n */\nexport enum ɵMicrofrontendRouteParams {\n /**\n * Named path segment in the microfrontend route representing the view capability for which to embed its microfrontend.\n */\n ɵVIEW_CAPABILITY_ID = 'ɵViewCapabilityId',\n}\n","/*\n * Copyright (c) 2018-2022 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\nimport {MonoTypeOperatorFunction, Observable} from 'rxjs';\nimport {Beans} from '@scion/toolkit/bean-manager';\nimport {ObservableDecorator} from '@scion/microfrontend-platform';\n\n/**\n * Decorates the source with registered {@link ObservableDecorator}, if any.\n *\n * @internal\n */\nexport function decorateObservable<T>(): MonoTypeOperatorFunction<T> {\n return (source$: Observable<T>) => Beans.opt(ObservableDecorator)?.decorate$(source$) ?? source$;\n}\n","/*\n * Copyright (c) 2018-2024 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\nimport {Beans, PreDestroy} from '@scion/toolkit/bean-manager';\nimport {combineLatest, firstValueFrom, merge, Observable, OperatorFunction, pipe, Subject, Subscription} from 'rxjs';\nimport {WorkbenchViewCapability} from './workbench-view-capability';\nimport {ManifestService, mapToBody, MessageClient, MicrofrontendPlatformClient} from '@scion/microfrontend-platform';\nimport {ɵWorkbenchCommands} from '../ɵworkbench-commands';\nimport {distinctUntilChanged, filter, map, mergeMap, shareReplay, skip, switchMap, takeUntil, tap} from 'rxjs/operators';\nimport {ɵMicrofrontendRouteParams} from '../routing/workbench-router';\nimport {Observables} from '@scion/toolkit/util';\nimport {CanCloseFn, CanCloseRef, ViewId, ViewSnapshot, WorkbenchView} from './workbench-view';\nimport {decorateObservable} from '../observable-decorator';\n\nexport class ɵWorkbenchView implements WorkbenchView, PreDestroy {\n\n private _propertyChange$ = new Subject<'title' | 'heading' | 'dirty' | 'closable'>();\n private _destroy$ = new Subject<void>();\n /**\n * Observable that emits when the application loaded into the current view receives an unloading event,\n * i.e., is just about to be replaced by a microfrontend of another application.\n */\n private _beforeUnload$: Observable<void>;\n /**\n * Observable that emits before navigating to a different microfrontend of the same app.\n */\n private _beforeInAppNavigation$ = new Subject<void>();\n private _canCloseFn: CanCloseFn | undefined;\n private _canCloseSubscription: Subscription | undefined;\n\n public active$: Observable<boolean>;\n public partId$: Observable<string>;\n public params$: Observable<ReadonlyMap<string, any>>;\n public capability$: Observable<WorkbenchViewCapability>;\n public whenProperties: Promise<void>;\n public snapshot: ViewSnapshot = {\n params: new Map<string, any>(),\n partId: undefined!,\n };\n\n constructor(public id: ViewId) {\n this._beforeUnload$ = Beans.get(MessageClient).observe$<void>(ɵWorkbenchCommands.viewUnloadingTopic(this.id))\n .pipe(map(() => undefined), shareReplay({refCount: false, bufferSize: 1}));\n\n this.params$ = Beans.get(MessageClient).observe$<Map<string, any>>(ɵWorkbenchCommands.viewParamsTopic(this.id))\n .pipe(\n mapToBody(),\n shareReplay({refCount: false, bufferSize: 1}),\n decorateObservable(),\n takeUntil(merge(this._beforeUnload$, this._destroy$)),\n );\n\n this.capability$ = this.params$\n .pipe(\n map(params => params.get(ɵMicrofrontendRouteParams.ɵVIEW_CAPABILITY_ID) as string),\n lookupViewCapabilityAndShareReplay(),\n decorateObservable(),\n takeUntil(this._beforeUnload$),\n );\n\n this.active$ = Beans.get(MessageClient).observe$<boolean>(ɵWorkbenchCommands.viewActiveTopic(this.id))\n .pipe(\n mapToBody(),\n shareReplay({refCount: false, bufferSize: 1}),\n decorateObservable(),\n takeUntil(this._beforeUnload$),\n );\n\n this.partId$ = Beans.get(MessageClient).observe$<string>(ɵWorkbenchCommands.viewPartIdTopic(this.id))\n .pipe(\n mapToBody(),\n shareReplay({refCount: false, bufferSize: 1}),\n decorateObservable(),\n takeUntil(this._beforeUnload$),\n );\n // Update part id snapshot when part changes.\n this.partId$\n .pipe(takeUntil(this._destroy$))\n .subscribe(partId => this.snapshot.partId = partId);\n // Update params snapshot when params change.\n this.params$\n .pipe(takeUntil(this._destroy$))\n .subscribe(params => this.snapshot.params = new Map(params));\n // Detect navigation to a different view capability of the same app.\n // Do NOT use `capability$` observable to detect capability change, as its lookup is asynchronous.\n this.params$\n .pipe(\n map(params => params.get(ɵMicrofrontendRouteParams.ɵVIEW_CAPABILITY_ID) as string),\n distinctUntilChanged(),\n skip(1), // skip the initial navigation\n takeUntil(merge(this._beforeUnload$, this._destroy$)),\n )\n .subscribe(() => {\n this._beforeInAppNavigation$.next();\n this._canCloseFn = undefined;\n this._canCloseSubscription?.unsubscribe();\n this._canCloseSubscription = undefined;\n });\n\n // Signal view properties available.\n this.whenProperties = firstValueFrom(combineLatest([this.partId$, this.params$])).then();\n }\n\n /** @inheritDoc */\n public signalReady(): void {\n MicrofrontendPlatformClient.signalReady();\n }\n\n /** @inheritDoc */\n public setTitle(title: string | Observable<string>): void {\n this._propertyChange$.next('title');\n\n Observables.coerce(title)\n .pipe(\n mergeMap(it => Beans.get(MessageClient).publish(ɵWorkbenchCommands.viewTitleTopic(this.id), it)),\n takeUntil(merge(this._propertyChange$.pipe(filter(prop => prop === 'title')), this._beforeInAppNavigation$, this._beforeUnload$, this._destroy$)),\n )\n .subscribe();\n }\n\n /** @inheritDoc */\n public setHeading(heading: string | Observable<string>): void {\n this._propertyChange$.next('heading');\n\n Observables.coerce(heading)\n .pipe(\n mergeMap(it => Beans.get(MessageClient).publish(ɵWorkbenchCommands.viewHeadingTopic(this.id), it)),\n takeUntil(merge(this._propertyChange$.pipe(filter(prop => prop === 'heading')), this._beforeInAppNavigation$, this._beforeUnload$, this._destroy$)),\n )\n .subscribe();\n }\n\n /** @inheritDoc */\n public markDirty(dirty: undefined | boolean | Observable<boolean>): void {\n this._propertyChange$.next('dirty');\n\n Observables.coerce(dirty ?? true)\n .pipe(\n mergeMap(it => Beans.get(MessageClient).publish(ɵWorkbenchCommands.viewDirtyTopic(this.id), it)),\n takeUntil(merge(this._propertyChange$.pipe(filter(prop => prop === 'dirty')), this._beforeInAppNavigation$, this._beforeUnload$, this._destroy$)),\n )\n .subscribe();\n }\n\n /** @inheritDoc */\n public setClosable(closable: boolean | Observable<boolean>): void {\n this._propertyChange$.next('closable');\n\n Observables.coerce(closable)\n .pipe(\n mergeMap(it => Beans.get(MessageClient).publish(ɵWorkbenchCommands.viewClosableTopic(this.id), it)),\n takeUntil(merge(this._propertyChange$.pipe(filter(prop => prop === 'closable')), this._beforeInAppNavigation$, this._beforeUnload$, this._destroy$)),\n )\n .subscribe();\n }\n\n /** @inheritDoc */\n public close(): void {\n void Beans.get(MessageClient).publish(ɵWorkbenchCommands.viewCloseTopic(this.id));\n }\n\n /** @inheritDoc */\n public canClose(canClose: CanCloseFn): CanCloseRef {\n this._canCloseFn = canClose;\n this._canCloseSubscription ??= Beans.get(MessageClient).onMessage(ɵWorkbenchCommands.canCloseTopic(this.id), () => this._canCloseFn?.() ?? true);\n return {\n dispose: () => {\n if (canClose === this._canCloseFn) {\n this._canCloseSubscription!.unsubscribe();\n this._canCloseSubscription = undefined;\n this._canCloseFn = undefined;\n }\n },\n };\n }\n\n public preDestroy(): void {\n this._canCloseSubscription?.unsubscribe();\n this._canCloseSubscription = undefined;\n this._destroy$.next();\n }\n}\n\n/**\n * Context key to retrieve the view ID for microfrontends embedded in the context of a workbench view.\n *\n * @docs-private Not public API, intended for internal use only.\n * @ignore\n * @see {@link ContextService}\n */\nexport const ɵVIEW_ID_CONTEXT_KEY = 'ɵworkbench.view.id';\n\n/**\n * Looks up the corresponding view capability for each capability id emitted by the source Observable.\n *\n * For new subscribers, the most recently looked up capability is replayed. It is guaranteed that no stale capability\n * is replayed, that is, that the replayed capability always corresponds to the most recent emitted capability id of\n * the source Observable.\n */\nfunction lookupViewCapabilityAndShareReplay(): OperatorFunction<string, WorkbenchViewCapability> {\n let latestViewCapabilityId: string;\n\n return pipe(\n distinctUntilChanged(),\n tap(viewCapabilityId => latestViewCapabilityId = viewCapabilityId),\n switchMap(viewCapabilityId => Beans.get(ManifestService).lookupCapabilities$<WorkbenchViewCapability>({id: viewCapabilityId})), // async call; long-living\n map(viewCapabilities => viewCapabilities[0]!),\n // Replay the latest looked up capability for new subscribers.\n shareReplay({refCount: false, bufferSize: 1}),\n // Ensure not to replay a stale capability upon the subscription of new subscribers. For this reason, we install a filter to filter them out.\n // 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.\n filter(viewCapability => latestViewCapabilityId === viewCapability.metadata!.id),\n );\n}\n","/*\n * Copyright (c) 2018-2022 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\nimport {Beans, Initializer} from '@scion/toolkit/bean-manager';\nimport {ContextService} from '@scion/microfrontend-platform';\nimport {ViewId, WorkbenchView} from './workbench-view';\nimport {ɵVIEW_ID_CONTEXT_KEY, ɵWorkbenchView} from './ɵworkbench-view';\n\n/**\n * Registers {@link WorkbenchView} in the bean manager if in the context of a workbench view.\n *\n * @internal\n */\nexport class WorkbenchViewInitializer implements Initializer {\n\n public async init(): Promise<void> {\n const viewId = await Beans.get(ContextService).lookup<ViewId>(ɵVIEW_ID_CONTEXT_KEY);\n if (viewId !== null) {\n const workbenchView = new ɵWorkbenchView(viewId);\n Beans.register(WorkbenchView, {useValue: workbenchView});\n // Wait until initialized the view, supporting synchronous access to view properties in microfrontend constructor.\n await workbenchView.whenProperties;\n }\n }\n}\n","/*\n * Copyright (c) 2018-2022 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\nimport {IntentClient, mapToBody, MessageClient, Qualifier, RequestError} from '@scion/microfrontend-platform';\nimport {Beans} from '@scion/toolkit/bean-manager';\nimport {finalize} from 'rxjs/operators';\nimport {WorkbenchCapabilities} from '../workbench-capabilities.enum';\nimport {Defined, Maps, Observables} from '@scion/toolkit/util';\nimport {fromBoundingClientRect$} from '@scion/toolkit/observable';\nimport {concat, firstValueFrom, NEVER, Observable} from 'rxjs';\nimport {WorkbenchView} from '../view/workbench-view';\nimport {ɵWorkbenchPopupCommand} from './workbench-popup-open-command';\nimport {ɵWorkbenchCommands} from '../ɵworkbench-commands';\nimport {UUID} from '@scion/toolkit/uuid';\nimport {WorkbenchPopupConfig} from './workbench-popup.config';\nimport {PopupOrigin} from './popup.origin';\n\n/**\n * Displays a microfrontend in a popup.\n *\n * A popup is a visual workbench component for displaying content above other content. It is positioned relative to an anchor and\n * moves when the anchor moves. Unlike a dialog, the popup closes on focus loss.\n *\n * A microfrontend provided as a `popup` capability can be opened in a popup. The qualifier differentiates between different\n * popup capabilities. An application can open the public popup capabilities of other applications if it manifests a respective\n * intention.\n *\n * @category Popup\n * @see WorkbenchPopupCapability\n */\nexport class WorkbenchPopupService {\n\n /**\n * Displays a microfrontend in a workbench popup based on the given qualifier.\n *\n * The qualifier identifies the microfrontend to display in the popup.\n *\n * The anchor is used to position the popup based on its preferred alignment:\n * - Using an element: The popup opens and sticks to the element.\n * - Using coordinates: The popup opens and sticks relative to the view or page bounds.\n *\n * If the popup is opened within a view, it only displays if the view is active and closes when the view is closed.\n *\n * By default, the popup closes on focus loss or when pressing the escape key.\n *\n * Pass data to the popup using parameters. Only declared parameters are allowed. Refer to the capability documentation for details.\n *\n * @param qualifier - Identifies the popup capability that provides the microfrontend for display as popup.\n * @param config - Controls popup behavior.\n * @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,\n * e.g., because of missing the intention or because no `popup` capability matching the qualifier and visible to the application\n * was found.\n */\n public async open<T>(qualifier: Qualifier, config: WorkbenchPopupConfig): Promise<T | undefined> {\n const popupCommand: ɵWorkbenchPopupCommand = {\n popupId: UUID.randomUUID(),\n align: config.align,\n closeStrategy: config.closeStrategy,\n cssClass: config.cssClass,\n context: {\n viewId: Defined.orElse(config.context?.viewId, () => Beans.opt(WorkbenchView)?.id),\n },\n };\n const popupOriginReporter = this.observePopupOrigin$(config)\n .pipe(finalize(() => void Beans.get(MessageClient).publish<PopupOrigin>(ɵWorkbenchCommands.popupOriginTopic(popupCommand.popupId), undefined, {retain: true})))\n .subscribe(origin => void Beans.get(MessageClient).publish<PopupOrigin>(ɵWorkbenchCommands.popupOriginTopic(popupCommand.popupId), origin, {retain: true}));\n\n try {\n const params = Maps.coerce(config.params);\n const openPopup$ = Beans.get(IntentClient).request$<T>({type: WorkbenchCapabilities.Popup, qualifier, params}, popupCommand).pipe(mapToBody());\n return await firstValueFrom(openPopup$, {defaultValue: undefined});\n }\n catch (error) {\n throw (error instanceof RequestError ? error.message : error);\n }\n finally {\n popupOriginReporter.unsubscribe();\n }\n }\n\n /**\n * Observes the position of the popup anchor.\n *\n * The Observable emits the anchor's initial position, and each time its position changes.\n * The Observable never completes.\n */\n private observePopupOrigin$(config: WorkbenchPopupConfig): Observable<PopupOrigin> {\n if (config.anchor instanceof Element) {\n return fromBoundingClientRect$(config.anchor as HTMLElement);\n }\n else {\n return concat(Observables.coerce(config.anchor), NEVER);\n }\n }\n}\n","/*\n * Copyright (c) 2018-2024 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\nimport {WorkbenchPopupCapability} from './workbench-popup-capability';\nimport {Beans} from '@scion/toolkit/bean-manager';\nimport {ContextService, MessageClient, MicrofrontendPlatformClient, OUTLET_CONTEXT, OutletContext} from '@scion/microfrontend-platform';\nimport {ɵWorkbenchCommands} from '../ɵworkbench-commands';\nimport {ɵPopupContext} from './workbench-popup-context';\nimport {WorkbenchPopupReferrer} from './workbench-popup-referrer';\n\n/**\n * A popup is a visual workbench component for displaying content above other content.\n *\n * If a microfrontend lives in the context of a workbench popup, regardless of its embedding level, it can inject an instance\n * of this class to interact with the workbench popup, such as reading passed parameters or closing the popup.\n *\n * #### Preferred Size\n * You can report preferred popup size using {@link @scion/microfrontend-platform!PreferredSizeService}. Typically, you would\n * subscribe to size changes of the microfrontend's primary content and report it. As a convenience, {@link @scion/microfrontend-platform!PreferredSizeService}\n * provides API to pass an element for automatic dimension monitoring. If your content can grow and shrink, e.g., if using expandable\n * panels, consider positioning primary content out of the document flow, that is, setting its position to `absolute`. This way,\n * you give it infinite space so that it can always be rendered at its preferred size.\n *\n * ```typescript\n * Beans.get(PreferredSizeService).fromDimension(<HTMLElement>);\n * ```\n *\n * Note that the microfrontend may take some time to load, causing the popup to flicker when opened. Therefore, for fixed-sized\n * popups, consider declaring the popup size in the popup capability.\n *\n * @category Popup\n */\nexport abstract class WorkbenchPopup<R = unknown> {\n\n /**\n * Capability that represents the microfrontend loaded into this workbench popup.\n */\n public abstract readonly capability: WorkbenchPopupCapability;\n\n /**\n * Signals readiness, notifying the workbench that this popup has completed initialization.\n *\n * If `showSplash` is set to `true` on the popup capability, the workbench displays a splash until the popup microfrontend signals readiness.\n *\n * @see WorkbenchPopupCapability.properties.showSplash\n */\n public abstract signalReady(): void;\n\n /**\n * Provides information about the context in which this popup was opened.\n */\n public abstract readonly referrer: WorkbenchPopupReferrer;\n\n /**\n * Parameters including qualifier entries as passed for navigation by the popup opener.\n */\n public abstract readonly params: Map<string, any>;\n\n /**\n * Sets a result that will be passed to the popup opener when the popup is closed on focus loss {@link CloseStrategy#onFocusLost}.\n */\n public abstract setResult(result?: R): void;\n\n /**\n * Closes the popup. Optionally, pass a result or an error to the popup opener.\n */\n public abstract close(result?: R | Error): void;\n}\n\n/**\n * @ignore\n */\nexport class ɵWorkbenchPopup implements WorkbenchPopup {\n\n public params: Map<string, any>;\n public capability: WorkbenchPopupCapability;\n public referrer: WorkbenchPopupReferrer;\n\n constructor(private _context: ɵPopupContext) {\n this.capability = this._context.capability;\n this.params = this._context.params;\n this.referrer = this._context.referrer;\n void this.requestFocus();\n }\n\n /**\n * @inheritDoc\n */\n public signalReady(): void {\n MicrofrontendPlatformClient.signalReady();\n }\n\n /**\n * @inheritDoc\n */\n public setResult(result?: unknown): void {\n void Beans.get(MessageClient).publish(ɵWorkbenchCommands.popupResultTopic(this._context.popupId), result);\n }\n\n /**\n * @inheritDoc\n */\n public close(result?: unknown | Error): void {\n if (result instanceof Error) {\n const headers = new Map().set(ɵWorkbenchPopupMessageHeaders.CLOSE_WITH_ERROR, true);\n void Beans.get(MessageClient).publish(ɵWorkbenchCommands.popupCloseTopic(this._context.popupId), result.message, {headers});\n }\n else {\n void Beans.get(MessageClient).publish(ɵWorkbenchCommands.popupCloseTopic(this._context.popupId), result);\n }\n }\n\n /**\n * If the document is not yet focused, make it focusable and request the focus.\n *\n * In order to close the popup on focus loss, microfrontend content must gain the focus first.\n */\n private async requestFocus(): Promise<void> {\n // Request focus only if this microfrontend is the actual popup microfrontend,\n // i.e. not nested microfrontends in the popup.\n const contexts = await Beans.get(ContextService).lookup<OutletContext>(OUTLET_CONTEXT, {collect: true});\n if (contexts.length > 1) {\n return;\n }\n\n // Do nothing if an element of this microfrontend already has the focus.\n if (document.activeElement !== document.body) {\n return;\n }\n\n // Ensure the body element to be focusable.\n if (document.body.getAttribute('tabindex') === null) {\n document.body.style.outline = 'none';\n document.body.setAttribute('tabindex', '-1');\n }\n\n // Request the focus.\n document.body.focus();\n }\n}\n\n/**\n * Message headers to interact with the workbench popup.\n *\n * @docs-private Not public API, intended for internal use only.\n * @ignore\n */\nexport enum ɵWorkbenchPopupMessageHeaders {\n CLOSE_WITH_ERROR = 'ɵWORKBENCH-POPUP:CLOSE_WITH_ERROR',\n}\n","/*\n * Copyright (c) 2018-2022 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\nimport {WorkbenchPopupCapability} from './workbench-popup-capability';\nimport {WorkbenchPopupReferrer} from './workbench-popup-referrer';\n\n/**\n * Context when displaying a microfrontend in a popup.\n *\n * This object can be obtained from the {@link ContextService} using the name {@link ɵPOPUP_CONTEXT}.\n *\n * @docs-private Not public API, intended for internal use only.\n * @ignore\n */\nexport interface ɵPopupContext {\n popupId: string;\n params: Map<string, any>;\n capability: WorkbenchPopupCapability;\n closeOnFocusLost: boolean;\n referrer: WorkbenchPopupReferrer;\n}\n\n/**\n * Key for obtaining the current popup context using {@link ContextService}.\n *\n * The popup context is only available to microfrontends loaded in a workbench popup.\n *\n * @docs-private Not public API, intended for internal use only.\n * @ignore\n * @see {@link ContextService}\n * @see {@link ɵPopupContext}\n */\nexport const ɵPOPUP_CONTEXT = 'ɵworkbench.popup';\n","/*\n * Copyright (c) 2018-2022 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\nimport {Beans, Initializer} from '@scion/toolkit/bean-manager';\nimport {ContextService} from '@scion/microfrontend-platform';\nimport {WorkbenchPopup, ɵWorkbenchPopup} from './workbench-popup';\nimport {ɵPOPUP_CONTEXT, ɵPopupContext} from './workbench-popup-context';\n\n/**\n * Registers {@link WorkbenchPopup} in the bean manager if in the context of a workbench popup.\n *\n * @internal\n */\nexport class WorkbenchPopupInitializer implements Initializer {\n\n public async init(): Promise<void> {\n const popupContext = await Beans.get(ContextService).lookup<ɵPopupContext>(ɵPOPUP_CONTEXT);\n if (popupContext !== null) {\n Beans.register(WorkbenchPopup, {useValue: new ɵWorkbenchPopup(popupContext)});\n }\n }\n}\n","/*\n * Copyright (c) 2018-2024 Swiss Federal Railways\n *\n * This program and the accompanying materials are made\n * available under the terms of the Eclipse Public License 2.0\n * which is available at https://www.eclipse.org/legal/epl-2.0/\n *\n * SPDX-License-Identifier: EPL-2.0\n */\n\nimport {Qualifier} from '@scion/microfrontend-platform';\nimport {WorkbenchMessageBoxOptions} from './workbench-message-box.options';\n\n/**\n * Displays a microfrontend in a message box.\n *\n * A message box is a standardized dialog for presenting a message to the user, such as an info, warning or alert,\n * or for prompting the user for confirmation.\n *\n * A microfrontend provided as a `messagebox` capability can be opened in a message box. The qualifier differentiates between different\n * message box capabilities. An application can open the public message box capabilities of other applications if it manifests a respective\n * intention.\n *\n * Displayed on top of other content, a message box blocks interaction with other parts of the application. Multiple message boxes are stacked,\n * and only the topmost message box in each modality stack can be interacted with.\n *\n * 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\n * with other views. An application-modal message box blocks the workbench, or the browser's viewport if configured in the workbench\n * host application.\n *\n * @category MessageBox\n * @see WorkbenchMessageBoxCapability\n */\nexport abstract class WorkbenchMessageBoxService {\n\n /**\n * Displays the specified message in a message box.\n *\n * By default, the calling context determines the modality of the message box. If the message box is opened from a view, only this view is blocked.\n * To open the message box with a different modality, specify the modality in {@link WorkbenchMessageBoxOptions.modality}.\n *\n * **This API requires the following intention: `{\"type\": \"messagebox\"}`**\n *\n * @param message - Specifies the text to display.\n * @param options - Controls the appearance and behavior of the message box.\n * @returns Promise that resolves to the key of the action button that the user clicked to close the message box,\n * or that rejects if the message box couldn't be opened, e.g., because of missing the intention.\n */\n public abstract open(message: string, options?: WorkbenchMessageBoxOptions): Promise<string