UNPKG

@scion/workbench

Version:

SCION Workbench enables the creation of Angular web applications that require a flexible layout to display content side-by-side or stacked, all personalizable by the user via drag & drop. This type of layout is ideal for applications with non-linear workf

1,019 lines (993 loc) 1.18 MB
import * as i0 from '@angular/core'; import { effect, untracked, Injectable, inject, Injector, signal, booleanAttribute, numberAttribute, DestroyRef, DOCUMENT, computed, InjectionToken, assertNotInReactiveContext, isSignal, createEnvironmentInjector, EnvironmentInjector, runInInjectionContext, Pipe, isDevMode, Component, ChangeDetectionStrategy, makeEnvironmentProviders, NgZone, ɵZONELESS_ENABLED as _ZONELESS_ENABLED, afterNextRender, ApplicationInitStatus, forwardRef, Directive, createComponent, ApplicationRef, ElementRef, viewChild, afterRenderEffect, input, output, ViewContainerRef, TemplateRef, Renderer2, ChangeDetectorRef, linkedSignal, HostListener, HostBinding, viewChildren, IterableDiffers, CUSTOM_ELEMENTS_SCHEMA, provideEnvironmentInitializer } from '@angular/core'; import { RouterOutlet, Router, PRIMARY_OUTLET, ChildrenOutletContexts, NavigationStart, ActivationStart, ActivationEnd, UrlSegment, RouterEvent, NavigationEnd, NavigationCancel, NavigationError, ActivatedRoute } from '@angular/router'; import { Arrays, Objects, Defined, Observables, Maps, Dictionaries } from '@scion/toolkit/util'; import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { skipUntil, mergeMap, observeOn, catchError, map, startWith, take, filter, switchMap as switchMap$1, skip, subscribeOn, finalize, distinctUntilChanged, takeUntil, first, combineLatestWith } from 'rxjs/operators'; import { coerceElement } from '@angular/cdk/coercion'; import { UUID } from '@scion/toolkit/uuid'; import { Observable, Subject, asapScheduler, AsyncSubject, lastValueFrom, iif, switchMap, race, pairwise, EMPTY, of, from, animationFrameScheduler, merge, fromEvent, noop, BehaviorSubject, mergeWith, mergeMap as mergeMap$1, delay, identity, debounce, animationFrames, firstValueFrom, concatWith, withLatestFrom, NEVER, map as map$1, share, timer, ReplaySubject, combineLatest, tap } from 'rxjs'; import { ManifestService, MicrofrontendPlatform, PlatformState, QualifierMatcher, ObservableDecorator, MicrofrontendPlatformConfig, MicrofrontendPlatformHost, APP_IDENTITY, MessageClient, mapToBody, OutletRouter, MessageHeaders, ResponseStatusCodes, ParamMatcher, CapabilityInterceptor, HostManifestInterceptor, IntentInterceptor, IntentClient, PlatformPropertyService } from '@scion/microfrontend-platform'; import { tapFirst, subscribeIn, observeIn, filterArray } from '@scion/toolkit/operators'; import { WorkbenchCapabilities, WorkbenchRouter as WorkbenchRouter$1, ɵWorkbenchRouter as _WorkbenchRouter, WorkbenchDialogService as WorkbenchDialogService$1, ɵWorkbenchDialogService as _WorkbenchDialogService, WorkbenchMessageBoxService as WorkbenchMessageBoxService$1, ɵWorkbenchMessageBoxService as _WorkbenchMessageBoxService, WorkbenchPopupService as WorkbenchPopupService$1, ɵWorkbenchPopupService as _WorkbenchPopupService, WorkbenchNotificationService as WorkbenchNotificationService$1, ɵWorkbenchNotificationService as _WorkbenchNotificationService, WorkbenchTextService, ɵWorkbenchTextService as _WorkbenchTextService, ɵTHEME_CONTEXT_KEY as _THEME_CONTEXT_KEY, ɵVIEW_ID_CONTEXT_KEY as _VIEW_ID_CONTEXT_KEY, ɵWorkbenchCommands as _WorkbenchCommands, ɵVIEW_CAPABILITY_ID_PARAM_NAME as _VIEW_CAPABILITY_ID_PARAM_NAME, ɵWORKBENCH_PART_CONTEXT as _WORKBENCH_PART_CONTEXT, eNOTIFICATION_MESSAGE_PARAM, ɵNOTIFICATION_CONTEXT as _NOTIFICATION_CONTEXT, ɵDIALOG_CONTEXT as _DIALOG_CONTEXT, ɵWorkbenchDialogMessageHeaders as _WorkbenchDialogMessageHeaders, ɵMESSAGE_BOX_CONTEXT as _MESSAGE_BOX_CONTEXT, eMESSAGE_BOX_MESSAGE_PARAM, WorkbenchClient, ɵPOPUP_CONTEXT as _POPUP_CONTEXT, ɵWorkbenchPopupMessageHeaders as _WorkbenchPopupMessageHeaders } from '@scion/workbench-client'; import { fromMutation$, fromResize$ } from '@scion/toolkit/observable'; import { boundingClientRect, SciDimensionDirective, dimension } from '@scion/components/dimension'; import { SciViewportComponent } from '@scion/components/viewport'; import { SciSashboxComponent, SciSashDirective } from '@scion/components/sashbox'; import { NgTemplateOutlet, NgComponentOutlet, AsyncPipe, KeyValuePipe, NgClass, LocationStrategy, Location } from '@angular/common'; import { OverlayRef, Overlay, OverlayConfig } from '@angular/cdk/overlay'; import { ComponentPortal, CdkPortalOutlet, DomPortalOutlet } from '@angular/cdk/portal'; import * as i1 from '@angular/forms'; import { NonNullableFormBuilder, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; import { FocusMonitor, CdkTrapFocus } from '@angular/cdk/a11y'; import { trigger, transition, style, animate } from '@angular/animations'; import { Beans } from '@scion/toolkit/bean-manager'; import { SciThrobberComponent } from '@scion/components/throbber'; import { Crypto } from '@scion/toolkit/crypto'; /* * 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 */ /** * Configuration of the SCION Workbench. */ class WorkbenchConfig { } /* * 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 */ /** * Throws the specified error, useful in arrow functions that do not support throwing without a body. */ function throwError(error) { throw Error(error); } /* * 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 */ /** * Returns a Promise that resolves when signaling `true`. * * This function must be called within an injection context, or an explicit {@link Injector} passed. */ function resolveWhen(condition, options) { return new Promise(resolve => { const effectRef = effect(() => { if (condition()) { untracked(() => resolve()); effectRef.destroy(); } }, { ...(ngDevMode ? { debugName: "effectRef" } : /* istanbul ignore next */ {}), injector: options?.injector }); }); } /* * 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 */ /** * Provides persistent storage to the SCION Workbench. */ class WorkbenchStorage { } /** * Default storage used by the SCION workbench to persist data in local storage. * * Local storage maintains a persistent storage area per origin. Data does not expire and remains after the browser restarts. * * @internal */ class DefaultWorkbenchStorage { load(key) { return localStorage.getItem(key); } store(key, value) { localStorage.setItem(key, value); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DefaultWorkbenchStorage, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DefaultWorkbenchStorage }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: DefaultWorkbenchStorage, decorators: [{ type: Injectable }] }); /* * 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 */ /** * Creates a writable signal to read and write specified flag from and to workbench storage. */ function renderingFlag(storageKey, defaultValue, options) { const injector = options?.injector ?? inject(Injector); const workbenchStorage = injector.get(WorkbenchStorage); const flag = signal(defaultValue, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "flag" }] : /* istanbul ignore next */ [])); // Read flag from storage. const whenReadFromStorage = readFromStorage(storageKey).then(storedValue => { if (storedValue === null) { return; } switch (typeof defaultValue) { case 'number': { flag.set(numberAttribute(storedValue, defaultValue)); break; } case 'boolean': { flag.set(booleanAttribute(storedValue)); break; } default: { flag.set(storedValue); } } }); // Write flag to storage. toObservable(flag, { injector }) .pipe(skipUntil(whenReadFromStorage), takeUntilDestroyed(injector.get(DestroyRef))) .subscribe(value => void workbenchStorage.store(storageKey, `${value}`)); return flag; } async function readFromStorage(key) { return inject(WorkbenchStorage).load(key); } /* * 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 */ /** * Generates a UID (unique identifier). */ const UID = { /** * Generates a UID (unique identifier) with length 8. */ randomUID: () => { return UUID.randomUUID().substring(0, 8); }, }; /* * 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 */ /** * Returns the value clamped to the inclusive range of min and max. */ function clamp(value, minmax) { return Math.max(minmax.min, Math.min(value, minmax.max)); } /** * Creates an HTML element and optionally adds it to the DOM. */ function createElement(tag, options) { const element = document.createElement(tag); options.style && setStyle(element, options.style); options.cssClass && element.classList.add(...Arrays.coerce(options.cssClass)); options.attributes && setAttribute(element, options.attributes); options.text && (element.innerText = options.text); options.parent?.appendChild(element); return element; } /** * Applies the given style(s) to the given element. * * Specify styles to be modified by passing a dictionary containing CSS property names (hyphen case). * To remove a style, set its value to `null`. */ function setStyle(element, styles) { const target = coerceElement(element); Object.entries(styles).forEach(([name, value]) => { if (value === null) { target.style.removeProperty(name); } else { target.style.setProperty(name, value); } }); } /** * Sets the given attribute(s) on the given element. * To remove an attribute, set its value to `null`. */ function setAttribute(element, attributes) { const target = coerceElement(element); Object.entries(attributes).forEach(([name, value]) => { if (value === null) { target.removeAttribute(name); } else { target.setAttribute(name, value); } }); } /** * Sets specified CSS variable(s) to the given element. * * To remove a CSS variable, set its value to `null`, or use {@link unsetCssVariable}. */ function setCssVariable(element, variables) { const target = coerceElement(element); Object.entries(variables).forEach(([name, value]) => { if (value === null) { target.style.removeProperty(name); } else { target.style.setProperty(name, value); } }); } /** * Removes specified CSS variable(s) from the given element. */ function unsetCssVariable(element, ...names) { const target = coerceElement(element); names.forEach(name => target.style.removeProperty(name)); } /** * Reads and parses specified CSS variable from given element. Defaults to the specified value if not available. */ function readCssVariable(element, variable, defaultValue) { const value = getComputedStyle(element).getPropertyValue(variable) || defaultValue; // empty string if not set switch (typeof defaultValue) { case 'number': { return numberAttribute(value, defaultValue); } case 'boolean': { return booleanAttribute(value); } default: { return value; } } } /** * Sets specified CSS class(es) to the given element. */ function setCssClass(element, ...classes) { coerceElement(element).classList.add(...classes); } /** * Removes specified CSS class(es) from the given element. */ function unsetCssClass(element, ...classes) { coerceElement(element).classList.remove(...classes); } /** * Reads the current vertical and horizontal translation of given element. */ function getCssTranslation(element) { const transformStyle = getComputedStyle(element).getPropertyValue('transform'); if (transformStyle === 'none') { return { translateX: 'none', translateY: 'none', }; } // The transform property returns a matrix in the form `matrix(a, b, c, d, tx, ty)`, where `tx` is the horizontal translation and `ty` is the vertical translation. const matrix = transformStyle.slice('matrix('.length, -1).split(/,\s+/); return { translateX: matrix[4], translateY: matrix[5], }; } /** * Ensures that the given HTML element is positioned, setting its position to `relative` if it is not already positioned. * * Positioning is set using a constructable CSS stylesheet with a CSS layer. CSS layers have lower priority than "normal" * CSS declarations, and the layer name indicates the styling originates from `@scion/workbench`. * * This function adds an attribute to the element to locate it from the stylesheet, with the name containing a random identifier plus the context. * * This function must be passed an injector or called in an injection context. Destroying the injector will remove the attribute and the stylesheet. */ function positionElement(elementLike, options) { const injector = options.injector ?? inject(Injector); const element = coerceElement(elementLike); const document = injector.get(DOCUMENT); const disposables = new Array(); // Generate identifiers to locate the element from the stylesheet. const elementIdentifier = `data-wb-${options.context}-${UID.randomUID()}`; const elementIdentifierImportant = `${elementIdentifier}-important`; // Add stylesheet to change the element's position to relative. const styleSheet = new CSSStyleSheet({}); styleSheet.insertRule(` @layer sci-workbench { [${elementIdentifier}] { position: relative; } }`); document.adoptedStyleSheets.push(styleSheet); disposables.push(() => Arrays.remove(document.adoptedStyleSheets, styleSheet)); // Add attribute to locate the element from the stylesheet. element.setAttribute(elementIdentifier, ''); disposables.push(() => element.removeAttribute(elementIdentifier)); // If CSS layer styles have no effect due to 'static' positioning or unset styles, enforce positioning with !important. const animationFrame = requestAnimationFrame(() => { if (getComputedStyle(element).position === 'static') { styleSheet.insertRule(` [${elementIdentifierImportant}] { position: relative !important; } `); element.setAttribute(elementIdentifierImportant, ''); disposables.push(() => element.removeAttribute(elementIdentifierImportant)); } }); disposables.push(() => cancelAnimationFrame(animationFrame)); // Clean up when the injection context is destroyed. injector.get(DestroyRef).onDestroy(() => disposables.forEach(disposable => disposable())); } /** * Emits when the next animation frame is executed. * * Unlike using `timer(0, animationFrameScheduler)`, this observable emits within the zone where it is subscribed. * * Note that the RxJS `animationFrameScheduler` may not necessarily execute in the current execution context, such as inside or outside Angular. * The scheduler always executes tasks in the zone where it was first used in the application. */ function nextAnimationFrame$() { return new Observable(observer => { const animationFrame = requestAnimationFrame(() => observer.next()); return () => cancelAnimationFrame(animationFrame); }); } function constrainClientRect(clientRect, constraints) { if (!clientRect) { return undefined; } if (!constraints) { return clientRect; } const top = clamp(clientRect.top, { min: constraints.top, max: constraints.bottom }); const right = clamp(clientRect.right, { min: constraints.left, max: constraints.right }); const bottom = clamp(clientRect.bottom, { min: constraints.top, max: constraints.bottom }); const left = clamp(clientRect.left, { min: constraints.left, max: constraints.right }); const width = right - left; const height = bottom - top; return new DOMRect(left, top, width, height); } /* * 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 */ /** * Provides access to the workbench layout. */ class WorkbenchLayoutService { _layout = signal(null, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "_layout" }] : /* istanbul ignore next */ [])); _dragging = signal(false, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "_dragging" }] : /* istanbul ignore next */ [])); _moving = signal(false, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "_moving" }] : /* istanbul ignore next */ [])); _resizing = signal(false, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "_resizing" }] : /* istanbul ignore next */ [])); /** * Provides the layout of the workbench, throwing an error if the initial layout is not yet available. */ layout = computed(() => this._layout() ?? throwError('[NullLayoutError] Workbench layout not available yet.'), /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ [])); /** * Indicates whether the layout is available, i.e., after Angular has performed the initial navigation. */ hasLayout = computed(() => this._layout() !== null, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "hasLayout" }] : /* istanbul ignore next */ [])); /** * Resolves when the layout is available, i.e., after Angular has performed the initial navigation. */ whenLayoutAvailable = resolveWhen(this.hasLayout); /** * Indicates if a drag operation is active, such as moving a view or dialog, or resizing a part. */ dragging = computed(() => this._dragging() || this._moving() || this._resizing(), /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ [])); /** * Controls the alignment of the bottom activity panel. * * Defaults to the `--sci-workbench-layout-panel-align` design token, or `justify` if not set. */ panelAlignment = renderingFlag('scion.workbench.layout.panel.align', readCssVariable(inject(DOCUMENT).documentElement, '--sci-workbench-layout-panel-align', 'justify')); /** * Controls animation of activity panels. * * Defaults to the `--sci-workbench-layout-panel-animate` design token, or `true` if not set. */ panelAnimation = renderingFlag('scion.workbench.layout.panel.animate', readCssVariable(inject(DOCUMENT).documentElement, '--sci-workbench-layout-panel-animate', true)); /** * Signals dragging a workbench element, such as dragging a view tab. */ signalDragging(dragging) { this._dragging.set(dragging); } /** * Signals moving a workbench element, such as moving a dialog. */ signalMoving(moving) { this._moving.set(moving); } /** * Signals resizing a workbench element, such as resizing a dialog or part. */ signalResizing(resizing) { this._resizing.set(resizing); } /** * Sets the given {@link WorkbenchLayout}. */ setLayout(layout) { this._layout.set(layout); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: WorkbenchLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: WorkbenchLayoutService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: WorkbenchLayoutService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /* * 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 */ /** * Name of the query parameter that contains the layout of the main area. */ const MAIN_AREA_LAYOUT_QUERY_PARAM = 'main_area'; /** * DI token to inject the context in which the view tab is rendered. */ const VIEW_TAB_RENDERING_CONTEXT = new InjectionToken('VIEW_TAB_RENDERING_CONTEXT'); /** * Prefix used to identify an anonymous perspective that the workbench creates for views moved to a new window. */ const ANONYMOUS_PERSPECTIVE_ID_PREFIX = 'anonymous.'; /** * DI token for registering workbench-specific routes for workbench outlets. */ const WORKBENCH_ROUTE = new InjectionToken('WORKBENCH_ROUTE'); /* * 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 */ /** * Serializes the execution of elements emitted by the source Observable. * * For each element emitted by the source, the specified callback function is called. * The next element will only be processed once the Promise or Observable returned by * the callback function resolves or completes. */ function serializeExecution(fn) { return mergeMap(element => fn(element), 1); } /* * 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 */ /** * Serializes navigation requests to the Angular Router to prevent the cancellation of previously initiated asynchronous navigations. */ const ANGULAR_ROUTER_MUTEX = new InjectionToken('ANGULAR_ROUTER_MUTEX', { providedIn: 'root', factory: () => new SingleTaskExecutor(), }); /** * Allows the serial execution of tasks. * * At any one time, there is only one task executing. When submitting a task and if there is a task already executing, * the submitted task will be queued for later execution. * * This executor must be constructed within an injection context. Destroying the injection context will also destroy the executor. */ class SingleTaskExecutor { _task$ = new Subject(); constructor() { this._task$ .pipe( // Schedule the task asynchronously so that it is not executed if the executor is destroyed in the same "call stack", // happening, for example, when destroying the Testbed in unit tests. Angular destroys contexts from the bottom up, // i.e., child contexts are destroyed before parent contexts. If a task is scheduled in a destroy lifecycle hook of // a child context, the task would still be executed because the executor is not destroyed yet. observeOn(asapScheduler), serializeExecution(task => task.execute()), catchError((error, caught) => caught), takeUntilDestroyed()) .subscribe(); } /** * Submits a task for serial execution. * * Returns a Promise that resolves to the result of the passed task. */ submit(task) { const ɵtask = new Task(task); this._task$.next(ɵtask); return ɵtask.await(); } } class Task { _work; _done$ = new AsyncSubject(); constructor(_work) { this._work = _work; } async execute() { try { const result = await this._work(); this._done$.next(result); this._done$.complete(); } catch (error) { this._done$.error(error); } } await() { return lastValueFrom(this._done$); } } /* * 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 */ /** * Provides methods to associate {@link WorkbenchNavigationalState} with a navigation. */ const WorkbenchNavigationalStates = { /** * Returns workbench-specific state associated with given navigation, or `null` if the navigation was not performed through the workbench router. */ fromNavigation: (navigation) => { return navigation.extras.state?.[WORKBENCH_NAVIGATION_STATE_KEY] ?? null; }, /** * Creates a state object with workbench-specific data that can be passed to a workbench navigation. */ create: (state) => { return { [WORKBENCH_NAVIGATION_STATE_KEY]: state, }; }, }; /** * Key for associating workbench-specific state with a navigation. */ const WORKBENCH_NAVIGATION_STATE_KEY = 'ɵworkbench'; /* * 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 */ /** * DI token for injecting text providers to localize the SCION Workbench. * * Multiple text providers can be registered. Providers are called in registration order. * If a provider does not provide the text, the next provider is called, and so on. */ const WORKBENCH_TEXT_PROVIDER = new InjectionToken('WORKBENCH_TEXT_PROVIDER'); /* * 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 */ function text(translatable, options) { assertNotInReactiveContext(text, 'Call text() in a non-reactive (non-tracking) context, as it may allocate resources that are not released until the injection context is destroyed.'); const injector = options?.injector ?? inject(Injector); // Use a separate injection context per translatable to clean up allocated resources when it changes. let provideTextInjector; injector.get(DestroyRef).onDestroy(() => provideTextInjector?.destroy()); // Call the text provider function. const translation = computed(() => { const keyOrText = isSignal(translatable) ? translatable() : translatable; return untracked(() => { // Destroy previous injection context (if any) to clean up allocated resources, like RxJS subscriptions. provideTextInjector?.destroy(); // Create a separate injection context. provideTextInjector = createEnvironmentInjector([], injector.get(EnvironmentInjector)); // Call text provider. return runInInjectionContext(provideTextInjector, () => provideText(keyOrText)); }); }, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "translation" }] : /* istanbul ignore next */ [])); // Track translation in separate reactive context to not call the text provider function on translation change. return computed(() => translation()()); } /** * Provides the text for the given {@link Translatable} based on text providers registered under the {@link WORKBENCH_TEXT_PROVIDER} DI token. * * Multiple text providers can be registered. Providers are called in registration order. If a provider does not provide the text, the next provider is called, and so on. */ function provideText(translatable) { const textProviders = inject(WORKBENCH_TEXT_PROVIDER, { optional: true }); if (!textProviders?.length || !translatable?.startsWith('%') || translatable === '%') { return signal(translatable); } const { key, params } = parseTranslatable(translatable); const errorHandler = (error) => { // Prefix the key with an additional `%` character to escape the leading `%` character. See console formatting rules: https://developer.mozilla.org/en-US/docs/Web/API/console console.error(`[TextProviderError] Failed to get text for '%${translatable}'. Caused by:`, error); return translatable; }; for (const textProvider of textProviders) { // If `textProvider` throws an error, `runSafe` catches it and delegates execution to the error handler, logging the error and returning the translatable. const text = runSafe(() => textProvider(key, params), errorHandler); if (text !== undefined) { // If signal, wrap it in `computed` to handle errors. return isSignal(text) ? computed(() => runSafe(() => text(), errorHandler)) : signal(text); } } return signal(translatable); } /** * Parses a translation key into its key and parameters, if any. * * Examples: * - `%key`: translation key * - `%key;param=value`: translation key with a single interpolation parameter * - `%key;param1=value1;param2=value2`: translation key with multiple interpolation parameters */ function parseTranslatable(translationKey) { const { key, params } = /^%(?<key>[^;]+)(;(?<params>.*))?$/.exec(translationKey).groups; return { key: key, params: parseMatrixParams(params) }; } /** * Parses params in matrix notation. * * Format: `param1=value1;param2=value2` */ function parseMatrixParams(matrixParams) { if (!matrixParams?.length) { return {}; } const params = {}; for (const match of encodeEscapedSemicolons(matrixParams).matchAll(/(?<paramName>[^=;]+)=(?<paramValue>[^;]*)/g)) { const { paramName, paramValue } = match.groups; params[paramName] = decodeSemicolons(paramValue); } return params; /** * Encodes escaped semicolons (`\\;`) as `&#x3b` (Unicode) to prevent interpretation as interpolation parameter separators. */ function encodeEscapedSemicolons(value) { return value.replaceAll('\\;', '&#x3b'); } /** * Decodes encoded semicolons (`&#x3b`) back to semicolons (`;`). */ function decodeSemicolons(value) { return value.replaceAll('&#x3b', ';'); } } /** * Runs the passed function. * * If the function throws an error, catches the error and delegates execution to the passed error handler, returning the handler's result. */ function runSafe(fn, onErrorFn) { try { return fn(); } catch (error) { return onErrorFn(error); } } /* * 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 */ /** * Enables the translation of a given {@link Translatable}. * * A {@link Translatable} is a string that, if starting with the percent symbol (`%`), is passed to the text provider for translation, with the percent symbol omitted. * Otherwise, the text is returned as is. A translation key may include parameters in matrix notation for text interpolation. * * Examples: * - `%key`: translation key * - `%key;param=value`: translation key with a single interpolation parameter * - `%key;param1=value1;param2=value2`: translation key with multiple interpolation parameters * - `text`: no translation key, text is returned as is * * @experimental since 20.0.0-beta.3; API and behavior may change in any version without notice. */ class TextPipe { _translatable = signal(undefined, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "_translatable" }] : /* istanbul ignore next */ [])); _text = text(this._translatable); transform(translatable) { // DO NOT call text() on key change to avoid stale RxJS subscriptions of previous texts, // as allocated resources are only cleaned up when the injection context, such as this pipe, is destroyed. untracked(() => this._translatable.set(translatable)); return this._text; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: TextPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: TextPipe, isStandalone: true, name: "wbText" }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: TextPipe, decorators: [{ type: Pipe, args: [{ name: 'wbText' }] }] }); /* * Copyright (c) 2018-2025 Swiss Federal Railways * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ /** * A view is a visual workbench element for displaying content stacked or side-by-side in the workbench layout. * * Users can drag views from one part to another, even across windows, or place them side-by-side, horizontally and vertically. * * The view component can inject this handle to interact with the view. * * @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 */ /** * A part is a visual element of the workbench layout. Parts can be docked to the side or * positioned relative to each other. A part can display content or stack views. * * The part component can inject this handle to interact with the part. * * @see WorkbenchView */ class WorkbenchPart { } /* * 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 */ /** * Handle to interact with a dialog opened via {@link WorkbenchDialogService}. * * The dialog component can inject this handle to interact with the dialog. * * Dialog inputs are available as input properties in the dialog component. */ class WorkbenchDialog { } /* * Copyright (c) 2018-2025 Swiss Federal Railways * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ /** * A popup is a visual workbench element for displaying content above other content. The popup is positioned relative * to an anchor based on its preferred alignment. The anchor can be an element or a coordinate. * * The popup component can inject this handle to interact with the popup. * * Popup inputs are available as input properties in the popup component. * * @see WorkbenchPopupService */ class WorkbenchPopup { } /* * Copyright (c) 2018-2024 Swiss Federal Railways * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ class PageNotFoundComponent { isDevMode = isDevMode(); workbenchService = inject(WorkbenchService); view = inject(WorkbenchView, { optional: true }); part = inject(WorkbenchPart, { optional: true }); dialog = inject(WorkbenchDialog, { optional: true }); popup = inject(WorkbenchPopup, { optional: true }); path = computed(() => (this.view ?? this.part)?.navigation()?.path.map(segment => `${segment}`).join('/') ?? '', /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "path" }] : /* istanbul ignore next */ [])); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PageNotFoundComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: PageNotFoundComponent, isStandalone: true, selector: "wb-page-not-found", ngImport: i0, template: "<header>{{('%workbench.page_not_found.title' | wbText)()}}</header>\n\n@if (view) {\n @let message = '%workbench.page_not_found_view.message;path=' + path() | wbText;\n <section class=\"message\" [innerHTML]=\"message()\"></section>\n} @else if (part) {\n @let message = '%workbench.page_not_found_part.message;path=' + path() | wbText;\n <section class=\"message\" [innerHTML]=\"message()\"></section>\n} @else {\n @let message = '%workbench.page_not_found.message;path=' + path() | wbText;\n <section class=\"message\" [innerHTML]=\"message()\"></section>\n}\n\n<section class=\"actions\">\n @if (view) {\n <button (click)=\"workbenchService.resetPerspective()\">{{('%workbench.reset_perspective.action' | wbText)()}}</button>\n <button (click)=\"view.close()\">{{('%workbench.close.action' | wbText)()}}</button>\n } @else if (part) {\n <button (click)=\"workbenchService.resetPerspective()\">{{('%workbench.reset_perspective.action' | wbText)()}}</button>\n } @else if (dialog) {\n <button (click)=\"dialog.close()\">{{('%workbench.close.action' | wbText)()}}</button>\n } @else if (popup) {\n <button (click)=\"popup.close()\">{{('%workbench.close.action' | wbText)()}}</button>\n }\n</section>\n\n@if (isDevMode) {\n @let hint = '%workbench.page_not_found_developer_hint.message' | wbText;\n @let tooltip = '%workbench.dev_mode_only_hint.tooltip' | wbText;\n <section class=\"developer-hint\" [title]=\"tooltip()\">{{hint()}}</section>\n}\n", styles: [":host{display:flex;flex-direction:column;gap:2em;padding:3em;align-items:center}:host>header{font-weight:700;font-size:1.3rem}:host>section.message{text-align:center;line-height:1.75}:host>section.developer-hint{border:1px solid var(--sci-color-accent);border-radius:var(--sci-corner);padding:1em;max-width:550px;color:var(--sci-color-accent);font-family:monospace;text-align:center}:host>section.actions{display:flex;gap:.5em}:host>section.actions>button:is(button,#sci-reset){all:unset;cursor:var(--sci-workbench-button-cursor);padding:.5em 1.5em;color:var(--sci-color-accent-inverse);background-color:var(--sci-color-accent);background-clip:padding-box;border:1px solid var(--sci-color-accent);border-radius:var(--sci-corner);text-align:center}:host>section.actions>button:is(button,#sci-reset):focus,:host>section.actions>button:is(button,#sci-reset):active{border-color:transparent;outline:1px solid var(--sci-color-accent);color:var(--sci-color-accent-inverse)}\n"], dependencies: [{ kind: "pipe", type: TextPipe, name: "wbText" }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PageNotFoundComponent, decorators: [{ type: Component, args: [{ selector: 'wb-page-not-found', imports: [ TextPipe, ], template: "<header>{{('%workbench.page_not_found.title' | wbText)()}}</header>\n\n@if (view) {\n @let message = '%workbench.page_not_found_view.message;path=' + path() | wbText;\n <section class=\"message\" [innerHTML]=\"message()\"></section>\n} @else if (part) {\n @let message = '%workbench.page_not_found_part.message;path=' + path() | wbText;\n <section class=\"message\" [innerHTML]=\"message()\"></section>\n} @else {\n @let message = '%workbench.page_not_found.message;path=' + path() | wbText;\n <section class=\"message\" [innerHTML]=\"message()\"></section>\n}\n\n<section class=\"actions\">\n @if (view) {\n <button (click)=\"workbenchService.resetPerspective()\">{{('%workbench.reset_perspective.action' | wbText)()}}</button>\n <button (click)=\"view.close()\">{{('%workbench.close.action' | wbText)()}}</button>\n } @else if (part) {\n <button (click)=\"workbenchService.resetPerspective()\">{{('%workbench.reset_perspective.action' | wbText)()}}</button>\n } @else if (dialog) {\n <button (click)=\"dialog.close()\">{{('%workbench.close.action' | wbText)()}}</button>\n } @else if (popup) {\n <button (click)=\"popup.close()\">{{('%workbench.close.action' | wbText)()}}</button>\n }\n</section>\n\n@if (isDevMode) {\n @let hint = '%workbench.page_not_found_developer_hint.message' | wbText;\n @let tooltip = '%workbench.dev_mode_only_hint.tooltip' | wbText;\n <section class=\"developer-hint\" [title]=\"tooltip()\">{{hint()}}</section>\n}\n", styles: [":host{display:flex;flex-direction:column;gap:2em;padding:3em;align-items:center}:host>header{font-weight:700;font-size:1.3rem}:host>section.message{text-align:center;line-height:1.75}:host>section.developer-hint{border:1px solid var(--sci-color-accent);border-radius:var(--sci-corner);padding:1em;max-width:550px;color:var(--sci-color-accent);font-family:monospace;text-align:center}:host>section.actions{display:flex;gap:.5em}:host>section.actions>button:is(button,#sci-reset){all:unset;cursor:var(--sci-workbench-button-cursor);padding:.5em 1.5em;color:var(--sci-color-accent-inverse);background-color:var(--sci-color-accent);background-clip:padding-box;border:1px solid var(--sci-color-accent);border-radius:var(--sci-corner);text-align:center}:host>section.actions>button:is(button,#sci-reset):focus,:host>section.actions>button:is(button,#sci-reset):active{border-color:transparent;outline:1px solid var(--sci-color-accent);color:var(--sci-color-accent-inverse)}\n"] }] }] }); /* * 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 */ /** * Keys for associating workbench-specific data with a route in {@link Route.data}. * * @see Route.data */ const WorkbenchRouteData = { /** * Property to set the title of a view in {@link Route.data}. * * Use a {@link Translatable} to localize the title. */ title: 'ɵworkbenchViewTitle', /** * Property to set the heading of a view in {@link Route.data}. * * Use a {@link Translatable} to localize the heading. */ heading: 'ɵworkbenchViewHeading', /** * Property to associate CSS class(es) with a workbench element in {@link Route.data}, e.g., to locate it in tests. */ cssClass: 'ɵworkbenchCssClass', }; /* * 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 */ /** * Angular standardizes component-less auxiliary routes by adding the `ɵEmptyOutletComponent` component, * but only for routes registered via {@link ROUTES} DI token or passed to {@link Router#resetConfig}. * * Consequently, auxiliary routes that the workbench dynamically registers based on the current workbench * state must also be standardized. However, we do not use Angular's {@link ɵEmptyOutletComponent} component * as it does not fill the content to the available space, required for view content. * * For more information, see the `standardizeConfig` function in Angular. */ class ɵEmptyOutletComponent { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ɵEmptyOutletComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.1", type: ɵEmptyOutletComponent, isStandalone: true, selector: "ng-component", ngImport: i0, template: "<router-outlet/>\n", styles: [":host{display:grid}:host>router-outlet{position:absolute}\n"], dependencies: [{ kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: ɵEmptyOutletComponent, decorators: [{ type: Component, args: [{ changeDetection: ChangeDetectionStrategy.Eager, imports: [RouterOutlet], template: "<router-outlet/>\n", styles: [":host{display:grid}:host>router-outlet{position:absolute}\n"] }] }] }); /* * 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 */ /** * Indicates that there is no content to display. */ class NullContentComponent { isDevMode = isDevMode(); view = inject(WorkbenchView, { optional: true }); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: NullContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.1", type: NullContentComponent, isStandalone: true, selector: "wb-null-content", ngImport: i0, template: "@if (isDevMode && view) {\n @let hint = '%workbench.null_view_developer_hint.message;view=' + (view.alternativeId ?? view.id) | wbText;\n @let tooltip = '%workbench.dev_mode_only_hint.tooltip' | wbText;\n <section class=\"developer-hint\" [title]=\"tooltip()\" [innerHTML]=\"hint()\"></section>\n} @else {\n {{('%workbench.null_content.message' | wbText)()}}\n}\n", styles: [":host{display:flex;flex-direction:column;gap:2em;padding:3em;align-items:center;color:var(--sci-color-text-subtlest);-webkit-user-select:none;user-select:none}:host>section.developer-hint{border:1px solid var(--sci-color-accent);border-radius:var(--sci-corner);padding:1em;max-width:550px;color:var(--sci-color-accent);font-family:monospace;text-align:center;-webkit-user-select:text;user-select:text}\n"], dependencies: [{ kind: "pipe", type: TextPipe, name: "wbText" }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: NullContentComponent, decorators: [{ type: Component, args: [{ selector: 'wb-null-content', imports: [ TextPipe, ], template: "@if (isDevMode && view) {\n @let hint = '%workbench.null_view_developer_hint.message;view=' + (view.alternativeId ?? view.id) | wbText;\n @let tooltip = '%workbench.dev_mode_only_hint.tooltip' | wbText;\n <section class=\"developer-hint\" [title]=\"tooltip()\" [innerHTML]=\"hint()\"></section>\n} @else {\n {{('%workbench.null_content.message' | wbText)()}}\n}\n", styles: [":host{display:flex;flex-direction:column;gap:2em;padding:3em;align-items:center;color:var(--sci-color-text-subtlest);-webkit-user-select:none;user-select:none}:host>section.developer-hint{border:1px solid var(--sci-color-accent);border-radius:var(--sci-corner);padding:1em;max-width:550px;color:var(--sci-color-accent);font-family:monospace;text-align:center;-webkit-user-select:text;user-select:text}\n"] }] }] }); /* * 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 */ function canMatchWorkbenchView(condition) { return () => { const outlet = inject(WORKBENCH_OUTLET, { optional: true }); switch (condition) { case true: return isViewOutlet(outlet); case false: return !isViewOutlet(outlet); default: { // hint if (!isViewOutlet(outlet)) { return false; } const layout = inject(ɵWorkbenchRouter).getCurrentNavigationContext().layout; const view = layout.view({ viewId: outlet }, { orElse: null }); return view?.navigation?.hint === condition; } } }; } function canMatchWorkbenchPart(condition) { return () => {