@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
JavaScript
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 `;` (Unicode) to prevent interpretation as interpolation parameter separators.
*/
function encodeEscapedSemicolons(value) {
return value.replaceAll('\\;', ';');
}
/**
* Decodes encoded semicolons (`;`) back to semicolons (`;`).
*/
function decodeSemicolons(value) {
return value.replaceAll(';', ';');
}
}
/**
* 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 () => {