diff --git a/README.md b/README.md index 4cfe4ac56..52fbc74fa 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,12 @@ SCION Workbench enables the creation of Angular web applications that require a - [**Getting Started**][link-getting-started]\ Follow these steps to install the SCION Workbench in your project and start with a basic introduction to the SCION Workbench. -#### Workbench Demo Applications +#### Workbench Sample Applications -- [**SCION Workbench Testing App**][link-testing-app]\ - Visit our technical testing application to explore the workbench and experiment with its features. +- [**Playground Application**][link-playground-app]\ + Visit our playground application to explore the workbench and experiment with its features. -- [**SCION Workbench Getting Started App**][link-getting-started-app]\ +- [**Getting Started Application**][link-getting-started-app]\ Open the application developed in the [Getting Started][link-getting-started] guide. #### Documentation @@ -72,7 +72,7 @@ SCION Workbench enables the creation of Angular web applications that require a [link-getting-started]: /docs/site/getting-started.md [link-howto]: /docs/site/howto/how-to.md [link-demo-app]: https://schweizerischebundesbahnen.github.io/scion-workbench-demo/#/(view.24:person/64//view.22:person/32//view.5:person/79//view.3:person/15//view.2:person/38//view.1:person/66//activity:person-list)?viewgrid=eyJpZCI6MSwic2FzaDEiOlsidmlld3BhcnQuMSIsInZpZXcuMSIsInZpZXcuMiIsInZpZXcuMSJdLCJzYXNoMiI6eyJpZCI6Miwic2FzaDEiOlsidmlld3BhcnQuMiIsInZpZXcuMyIsInZpZXcuMyJdLCJzYXNoMiI6eyJpZCI6Mywic2FzaDEiOlsidmlld3BhcnQuNCIsInZpZXcuMjQiLCJ2aWV3LjI0Il0sInNhc2gyIjpbInZpZXdwYXJ0LjMiLCJ2aWV3LjIyIiwidmlldy41Iiwidmlldy4yMiJdLCJzcGxpdHRlciI6MC41MTk0Mzg0NDQ5MjQ0MDY2LCJoc3BsaXQiOmZhbHNlfSwic3BsaXR0ZXIiOjAuNTU5NDI0MzI2ODMzNzk3NSwiaHNwbGl0Ijp0cnVlfSwic3BsaXR0ZXIiOjAuMzIyNjI3NzM3MjI2Mjc3MywiaHNwbGl0IjpmYWxzZX0%3D -[link-testing-app]: https://scion-workbench-testing-app.vercel.app +[link-playground-app]: https://scion-workbench-testing-app.vercel.app [link-getting-started-app]: https://scion-workbench-getting-started.vercel.app [link-features]: /docs/site/features.md [link-announcements]: /docs/site/announcements.md diff --git a/apps/workbench-client-testing-app/src/app/app.component.html b/apps/workbench-client-testing-app/src/app/app.component.html index 687ba960f..1fb07fe1d 100644 --- a/apps/workbench-client-testing-app/src/app/app.component.html +++ b/apps/workbench-client-testing-app/src/app/app.component.html @@ -1,6 +1,6 @@ - +
has-focus diff --git a/apps/workbench-client-testing-app/src/app/css-class/css-class.component.html b/apps/workbench-client-testing-app/src/app/css-class/css-class.component.html new file mode 100644 index 000000000..1579b5c12 --- /dev/null +++ b/apps/workbench-client-testing-app/src/app/css-class/css-class.component.html @@ -0,0 +1 @@ + diff --git a/apps/workbench-client-testing-app/src/app/css-class/css-class.component.scss b/apps/workbench-client-testing-app/src/app/css-class/css-class.component.scss new file mode 100644 index 000000000..85980ba98 --- /dev/null +++ b/apps/workbench-client-testing-app/src/app/css-class/css-class.component.scss @@ -0,0 +1,10 @@ +@use '@scion/components.internal/design' as sci-design; + +:host { + display: inline-grid; + + > input { + @include sci-design.style-input-field(); + min-width: 0; + } +} diff --git a/apps/workbench-client-testing-app/src/app/css-class/css-class.component.ts b/apps/workbench-client-testing-app/src/app/css-class/css-class.component.ts new file mode 100644 index 000000000..2156983d7 --- /dev/null +++ b/apps/workbench-client-testing-app/src/app/css-class/css-class.component.ts @@ -0,0 +1,84 @@ +/* + * 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 + */ + +import {Component, forwardRef} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR, NonNullableFormBuilder, ReactiveFormsModule} from '@angular/forms'; +import {noop} from 'rxjs'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {Arrays} from '@scion/toolkit/util'; + +@Component({ + selector: 'app-css-class', + templateUrl: './css-class.component.html', + styleUrls: ['./css-class.component.scss'], + standalone: true, + imports: [ + ReactiveFormsModule, + ], + providers: [ + {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => CssClassComponent)}, + ], +}) +export class CssClassComponent implements ControlValueAccessor { + + private _cvaChangeFn: (cssClasses: string | string[] | undefined) => void = noop; + private _cvaTouchedFn: () => void = noop; + + protected formControl = this._formBuilder.control(''); + + constructor(private _formBuilder: NonNullableFormBuilder) { + this.formControl.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(() => { + this._cvaChangeFn(this.parse(this.formControl.value)); + this._cvaTouchedFn(); + }); + } + + private parse(stringified: string): string[] | string | undefined { + const cssClasses = stringified.split(/\s+/).filter(Boolean); + switch (cssClasses.length) { + case 0: + return undefined; + case 1: + return cssClasses[0]; + default: + return cssClasses; + } + } + + private stringify(cssClasses: string | string[] | undefined | null): string { + return Arrays.coerce(cssClasses).join(' '); + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public writeValue(cssClasses: string | string[] | undefined | null): void { + this.formControl.setValue(this.stringify(cssClasses), {emitEvent: false}); + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public registerOnChange(fn: any): void { + this._cvaChangeFn = fn; + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public registerOnTouched(fn: any): void { + this._cvaTouchedFn = fn; + } +} diff --git a/apps/workbench-client-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.html b/apps/workbench-client-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.html index b673cc6f9..d3333418e 100644 --- a/apps/workbench-client-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.html +++ b/apps/workbench-client-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.html @@ -27,7 +27,7 @@ - +
diff --git a/apps/workbench-client-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.ts b/apps/workbench-client-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.ts index c07f99fae..b8d310759 100644 --- a/apps/workbench-client-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.ts +++ b/apps/workbench-client-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.ts @@ -10,13 +10,14 @@ import {Component} from '@angular/core'; import {FormGroup, NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; -import {WorkbenchDialogService, WorkbenchView} from '@scion/workbench-client'; +import {WorkbenchDialogService, WorkbenchView, ViewId} from '@scion/workbench-client'; import {stringifyError} from '../common/stringify-error.util'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {KeyValueEntry, SciKeyValueFieldComponent} from '@scion/components.internal/key-value-field'; import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; import {startWith} from 'rxjs/operators'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {CssClassComponent} from '../css-class/css-class.component'; @Component({ selector: 'app-dialog-opener-page', @@ -28,6 +29,7 @@ import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; SciFormFieldComponent, SciKeyValueFieldComponent, SciCheckboxComponent, + CssClassComponent, ], }) export default class DialogOpenerPageComponent { @@ -46,9 +48,9 @@ export default class DialogOpenerPageComponent { options: this._formBuilder.group({ params: this._formBuilder.array>([]), modality: this._formBuilder.control<'application' | 'view' | ''>(''), - contextualViewId: this._formBuilder.control(''), + contextualViewId: this._formBuilder.control(''), animate: this._formBuilder.control(undefined), - cssClass: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), }), }); @@ -76,7 +78,7 @@ export default class DialogOpenerPageComponent { context: { viewId: this.form.controls.options.controls.contextualViewId.value || undefined, }, - cssClass: this.form.controls.options.controls.cssClass.value.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.options.controls.cssClass.value, }) .then(result => this.returnValue = result) .catch(error => this.dialogError = stringifyError(error) || 'Dialog was closed with an error'); diff --git a/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html b/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html index 1eda144db..7c72c3ca4 100644 --- a/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html +++ b/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html @@ -44,7 +44,7 @@ - + diff --git a/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.ts b/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.ts index 36dbd6f54..cfb84019c 100644 --- a/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.ts +++ b/apps/workbench-client-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.ts @@ -17,6 +17,7 @@ import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {NgIf} from '@angular/common'; import {stringifyError} from '../common/stringify-error.util'; import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; +import {CssClassComponent} from '../css-class/css-class.component'; @Component({ selector: 'app-message-box-opener-page', @@ -29,6 +30,7 @@ import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; SciFormFieldComponent, SciKeyValueFieldComponent, SciCheckboxComponent, + CssClassComponent, ], }) export default class MessageBoxOpenerPageComponent { @@ -42,7 +44,7 @@ export default class MessageBoxOpenerPageComponent { severity: this._formBuilder.control<'info' | 'warn' | 'error' | ''>(''), modality: this._formBuilder.control<'application' | 'view' | ''>(''), contentSelectable: this._formBuilder.control(true), - cssClass: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), viewContext: this._formBuilder.control(true), }); @@ -76,7 +78,7 @@ export default class MessageBoxOpenerPageComponent { severity: this.form.controls.severity.value || undefined, modality: this.form.controls.modality.value || undefined, contentSelectable: this.form.controls.contentSelectable.value || undefined, - cssClass: this.form.controls.cssClass.value.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.cssClass.value, }, qualifier ?? undefined) .then(closeAction => this.closeAction = closeAction) .catch(error => this.openError = stringifyError(error)); diff --git a/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.html b/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.html index bddf49792..b9cb41181 100644 --- a/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.html +++ b/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.html @@ -28,8 +28,8 @@ - - + + @@ -43,7 +43,7 @@ - + diff --git a/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.ts b/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.ts index e451c5bbd..5c518810f 100644 --- a/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.ts +++ b/apps/workbench-client-testing-app/src/app/notification-opener-page/notification-opener-page.component.ts @@ -15,6 +15,8 @@ import {NgIf} from '@angular/common'; import {stringifyError} from '../common/stringify-error.util'; import {KeyValueEntry, SciKeyValueFieldComponent} from '@scion/components.internal/key-value-field'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; +import {CssClassComponent} from '../css-class/css-class.component'; +import {UUID} from '@scion/toolkit/uuid'; @Component({ selector: 'app-notification-opener-page', @@ -26,6 +28,7 @@ import {SciFormFieldComponent} from '@scion/components.internal/form-field'; ReactiveFormsModule, SciFormFieldComponent, SciKeyValueFieldComponent, + CssClassComponent, ], }) export default class NotificationOpenerPageComponent { @@ -38,11 +41,13 @@ export default class NotificationOpenerPageComponent { severity: this._formBuilder.control<'info' | 'warn' | 'error' | ''>(''), duration: this._formBuilder.control<'short' | 'medium' | 'long' | 'infinite' | number | ''>(''), group: this._formBuilder.control(''), - cssClass: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), }); public notificationOpenError: string | undefined; + public durationList = `duration-list-${UUID.randomUUID()}`; + constructor(view: WorkbenchView, private _formBuilder: NonNullableFormBuilder, private _notificationService: WorkbenchNotificationService) { @@ -61,7 +66,7 @@ export default class NotificationOpenerPageComponent { severity: this.form.controls.severity.value || undefined, duration: this.parseDurationFromUI(), group: this.form.controls.group.value || undefined, - cssClass: this.form.controls.cssClass.value.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.cssClass.value, }, qualifier ?? undefined) .catch(error => this.notificationOpenError = stringifyError(error) || 'Workbench Notification could not be opened'); } diff --git a/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.html b/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.html index afbab410c..a59132970 100644 --- a/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.html +++ b/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.html @@ -23,7 +23,7 @@ - + diff --git a/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.ts b/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.ts index 331a95818..1d4a57639 100644 --- a/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.ts +++ b/apps/workbench-client-testing-app/src/app/popup-opener-page/popup-opener-page.component.ts @@ -10,7 +10,7 @@ import {Component, ElementRef, ViewChild} from '@angular/core'; import {FormGroup, NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; -import {CloseStrategy, PopupOrigin, WorkbenchPopupService, WorkbenchView} from '@scion/workbench-client'; +import {CloseStrategy, PopupOrigin, ViewId, WorkbenchPopupService, WorkbenchView} from '@scion/workbench-client'; import {Observable} from 'rxjs'; import {map, startWith} from 'rxjs/operators'; import {undefinedIfEmpty} from '../common/undefined-if-empty.util'; @@ -22,6 +22,7 @@ import {KeyValueEntry, SciKeyValueFieldComponent} from '@scion/components.intern import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; import {SciAccordionComponent, SciAccordionItemDirective} from '@scion/components.internal/accordion'; import {parseTypedString} from '../common/parse-typed-value.util'; +import {CssClassComponent} from '../css-class/css-class.component'; @Component({ selector: 'app-popup-opener-page', @@ -37,6 +38,7 @@ import {parseTypedString} from '../common/parse-typed-value.util'; SciAccordionItemDirective, SciCheckboxComponent, PopupPositionLabelPipe, + CssClassComponent, ], }) export default class PopupOpenerPageComponent { @@ -62,9 +64,9 @@ export default class PopupOpenerPageComponent { width: this._formBuilder.control(undefined), height: this._formBuilder.control(undefined), }), - contextualViewId: this._formBuilder.control(''), + contextualViewId: this._formBuilder.control(''), align: this._formBuilder.control<'east' | 'west' | 'north' | 'south' | ''>(''), - cssClass: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), closeStrategy: this._formBuilder.group({ onFocusLost: this._formBuilder.control(true), onEscape: this._formBuilder.control(true), @@ -99,7 +101,7 @@ export default class PopupOpenerPageComponent { onFocusLost: this.form.controls.closeStrategy.controls.onFocusLost.value ?? undefined, onEscape: this.form.controls.closeStrategy.controls.onEscape.value ?? undefined, }), - cssClass: this.form.controls.cssClass.value.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.cssClass.value, context: { viewId: parseTypedString(this.form.controls.contextualViewId.value || undefined), }, diff --git a/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.html b/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.html index c46d17b03..cf1d28cf0 100644 --- a/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.html +++ b/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.html @@ -33,131 +33,131 @@ @if (form.controls.type.value === WorkbenchCapabilities.View) { - + - + - + - + - + - + - + } @if (form.controls.type.value === WorkbenchCapabilities.Popup) { - + - + - + - + - + - + - + - + - + - + } @if (form.controls.type.value === WorkbenchCapabilities.Dialog) { - + - + - + - + - + - + - + - + - + - + - + - + - + - + } diff --git a/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.ts b/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.ts index 19ee03407..64d578b9a 100644 --- a/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.ts +++ b/apps/workbench-client-testing-app/src/app/register-workbench-capability-page/register-workbench-capability-page.component.ts @@ -21,6 +21,7 @@ import {stringifyError} from '../common/stringify-error.util'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; import {parseTypedString} from '../common/parse-typed-value.util'; +import {CssClassComponent} from '../css-class/css-class.component'; /** * Allows registering workbench capabilities. @@ -37,6 +38,7 @@ import {parseTypedString} from '../common/parse-typed-value.util'; SciKeyValueFieldComponent, SciCheckboxComponent, SciViewportComponent, + CssClassComponent, ], }) export default class RegisterWorkbenchCapabilityPageComponent { @@ -54,7 +56,7 @@ export default class RegisterWorkbenchCapabilityPageComponent { heading: this._formBuilder.control(''), closable: this._formBuilder.control(null), showSplash: this._formBuilder.control(null), - cssClass: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), pinToStartPage: this._formBuilder.control(false), }), popupProperties: this._formBuilder.group({ @@ -69,7 +71,7 @@ export default class RegisterWorkbenchCapabilityPageComponent { }), showSplash: this._formBuilder.control(null), pinToStartPage: this._formBuilder.control(false), - cssClass: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), }), dialogProperties: this._formBuilder.group({ path: this._formBuilder.control(''), @@ -87,7 +89,7 @@ export default class RegisterWorkbenchCapabilityPageComponent { padding: this._formBuilder.control(null), showSplash: this._formBuilder.control(null), pinToStartPage: this._formBuilder.control(false), - cssClass: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), }), }); @@ -111,7 +113,7 @@ export default class RegisterWorkbenchCapabilityPageComponent { case WorkbenchCapabilities.Dialog: return this.readDialogCapabilityFromUI(); default: - throw Error('[IllegalArgumentError] Capability expected to be a workbench capability, but was not.'); + throw Error('Capability expected to be a workbench capability, but was not.'); } })(); @@ -144,7 +146,7 @@ export default class RegisterWorkbenchCapabilityPageComponent { path: parseTypedString(this.form.controls.viewProperties.controls.path.value), // allow `undefined` to test capability validation title: this.form.controls.viewProperties.controls.title.value || undefined, heading: this.form.controls.viewProperties.controls.heading.value || undefined, - cssClass: this.form.controls.viewProperties.controls.cssClass.value.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.viewProperties.controls.cssClass.value, closable: this.form.controls.viewProperties.controls.closable.value ?? undefined, showSplash: this.form.controls.viewProperties.controls.showSplash.value ?? undefined, pinToStartPage: this.form.controls.viewProperties.controls.pinToStartPage.value, @@ -175,7 +177,7 @@ export default class RegisterWorkbenchCapabilityPageComponent { }), showSplash: this.form.controls.popupProperties.controls.showSplash.value ?? undefined, pinToStartPage: this.form.controls.popupProperties.controls.pinToStartPage.value, - cssClass: this.form.controls.popupProperties.controls.cssClass.value.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.popupProperties.controls.cssClass.value, }, }; } @@ -207,7 +209,7 @@ export default class RegisterWorkbenchCapabilityPageComponent { padding: this.form.controls.dialogProperties.controls.padding.value ?? undefined, showSplash: this.form.controls.dialogProperties.controls.showSplash.value ?? undefined, pinToStartPage: this.form.controls.dialogProperties.controls.pinToStartPage.value, - cssClass: this.form.controls.dialogProperties.controls.cssClass.value.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.dialogProperties.controls.cssClass.value, }, }; } diff --git a/apps/workbench-client-testing-app/src/app/router-page/router-page.component.html b/apps/workbench-client-testing-app/src/app/router-page/router-page.component.html index e9562eb6d..94c2034d6 100644 --- a/apps/workbench-client-testing-app/src/app/router-page/router-page.component.html +++ b/apps/workbench-client-testing-app/src/app/router-page/router-page.component.html @@ -15,18 +15,20 @@
Navigation Extras
- - + + - - - + + + @@ -39,7 +41,7 @@ - + diff --git a/apps/workbench-client-testing-app/src/app/router-page/router-page.component.ts b/apps/workbench-client-testing-app/src/app/router-page/router-page.component.ts index 33d5b7c0f..67411e5fd 100644 --- a/apps/workbench-client-testing-app/src/app/router-page/router-page.component.ts +++ b/apps/workbench-client-testing-app/src/app/router-page/router-page.component.ts @@ -17,6 +17,8 @@ import {NgIf} from '@angular/common'; import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {parseTypedObject} from '../common/parse-typed-value.util'; +import {CssClassComponent} from '../css-class/css-class.component'; +import {UUID} from '@scion/toolkit/uuid'; @Component({ selector: 'app-router-page', @@ -29,6 +31,7 @@ import {parseTypedObject} from '../common/parse-typed-value.util'; SciFormFieldComponent, SciKeyValueFieldComponent, SciCheckboxComponent, + CssClassComponent, ], }) export default class RouterPageComponent { @@ -37,13 +40,16 @@ export default class RouterPageComponent { qualifier: this._formBuilder.array>([], Validators.required), params: this._formBuilder.array>([]), target: this._formBuilder.control(''), - insertionIndex: this._formBuilder.control(''), + position: this._formBuilder.control(''), activate: this._formBuilder.control(undefined), close: this._formBuilder.control(undefined), - cssClass: this._formBuilder.control(undefined), + cssClass: this._formBuilder.control(undefined), }); public navigateError: string | undefined; + public targetList = `target-list-${UUID.randomUUID()}`; + public positionList = `position-list-${UUID.randomUUID()}`; + constructor(view: WorkbenchView, private _formBuilder: NonNullableFormBuilder, private _router: WorkbenchRouter) { @@ -60,9 +66,9 @@ export default class RouterPageComponent { activate: this.form.controls.activate.value, close: this.form.controls.close.value, target: this.form.controls.target.value || undefined, - blankInsertionIndex: coerceInsertionIndex(this.form.controls.insertionIndex.value), + position: coercePosition(this.form.controls.position.value), params: params || undefined, - cssClass: this.form.controls.cssClass.value?.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.cssClass.value, }; await this._router.navigate(qualifier, extras) .then(success => success ? Promise.resolve() : Promise.reject('Navigation failed')) @@ -70,11 +76,11 @@ export default class RouterPageComponent { } } -function coerceInsertionIndex(value: any): number | 'start' | 'end' | undefined { +function coercePosition(value: any): number | 'start' | 'end' | undefined { if (value === '') { return undefined; } - if (value === 'start' || value === 'end' || value === undefined) { + if (value === 'start' || value === 'end' || value === 'before-active-view' || value === 'after-active-view' || value === undefined) { return value; } return coerceNumberProperty(value); diff --git a/apps/workbench-client-testing-app/src/app/test-pages/angular-zone-test-page/angular-zone-test-page.component.ts b/apps/workbench-client-testing-app/src/app/test-pages/angular-zone-test-page/angular-zone-test-page.component.ts index a5336a784..0ab3bae84 100644 --- a/apps/workbench-client-testing-app/src/app/test-pages/angular-zone-test-page/angular-zone-test-page.component.ts +++ b/apps/workbench-client-testing-app/src/app/test-pages/angular-zone-test-page/angular-zone-test-page.component.ts @@ -45,7 +45,7 @@ export default class AngularZoneTestPageComponent { } private async testWorkbenchViewCapability(model: TestCaseModel): Promise { - const workbenchViewTestee = this._zone.runOutsideAngular(() => new ɵWorkbenchView('VIEW_ID')); + const workbenchViewTestee = this._zone.runOutsideAngular(() => new ɵWorkbenchView('view.999')); // Register two view capabilities const viewCapabilityId1 = await Beans.get(ManifestService).registerCapability({ @@ -79,7 +79,7 @@ export default class AngularZoneTestPageComponent { } private async testWorkbenchViewParams(model: TestCaseModel): Promise { - const workbenchViewTestee = this._zone.runOutsideAngular(() => new ɵWorkbenchView('VIEW_ID')); + const workbenchViewTestee = this._zone.runOutsideAngular(() => new ɵWorkbenchView('view.999')); // Subscribe to params workbenchViewTestee.params$ @@ -97,7 +97,7 @@ export default class AngularZoneTestPageComponent { } private async testWorkbenchViewActive(model: TestCaseModel): Promise { - const workbenchViewTestee = this._zone.runOutsideAngular(() => new ɵWorkbenchView('VIEW_ID')); + const workbenchViewTestee = this._zone.runOutsideAngular(() => new ɵWorkbenchView('view.999')); // Subscribe to active state workbenchViewTestee.active$ diff --git a/apps/workbench-client-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.html b/apps/workbench-client-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.html index 2958909b1..dac957f23 100644 --- a/apps/workbench-client-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.html +++ b/apps/workbench-client-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.html @@ -3,8 +3,8 @@ - - + + diff --git a/apps/workbench-client-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.ts b/apps/workbench-client-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.ts index 9f66f70ea..7a9e3c375 100644 --- a/apps/workbench-client-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.ts +++ b/apps/workbench-client-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.ts @@ -13,6 +13,7 @@ import {WorkbenchRouter} from '@scion/workbench-client'; import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; import {APP_IDENTITY} from '@scion/microfrontend-platform'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; +import {CssClassComponent} from '../../css-class/css-class.component'; @Component({ selector: 'app-bulk-navigation-test-page', @@ -22,13 +23,14 @@ import {SciFormFieldComponent} from '@scion/components.internal/form-field'; imports: [ SciFormFieldComponent, ReactiveFormsModule, + CssClassComponent, ], }) export default class BulkNavigationTestPageComponent { public form = this._formBuilder.group({ viewCount: this._formBuilder.control(1, Validators.required), - cssClass: this._formBuilder.control('', Validators.required), + cssClass: this._formBuilder.control(undefined, Validators.required), }); constructor(private _formBuilder: NonNullableFormBuilder, diff --git a/apps/workbench-client-testing-app/src/app/view-page/view-page.component.ts b/apps/workbench-client-testing-app/src/app/view-page/view-page.component.ts index a9fd574ff..b254e2d09 100644 --- a/apps/workbench-client-testing-app/src/app/view-page/view-page.component.ts +++ b/apps/workbench-client-testing-app/src/app/view-page/view-page.component.ts @@ -10,7 +10,7 @@ import {Component, Inject, OnDestroy} from '@angular/core'; import {FormGroup, NonNullableFormBuilder, ReactiveFormsModule} from '@angular/forms'; -import {ViewClosingEvent, ViewClosingListener, WorkbenchMessageBoxService, WorkbenchRouter, WorkbenchView} from '@scion/workbench-client'; +import {CanClose, WorkbenchMessageBoxService, WorkbenchRouter, WorkbenchView} from '@scion/workbench-client'; import {ActivatedRoute} from '@angular/router'; import {UUID} from '@scion/toolkit/uuid'; import {MonoTypeOperatorFunction, NEVER} from 'rxjs'; @@ -49,7 +49,7 @@ import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; SciViewportComponent, ], }) -export default class ViewPageComponent implements ViewClosingListener, OnDestroy { +export default class ViewPageComponent implements CanClose, OnDestroy { public form = this._formBuilder.group({ title: this._formBuilder.control(''), @@ -92,25 +92,22 @@ export default class ViewPageComponent implements ViewClosingListener, OnDestroy }); } - public async onClosing(event: ViewClosingEvent): Promise { + public async canClose(): Promise { if (!this.form.controls.confirmClosing.value) { - return; + return true; } const action = await this._messageBoxService.open({ content: 'Do you want to close this view?', - severity: 'info', - actions: { - yes: 'Yes', - no: 'No', - }, + actions: {yes: 'Yes', no: 'No', error: 'Throw Error'}, cssClass: ['e2e-close-view', this.view.id], - modality: 'application', // message box is displayed even if closing view is not active + modality: 'application', }); - if (action === 'no') { - event.preventDefault(); + if (action === 'error') { + throw Error(`[CanCloseSpecError] Error in CanLoad of view '${this.view.id}'.`); } + return action === 'yes'; } public onMarkDirty(dirty?: boolean): void { @@ -145,10 +142,10 @@ export default class ViewPageComponent implements ViewClosingListener, OnDestroy ) .subscribe(confirmClosing => { if (confirmClosing) { - this.view.addClosingListener(this); + this.view.addCanClose(this); } else { - this.view.removeClosingListener(this); + this.view.removeCanClose(this); } }); } @@ -198,6 +195,6 @@ export default class ViewPageComponent implements ViewClosingListener, OnDestroy } public ngOnDestroy(): void { - this.view.removeClosingListener(this); + this.view.removeCanClose(this); } } diff --git a/apps/workbench-getting-started-app/src/app/app.config.ts b/apps/workbench-getting-started-app/src/app/app.config.ts index 649785ebe..e92a5e33c 100644 --- a/apps/workbench-getting-started-app/src/app/app.config.ts +++ b/apps/workbench-getting-started-app/src/app/app.config.ts @@ -8,9 +8,8 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {provideWorkbench} from './workbench.provider'; import {ApplicationConfig} from '@angular/core'; -import {MAIN_AREA, WorkbenchLayoutFactory} from '@scion/workbench'; +import {MAIN_AREA, provideWorkbench, WorkbenchLayoutFactory} from '@scion/workbench'; import {provideRouter, withHashLocation} from '@angular/router'; import {provideAnimations} from '@angular/platform-browser/animations'; @@ -23,11 +22,12 @@ export const appConfig: ApplicationConfig = { layout: (factory: WorkbenchLayoutFactory) => factory .addPart(MAIN_AREA) .addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) - .addView('todos', {partId: 'left', activateView: true}), + .addView('todos', {partId: 'left', activateView: true}) + .navigateView('todos', ['todos']), }), provideRouter([ {path: '', loadComponent: () => import('./welcome/welcome.component')}, - {path: '', outlet: 'todos', loadComponent: () => import('./todos/todos.component')}, + {path: 'todos', loadComponent: () => import('./todos/todos.component')}, {path: 'todos/:id', loadComponent: () => import('./todo/todo.component')}, ], withHashLocation()), provideAnimations(), diff --git a/apps/workbench-getting-started-app/src/app/todo.service.ts b/apps/workbench-getting-started-app/src/app/todo.service.ts index aac5b15f9..ec2a50ba1 100644 --- a/apps/workbench-getting-started-app/src/app/todo.service.ts +++ b/apps/workbench-getting-started-app/src/app/todo.service.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2023 Swiss Federal Railways + * 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 diff --git a/apps/workbench-getting-started-app/src/app/todo/todo.component.html b/apps/workbench-getting-started-app/src/app/todo/todo.component.html index d9574d0e4..3b3a50a22 100644 --- a/apps/workbench-getting-started-app/src/app/todo/todo.component.html +++ b/apps/workbench-getting-started-app/src/app/todo/todo.component.html @@ -1,5 +1,5 @@ - +@if (todo$ | async; as todo) { Task:{{todo.task}} Due Date:{{todo.dueDate | date:'short'}} Notes:{{todo.notes}} - +} diff --git a/apps/workbench-getting-started-app/src/app/todo/todo.component.ts b/apps/workbench-getting-started-app/src/app/todo/todo.component.ts index 9bd977a8b..ee6295d43 100644 --- a/apps/workbench-getting-started-app/src/app/todo/todo.component.ts +++ b/apps/workbench-getting-started-app/src/app/todo/todo.component.ts @@ -13,7 +13,7 @@ import {WorkbenchView} from '@scion/workbench'; import {Todo, TodoService} from '../todo.service'; import {ActivatedRoute} from '@angular/router'; import {map, Observable, tap} from 'rxjs'; -import {AsyncPipe, DatePipe, formatDate, NgIf} from '@angular/common'; +import {AsyncPipe, DatePipe, formatDate} from '@angular/common'; @Component({ selector: 'app-todo', @@ -21,7 +21,6 @@ import {AsyncPipe, DatePipe, formatDate, NgIf} from '@angular/common'; styleUrls: ['./todo.component.scss'], standalone: true, imports: [ - NgIf, AsyncPipe, DatePipe, ], diff --git a/apps/workbench-getting-started-app/src/app/todos/todos.component.html b/apps/workbench-getting-started-app/src/app/todos/todos.component.html index c03e41d73..db32b8156 100644 --- a/apps/workbench-getting-started-app/src/app/todos/todos.component.html +++ b/apps/workbench-getting-started-app/src/app/todos/todos.component.html @@ -1,5 +1,7 @@
    -
  1. - {{todo.task}} -
  2. + @for (todo of todoService.todos; track todo.id) { +
  3. + {{todo.task}} +
  4. + }
diff --git a/apps/workbench-getting-started-app/src/app/todos/todos.component.ts b/apps/workbench-getting-started-app/src/app/todos/todos.component.ts index 2ec90fb8e..985326496 100644 --- a/apps/workbench-getting-started-app/src/app/todos/todos.component.ts +++ b/apps/workbench-getting-started-app/src/app/todos/todos.component.ts @@ -11,14 +11,12 @@ import {Component} from '@angular/core'; import {WorkbenchRouterLinkDirective, WorkbenchView} from '@scion/workbench'; import {TodoService} from '../todo.service'; -import {NgFor} from '@angular/common'; @Component({ selector: 'app-todos', templateUrl: './todos.component.html', standalone: true, imports: [ - NgFor, WorkbenchRouterLinkDirective, ], }) diff --git a/apps/workbench-getting-started-app/src/app/workbench.provider.ts b/apps/workbench-getting-started-app/src/app/workbench.provider.ts deleted file mode 100644 index 353d4bd63..000000000 --- a/apps/workbench-getting-started-app/src/app/workbench.provider.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {EnvironmentProviders, importProvidersFrom} from '@angular/core'; -import {WorkbenchModule, WorkbenchModuleConfig} from '@scion/workbench'; - -/** - * Provides a set of DI providers to set up @scion/workbench. - */ -export function provideWorkbench(config: WorkbenchModuleConfig): EnvironmentProviders { - return importProvidersFrom(WorkbenchModule.forRoot(config)); -} diff --git a/apps/workbench-testing-app/src/app/app.component.html b/apps/workbench-testing-app/src/app/app.component.html index 1cfc844c5..4367cd2ed 100644 --- a/apps/workbench-testing-app/src/app/app.component.html +++ b/apps/workbench-testing-app/src/app/app.component.html @@ -1,4 +1,4 @@
- +
diff --git a/apps/workbench-testing-app/src/app/app.config.ts b/apps/workbench-testing-app/src/app/app.config.ts index f84826a05..b2ba7673f 100644 --- a/apps/workbench-testing-app/src/app/app.config.ts +++ b/apps/workbench-testing-app/src/app/app.config.ts @@ -8,11 +8,10 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {provideWorkbench} from './workbench.provider'; import {ApplicationConfig, EnvironmentProviders, makeEnvironmentProviders} from '@angular/core'; import {provideRouter, withHashLocation} from '@angular/router'; import {routes} from './app.routes'; -import {workbenchModuleConfig} from './workbench.config'; +import {workbenchConfig} from './workbench.config'; import {provideConfirmWorkbenchStartupInitializer} from './workbench/confirm-workbench-startup-initializer.service'; import {provideThrottleCapabilityLookupInterceptor} from './workbench/throttle-capability-lookup-initializer.service'; import {provideWorkbenchLifecycleHookLoggers} from './workbench/workbench-lifecycle-hook-loggers'; @@ -22,6 +21,7 @@ import {provideNotificationPage} from './notification-page/notification-page-int import {Perspectives} from './workbench.perspectives'; import {environment} from '../environments/environment'; import {provideAnimations, provideNoopAnimations} from '@angular/platform-browser/animations'; +import {provideWorkbench} from '@scion/workbench'; /** * Central place to configure the workbench-testing-app. @@ -29,7 +29,7 @@ import {provideAnimations, provideNoopAnimations} from '@angular/platform-browse export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes, withHashLocation()), - provideWorkbench(workbenchModuleConfig), + provideWorkbench(workbenchConfig), provideConfirmWorkbenchStartupInitializer(), provideThrottleCapabilityLookupInterceptor(), provideWorkbenchLifecycleHookLoggers(), diff --git a/apps/workbench-testing-app/src/app/app.routes.ts b/apps/workbench-testing-app/src/app/app.routes.ts index d79ad07db..773641cb7 100644 --- a/apps/workbench-testing-app/src/app/app.routes.ts +++ b/apps/workbench-testing-app/src/app/app.routes.ts @@ -10,16 +10,17 @@ import {Routes} from '@angular/router'; import {WorkbenchComponent} from './workbench/workbench.component'; -import {WorkbenchRouteData} from '@scion/workbench'; import {topLevelTestPageRoutes} from './test-pages/routes'; +import {canMatchWorkbenchView, WorkbenchRouteData} from '@scion/workbench'; export const routes: Routes = [ { path: '', component: WorkbenchComponent, + canMatch: [canMatchWorkbenchView(false)], children: [ { - path: '', // default workbench page + path: '', loadComponent: () => import('./start-page/start-page.component'), }, ], @@ -28,6 +29,30 @@ export const routes: Routes = [ path: 'workbench-page', redirectTo: '', }, + { + path: '', + canMatch: [canMatchWorkbenchView('test-router')], + loadComponent: () => import('./router-page/router-page.component'), + data: { + [WorkbenchRouteData.title]: 'Workbench Router', + [WorkbenchRouteData.heading]: 'Workbench E2E Testpage', + [WorkbenchRouteData.cssClass]: 'e2e-test-router', + path: '', + navigationHint: 'test-router', + }, + }, + { + path: '', + canMatch: [canMatchWorkbenchView('test-view')], + loadComponent: () => import('./view-page/view-page.component'), + data: { + [WorkbenchRouteData.title]: 'Workbench View', + [WorkbenchRouteData.heading]: 'Workbench E2E Testpage', + [WorkbenchRouteData.cssClass]: 'e2e-test-view', + path: '', + navigationHint: 'test-view', + }, + }, { path: 'start-page', loadComponent: () => import('./start-page/start-page.component'), @@ -36,17 +61,38 @@ export const routes: Routes = [ { path: 'test-router', loadComponent: () => import('./router-page/router-page.component'), - data: {[WorkbenchRouteData.title]: 'Workbench Router', [WorkbenchRouteData.heading]: 'Workbench E2E Testpage', [WorkbenchRouteData.cssClass]: 'e2e-test-router', pinToStartPage: true}, + data: { + [WorkbenchRouteData.title]: 'Workbench Router', + [WorkbenchRouteData.heading]: 'Workbench E2E Testpage', + [WorkbenchRouteData.cssClass]: 'e2e-test-router', + pinToStartPage: true, + path: 'test-router', + navigationHint: '', + }, }, { path: 'test-view', + canMatch: [canMatchWorkbenchView('test-view')], loadComponent: () => import('./view-page/view-page.component'), - data: {[WorkbenchRouteData.title]: 'Workbench View', [WorkbenchRouteData.heading]: 'Workbench E2E Testpage', [WorkbenchRouteData.cssClass]: 'e2e-test-view', pinToStartPage: true}, + data: { + [WorkbenchRouteData.title]: 'Workbench View', + [WorkbenchRouteData.heading]: 'Workbench E2E Testpage', + [WorkbenchRouteData.cssClass]: 'e2e-test-view', + path: 'test-view', + navigationHint: 'test-view', + }, }, { - path: 'test-perspective', - loadComponent: () => import('./perspective-page/perspective-page.component'), - data: {[WorkbenchRouteData.title]: 'Workbench Perspective', [WorkbenchRouteData.heading]: 'Workbench E2E Testpage', [WorkbenchRouteData.cssClass]: 'e2e-test-perspective', pinToStartPage: true}, + path: 'test-view', + loadComponent: () => import('./view-page/view-page.component'), + data: { + [WorkbenchRouteData.title]: 'Workbench View', + [WorkbenchRouteData.heading]: 'Workbench E2E Testpage', + [WorkbenchRouteData.cssClass]: 'e2e-test-view', + pinToStartPage: true, + path: 'test-view', + navigationHint: '', + }, }, { path: 'test-layout', diff --git a/apps/workbench-testing-app/src/app/css-class/css-class.component.html b/apps/workbench-testing-app/src/app/css-class/css-class.component.html new file mode 100644 index 000000000..1579b5c12 --- /dev/null +++ b/apps/workbench-testing-app/src/app/css-class/css-class.component.html @@ -0,0 +1 @@ + diff --git a/apps/workbench-testing-app/src/app/css-class/css-class.component.scss b/apps/workbench-testing-app/src/app/css-class/css-class.component.scss new file mode 100644 index 000000000..85980ba98 --- /dev/null +++ b/apps/workbench-testing-app/src/app/css-class/css-class.component.scss @@ -0,0 +1,10 @@ +@use '@scion/components.internal/design' as sci-design; + +:host { + display: inline-grid; + + > input { + @include sci-design.style-input-field(); + min-width: 0; + } +} diff --git a/apps/workbench-testing-app/src/app/css-class/css-class.component.ts b/apps/workbench-testing-app/src/app/css-class/css-class.component.ts new file mode 100644 index 000000000..2156983d7 --- /dev/null +++ b/apps/workbench-testing-app/src/app/css-class/css-class.component.ts @@ -0,0 +1,84 @@ +/* + * 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 + */ + +import {Component, forwardRef} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR, NonNullableFormBuilder, ReactiveFormsModule} from '@angular/forms'; +import {noop} from 'rxjs'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {Arrays} from '@scion/toolkit/util'; + +@Component({ + selector: 'app-css-class', + templateUrl: './css-class.component.html', + styleUrls: ['./css-class.component.scss'], + standalone: true, + imports: [ + ReactiveFormsModule, + ], + providers: [ + {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => CssClassComponent)}, + ], +}) +export class CssClassComponent implements ControlValueAccessor { + + private _cvaChangeFn: (cssClasses: string | string[] | undefined) => void = noop; + private _cvaTouchedFn: () => void = noop; + + protected formControl = this._formBuilder.control(''); + + constructor(private _formBuilder: NonNullableFormBuilder) { + this.formControl.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(() => { + this._cvaChangeFn(this.parse(this.formControl.value)); + this._cvaTouchedFn(); + }); + } + + private parse(stringified: string): string[] | string | undefined { + const cssClasses = stringified.split(/\s+/).filter(Boolean); + switch (cssClasses.length) { + case 0: + return undefined; + case 1: + return cssClasses[0]; + default: + return cssClasses; + } + } + + private stringify(cssClasses: string | string[] | undefined | null): string { + return Arrays.coerce(cssClasses).join(' '); + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public writeValue(cssClasses: string | string[] | undefined | null): void { + this.formControl.setValue(this.stringify(cssClasses), {emitEvent: false}); + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public registerOnChange(fn: any): void { + this._cvaChangeFn = fn; + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public registerOnTouched(fn: any): void { + this._cvaTouchedFn = fn; + } +} diff --git a/apps/workbench-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.html b/apps/workbench-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.html index 6d20acd0c..5a50bbf64 100644 --- a/apps/workbench-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.html +++ b/apps/workbench-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.html @@ -33,7 +33,7 @@
- + diff --git a/apps/workbench-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.ts b/apps/workbench-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.ts index 2cde0d9b4..36c8b819a 100644 --- a/apps/workbench-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.ts +++ b/apps/workbench-testing-app/src/app/dialog-opener-page/dialog-opener-page.component.ts @@ -10,7 +10,7 @@ import {ApplicationRef, Component, Type} from '@angular/core'; import {FormGroup, NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; -import {WorkbenchDialogService} from '@scion/workbench'; +import {ViewId, WorkbenchDialogService} from '@scion/workbench'; import {startWith} from 'rxjs/operators'; import {NgIf} from '@angular/common'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; @@ -23,6 +23,7 @@ import BlankTestPageComponent from '../test-pages/blank-test-page/blank-test-pag import FocusTestPageComponent from '../test-pages/focus-test-page/focus-test-page.component'; import PopupOpenerPageComponent from '../popup-opener-page/popup-opener-page.component'; import InputFieldTestPageComponent from '../test-pages/input-field-test-page/input-field-test-page.component'; +import {CssClassComponent} from '../css-class/css-class.component'; @Component({ selector: 'app-dialog-opener-page', @@ -35,6 +36,7 @@ import InputFieldTestPageComponent from '../test-pages/input-field-test-page/inp SciFormFieldComponent, SciKeyValueFieldComponent, SciCheckboxComponent, + CssClassComponent, ], }) export default class DialogOpenerPageComponent { @@ -44,8 +46,8 @@ export default class DialogOpenerPageComponent { options: this._formBuilder.group({ inputs: this._formBuilder.array>([]), modality: this._formBuilder.control<'application' | 'view' | ''>(''), - contextualViewId: this._formBuilder.control(''), - cssClass: this._formBuilder.control(''), + contextualViewId: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), animate: this._formBuilder.control(undefined), }), count: this._formBuilder.control(''), @@ -79,7 +81,7 @@ export default class DialogOpenerPageComponent { return dialogService.open(component, { inputs: SciKeyValueFieldComponent.toDictionary(this.form.controls.options.controls.inputs) ?? undefined, modality: this.form.controls.options.controls.modality.value || undefined, - cssClass: [`index-${index}`].concat(this.form.controls.options.controls.cssClass.value.split(/\s+/).filter(Boolean) || []), + cssClass: [`index-${index}`].concat(this.form.controls.options.controls.cssClass.value ?? []), animate: this.form.controls.options.controls.animate.value, context: { viewId: this.form.controls.options.controls.contextualViewId.value || undefined, diff --git a/apps/workbench-testing-app/src/app/header/header.component.ts b/apps/workbench-testing-app/src/app/header/header.component.ts index 4f957410c..72f66fb1a 100644 --- a/apps/workbench-testing-app/src/app/header/header.component.ts +++ b/apps/workbench-testing-app/src/app/header/header.component.ts @@ -189,6 +189,11 @@ export class HeaderComponent { private contributeSettingsMenuItems(): MenuItem[] { return [ + new MenuItem({ + text: 'Reset forms on submit', + checked: this._settingsService.isEnabled('resetFormsOnSubmit'), + onAction: () => this._settingsService.toggle('resetFormsOnSubmit'), + }), new MenuItem({ text: 'Log Angular change detection cycles', cssClass: 'e2e-log-angular-change-detection-cycles', diff --git a/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.html b/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.html deleted file mode 100644 index e608d70a1..000000000 --- a/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.html +++ /dev/null @@ -1,27 +0,0 @@ -
-
- - - -
- -
-
Options
- - - - -
- - -
- - - Success - - - - {{navigateError}} - diff --git a/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.scss b/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.scss deleted file mode 100644 index 05e8cc1f9..000000000 --- a/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.scss +++ /dev/null @@ -1,42 +0,0 @@ -:host { - display: flex; - flex-direction: column; - gap: 1em; - - > form { - flex: none; - display: flex; - flex-direction: column; - gap: 1em; - - > section { - flex: none; - display: flex; - flex-direction: column; - gap: .5em; - border: 1px solid var(--sci-color-border); - border-radius: var(--sci-corner); - padding: 1em; - - > header { - margin-bottom: 1em; - font-weight: bold; - } - } - } - - > output.navigate-success { - flex: none; - display: none; - } - - > output.navigate-error { - flex: none; - border: 1px solid var(--sci-color-negative); - background-color: var(--sci-color-background-negative); - color: var(--sci-color-negative); - border-radius: var(--sci-corner); - padding: 1em; - font-family: monospace; - } -} diff --git a/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.ts b/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.ts deleted file mode 100644 index 8db2fee99..000000000 --- a/apps/workbench-testing-app/src/app/layout-page/activate-view-page/activate-view-page.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 - */ - -import {Component} from '@angular/core'; -import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; -import {WorkbenchRouter, WorkbenchService} from '@scion/workbench'; -import {AsyncPipe, NgFor, NgIf} from '@angular/common'; -import {stringifyError} from '../../common/stringify-error.util'; -import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; -import {SciFormFieldComponent} from '@scion/components.internal/form-field'; - -@Component({ - selector: 'app-activate-view-page', - templateUrl: './activate-view-page.component.html', - styleUrls: ['./activate-view-page.component.scss'], - standalone: true, - imports: [ - NgIf, - NgFor, - AsyncPipe, - ReactiveFormsModule, - SciFormFieldComponent, - SciCheckboxComponent, - ], -}) -export default class ActivateViewPageComponent { - - public form = this._formBuilder.group({ - viewId: this._formBuilder.control('', Validators.required), - options: this._formBuilder.group({ - activatePart: this._formBuilder.control(undefined), - }), - }); - public navigateError: string | false | undefined; - - constructor(private _formBuilder: NonNullableFormBuilder, - private _wbRouter: WorkbenchRouter, - public workbenchService: WorkbenchService) { - } - - public onNavigate(): void { - this.navigateError = undefined; - - this._wbRouter - .ɵnavigate(layout => layout.activateView(this.form.controls.viewId.value, { - activatePart: this.form.controls.options.controls.activatePart.value, - })) - .then(() => { - this.navigateError = false; - this.form.reset(); - }) - .catch(error => this.navigateError = stringifyError(error)); - } -} diff --git a/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.html b/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.html deleted file mode 100644 index 3271d2bc3..000000000 --- a/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.html +++ /dev/null @@ -1,47 +0,0 @@ -
-
- - - - - - - -
- -
-
Relative To
- - - - - - - - - - - - - - - -
- - -
- - - Success - - - - {{navigateError}} - diff --git a/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.scss b/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.scss deleted file mode 100644 index 05e8cc1f9..000000000 --- a/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.scss +++ /dev/null @@ -1,42 +0,0 @@ -:host { - display: flex; - flex-direction: column; - gap: 1em; - - > form { - flex: none; - display: flex; - flex-direction: column; - gap: 1em; - - > section { - flex: none; - display: flex; - flex-direction: column; - gap: .5em; - border: 1px solid var(--sci-color-border); - border-radius: var(--sci-corner); - padding: 1em; - - > header { - margin-bottom: 1em; - font-weight: bold; - } - } - } - - > output.navigate-success { - flex: none; - display: none; - } - - > output.navigate-error { - flex: none; - border: 1px solid var(--sci-color-negative); - background-color: var(--sci-color-background-negative); - color: var(--sci-color-negative); - border-radius: var(--sci-corner); - padding: 1em; - font-family: monospace; - } -} diff --git a/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.ts b/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.ts deleted file mode 100644 index 1eda67ac9..000000000 --- a/apps/workbench-testing-app/src/app/layout-page/add-part-page/add-part-page.component.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 - */ - -import {Component} from '@angular/core'; -import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; -import {WorkbenchRouter, WorkbenchService} from '@scion/workbench'; -import {AsyncPipe, NgFor, NgIf} from '@angular/common'; -import {stringifyError} from '../../common/stringify-error.util'; -import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; -import {SciFormFieldComponent} from '@scion/components.internal/form-field'; - -@Component({ - selector: 'app-add-part-page', - templateUrl: './add-part-page.component.html', - styleUrls: ['./add-part-page.component.scss'], - standalone: true, - imports: [ - NgIf, - NgFor, - AsyncPipe, - ReactiveFormsModule, - SciFormFieldComponent, - SciCheckboxComponent, - ], -}) -export default class AddPartPageComponent { - - public form = this._formBuilder.group({ - partId: this._formBuilder.control('', Validators.required), - relativeTo: this._formBuilder.group({ - partId: this._formBuilder.control(undefined), - align: this._formBuilder.control<'left' | 'right' | 'top' | 'bottom' | undefined>(undefined, Validators.required), - ratio: this._formBuilder.control(undefined), - }), - activate: this._formBuilder.control(undefined), - }); - - public navigateError: string | false | undefined; - - constructor(private _formBuilder: NonNullableFormBuilder, - private _wbRouter: WorkbenchRouter, - public workbenchService: WorkbenchService) { - } - - public onNavigate(): void { - this.navigateError = undefined; - - this._wbRouter - .ɵnavigate(layout => layout.addPart(this.form.controls.partId.value!, { - relativeTo: this.form.controls.relativeTo.controls.partId.value || undefined, - align: this.form.controls.relativeTo.controls.align.value!, - ratio: this.form.controls.relativeTo.controls.ratio.value, - }, - {activate: this.form.controls.activate.value}, - )) - .then(() => { - this.navigateError = false; - this.form.reset(); - }) - .catch(error => this.navigateError = stringifyError(error)); - } -} diff --git a/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.html b/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.html deleted file mode 100644 index 2136a6d36..000000000 --- a/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.html +++ /dev/null @@ -1,42 +0,0 @@ -
-
- - - -
- -
-
Options
- - - - - - - - - - - - - - - - - - - -
- - -
- - - Success - - - - {{navigateError}} - diff --git a/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.ts b/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.ts deleted file mode 100644 index 64bc924bb..000000000 --- a/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 - */ - -import {Component} from '@angular/core'; -import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; -import {WorkbenchRouter, WorkbenchService} from '@scion/workbench'; -import {AsyncPipe, NgFor, NgIf} from '@angular/common'; -import {stringifyError} from '../../common/stringify-error.util'; -import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; -import {SciFormFieldComponent} from '@scion/components.internal/form-field'; - -@Component({ - selector: 'app-add-view-page', - templateUrl: './add-view-page.component.html', - styleUrls: ['./add-view-page.component.scss'], - standalone: true, - imports: [ - NgIf, - NgFor, - AsyncPipe, - ReactiveFormsModule, - SciFormFieldComponent, - SciCheckboxComponent, - ], -}) -export default class AddViewPageComponent { - - public form = this._formBuilder.group({ - viewId: this._formBuilder.control('', Validators.required), - options: this._formBuilder.group({ - partId: this._formBuilder.control('', Validators.required), - position: this._formBuilder.control(undefined), - activateView: this._formBuilder.control(undefined), - activatePart: this._formBuilder.control(undefined), - }), - }); - public navigateError: string | false | undefined; - - constructor(private _formBuilder: NonNullableFormBuilder, - private _wbRouter: WorkbenchRouter, - public workbenchService: WorkbenchService) { - } - - public onNavigate(): void { - this.navigateError = undefined; - - this._wbRouter - .ɵnavigate(layout => layout.addView(this.form.controls.viewId.value, { - partId: this.form.controls.options.controls.partId.value, - position: this.form.controls.options.controls.position.value ?? undefined, - activateView: this.form.controls.options.controls.activateView.value, - activatePart: this.form.controls.options.controls.activatePart.value, - })) - .then(() => { - this.navigateError = false; - this.form.reset(); - }) - .catch(error => this.navigateError = stringifyError(error)); - } -} diff --git a/apps/workbench-testing-app/src/app/layout-page/create-perspective-page/create-perspective-page.component.html b/apps/workbench-testing-app/src/app/layout-page/create-perspective-page/create-perspective-page.component.html new file mode 100644 index 000000000..b4e93f787 --- /dev/null +++ b/apps/workbench-testing-app/src/app/layout-page/create-perspective-page/create-perspective-page.component.html @@ -0,0 +1,38 @@ +
+
+ + + + + + + + + +
+ +
+
Parts
+ +
+ +
+
Views
+ +
+ +
+
View Navigations
+ +
+ + +
+ +@if (registerError === false) { + Success +} + +@if (registerError) { + {{registerError}} +} diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.scss b/apps/workbench-testing-app/src/app/layout-page/create-perspective-page/create-perspective-page.component.scss similarity index 98% rename from apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.scss rename to apps/workbench-testing-app/src/app/layout-page/create-perspective-page/create-perspective-page.component.scss index 46a42ce96..242c1f2f0 100644 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.scss +++ b/apps/workbench-testing-app/src/app/layout-page/create-perspective-page/create-perspective-page.component.scss @@ -2,7 +2,6 @@ display: flex; flex-direction: column; gap: 1em; - padding: 1em; > form { flex: none; diff --git a/apps/workbench-testing-app/src/app/layout-page/create-perspective-page/create-perspective-page.component.ts b/apps/workbench-testing-app/src/app/layout-page/create-perspective-page/create-perspective-page.component.ts new file mode 100644 index 000000000..d005fa094 --- /dev/null +++ b/apps/workbench-testing-app/src/app/layout-page/create-perspective-page/create-perspective-page.component.ts @@ -0,0 +1,133 @@ +/* + * 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 + */ + +import {Component} from '@angular/core'; +import {AddPartsComponent, PartDescriptor} from '../tables/add-parts/add-parts.component'; +import {AddViewsComponent, ViewDescriptor} from '../tables/add-views/add-views.component'; +import {NavigateViewsComponent, NavigationDescriptor} from '../tables/navigate-views/navigate-views.component'; +import {FormGroup, NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; +import {SettingsService} from '../../settings.service'; +import {WorkbenchLayout, WorkbenchLayoutFactory, WorkbenchLayoutFn, WorkbenchService} from '@scion/workbench'; +import {stringifyError} from '../../common/stringify-error.util'; +import {SciFormFieldComponent} from '@scion/components.internal/form-field'; +import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; +import {KeyValueEntry, SciKeyValueFieldComponent} from '@scion/components.internal/key-value-field'; +import {Observable} from 'rxjs'; +import {mapArray} from '@scion/toolkit/operators'; +import {AsyncPipe} from '@angular/common'; + +@Component({ + selector: 'app-create-perspective-page', + templateUrl: './create-perspective-page.component.html', + styleUrls: ['./create-perspective-page.component.scss'], + standalone: true, + imports: [ + AddPartsComponent, + AddViewsComponent, + NavigateViewsComponent, + ReactiveFormsModule, + SciFormFieldComponent, + SciCheckboxComponent, + SciKeyValueFieldComponent, + AsyncPipe, + ], +}) +export default class CreatePerspectivePageComponent { + + protected form = this._formBuilder.group({ + id: this._formBuilder.control('', Validators.required), + transient: this._formBuilder.control(undefined), + data: this._formBuilder.array>([]), + parts: this._formBuilder.control([], Validators.required), + views: this._formBuilder.control([]), + viewNavigations: this._formBuilder.control([]), + }); + + protected registerError: string | false | undefined; + protected partProposals$: Observable; + protected viewProposals$: Observable; + + constructor(private _formBuilder: NonNullableFormBuilder, + private _settingsService: SettingsService, + private _workbenchService: WorkbenchService) { + this.partProposals$ = this.form.controls.parts.valueChanges + .pipe(mapArray(part => part.id)); + this.viewProposals$ = this.form.controls.views.valueChanges + .pipe(mapArray(view => view.id)); + } + + protected async onRegister(): Promise { + this.registerError = undefined; + try { + await this._workbenchService.registerPerspective({ + id: this.form.controls.id.value, + transient: this.form.controls.transient.value || undefined, + data: SciKeyValueFieldComponent.toDictionary(this.form.controls.data) ?? undefined, + layout: this.createLayout(), + }); + this.registerError = false; + this.resetForm(); + } + catch (error) { + this.registerError = stringifyError(error); + } + } + + private createLayout(): WorkbenchLayoutFn { + // Capture form values, since the `layout` function is evaluated independently of the form life-cycle + const [initialPart, ...parts] = this.form.controls.parts.value; + const views = this.form.controls.views.value; + const viewNavigations = this.form.controls.viewNavigations.value; + + return (factory: WorkbenchLayoutFactory): WorkbenchLayout => { + // Add initial part. + let layout = factory.addPart(initialPart.id, { + activate: initialPart.options?.activate, + }); + + // Add other parts. + for (const part of parts) { + layout = layout.addPart(part.id, { + relativeTo: part.relativeTo!.relativeTo, + align: part.relativeTo!.align!, + ratio: part.relativeTo!.ratio, + }, {activate: part.options?.activate}); + } + + // Add views. + for (const view of views) { + layout = layout.addView(view.id, { + partId: view.options.partId, + position: view.options.position, + activateView: view.options.activateView, + activatePart: view.options.activatePart, + cssClass: view.options.cssClass, + }); + } + + // Add navigations. + for (const viewNavigation of viewNavigations) { + layout = layout.navigateView(viewNavigation.id, viewNavigation.commands, { + hint: viewNavigation.extras?.hint, + state: viewNavigation.extras?.state, + cssClass: viewNavigation.extras?.cssClass, + }); + } + return layout; + }; + } + + private resetForm(): void { + if (this._settingsService.isEnabled('resetFormsOnSubmit')) { + this.form.reset(); + this.form.setControl('data', this._formBuilder.array>([])); + } + } +} diff --git a/apps/workbench-testing-app/src/app/layout-page/layout-page.component.html b/apps/workbench-testing-app/src/app/layout-page/layout-page.component.html index 8a5ace030..f7e2e58ae 100644 --- a/apps/workbench-testing-app/src/app/layout-page/layout-page.component.html +++ b/apps/workbench-testing-app/src/app/layout-page/layout-page.component.html @@ -1,17 +1,11 @@ - - + + - - - - - + + - - - diff --git a/apps/workbench-testing-app/src/app/layout-page/layout-page.component.ts b/apps/workbench-testing-app/src/app/layout-page/layout-page.component.ts index 8510cf8bf..2db924b6c 100644 --- a/apps/workbench-testing-app/src/app/layout-page/layout-page.component.ts +++ b/apps/workbench-testing-app/src/app/layout-page/layout-page.component.ts @@ -9,12 +9,10 @@ */ import {Component} from '@angular/core'; -import AddPartPageComponent from './add-part-page/add-part-page.component'; -import AddViewPageComponent from './add-view-page/add-view-page.component'; -import ActivateViewPageComponent from './activate-view-page/activate-view-page.component'; import RegisterPartActionPageComponent from './register-part-action-page/register-part-action-page.component'; -import RegisterRoutePageComponent from './register-route-page/register-route-page.component'; import {SciTabbarComponent, SciTabDirective} from '@scion/components.internal/tabbar'; +import ModifyLayoutPageComponent from './modify-layout-page/modify-layout-page.component'; +import CreatePerspectivePageComponent from './create-perspective-page/create-perspective-page.component'; @Component({ selector: 'app-layout-page', @@ -24,11 +22,9 @@ import {SciTabbarComponent, SciTabDirective} from '@scion/components.internal/ta imports: [ SciTabbarComponent, SciTabDirective, - AddPartPageComponent, - AddViewPageComponent, - ActivateViewPageComponent, + ModifyLayoutPageComponent, + CreatePerspectivePageComponent, RegisterPartActionPageComponent, - RegisterRoutePageComponent, ], }) export default class LayoutPageComponent { diff --git a/apps/workbench-testing-app/src/app/layout-page/modify-layout-page/modify-layout-page.component.html b/apps/workbench-testing-app/src/app/layout-page/modify-layout-page/modify-layout-page.component.html new file mode 100644 index 000000000..151eec0b5 --- /dev/null +++ b/apps/workbench-testing-app/src/app/layout-page/modify-layout-page/modify-layout-page.component.html @@ -0,0 +1,22 @@ +
+
+
Parts
+ +
+ +
+
Views
+ +
+ +
+
View Navigations
+ +
+ + +
+ +@if (modifyError) { + {{modifyError}} +} diff --git a/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.scss b/apps/workbench-testing-app/src/app/layout-page/modify-layout-page/modify-layout-page.component.scss similarity index 87% rename from apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.scss rename to apps/workbench-testing-app/src/app/layout-page/modify-layout-page/modify-layout-page.component.scss index 05e8cc1f9..6a312ca72 100644 --- a/apps/workbench-testing-app/src/app/layout-page/add-view-page/add-view-page.component.scss +++ b/apps/workbench-testing-app/src/app/layout-page/modify-layout-page/modify-layout-page.component.scss @@ -25,12 +25,7 @@ } } - > output.navigate-success { - flex: none; - display: none; - } - - > output.navigate-error { + > output.modify-error { flex: none; border: 1px solid var(--sci-color-negative); background-color: var(--sci-color-background-negative); diff --git a/apps/workbench-testing-app/src/app/layout-page/modify-layout-page/modify-layout-page.component.ts b/apps/workbench-testing-app/src/app/layout-page/modify-layout-page/modify-layout-page.component.ts new file mode 100644 index 000000000..4565583d2 --- /dev/null +++ b/apps/workbench-testing-app/src/app/layout-page/modify-layout-page/modify-layout-page.component.ts @@ -0,0 +1,115 @@ +/* + * 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 + */ + +import {Component} from '@angular/core'; +import {AddPartsComponent, PartDescriptor} from '../tables/add-parts/add-parts.component'; +import {AddViewsComponent, ViewDescriptor} from '../tables/add-views/add-views.component'; +import {NavigateViewsComponent, NavigationDescriptor} from '../tables/navigate-views/navigate-views.component'; +import {NonNullableFormBuilder, ReactiveFormsModule} from '@angular/forms'; +import {SettingsService} from '../../settings.service'; +import {WorkbenchRouter, WorkbenchService} from '@scion/workbench'; +import {stringifyError} from '../../common/stringify-error.util'; +import {combineLatestWith, Observable} from 'rxjs'; +import {mapArray} from '@scion/toolkit/operators'; +import {map, startWith} from 'rxjs/operators'; +import {AsyncPipe} from '@angular/common'; + +@Component({ + selector: 'app-modify-layout-page', + templateUrl: './modify-layout-page.component.html', + styleUrls: ['./modify-layout-page.component.scss'], + standalone: true, + imports: [ + AddPartsComponent, + AddViewsComponent, + NavigateViewsComponent, + ReactiveFormsModule, + AsyncPipe, + ], +}) +export default class ModifyLayoutPageComponent { + + protected form = this._formBuilder.group({ + parts: this._formBuilder.control([]), + views: this._formBuilder.control([]), + viewNavigations: this._formBuilder.control([]), + }); + + protected modifyError: string | false | undefined; + protected partProposals$: Observable; + protected viewProposals$: Observable; + + constructor(workbenchService: WorkbenchService, + private _formBuilder: NonNullableFormBuilder, + private _settingsService: SettingsService, + private _workbenchRouter: WorkbenchRouter) { + this.partProposals$ = workbenchService.parts$ + .pipe( + combineLatestWith(this.form.controls.parts.valueChanges.pipe(startWith([]))), + map(([a, b]) => [...a, ...b]), + mapArray(part => part.id), + ); + this.viewProposals$ = workbenchService.views$ + .pipe( + combineLatestWith(this.form.controls.views.valueChanges.pipe(startWith([]))), + map(([a, b]) => [...a, ...b]), + mapArray(view => view.id), + ); + } + + protected async onModify(): Promise { + this.modifyError = undefined; + this.navigate() + .then(success => success ? Promise.resolve() : Promise.reject('Modification failed')) + .then(() => this.resetForm()) + .catch(error => this.modifyError = stringifyError(error)); + } + + private navigate(): Promise { + return this._workbenchRouter.navigate(layout => { + // Add parts. + for (const part of this.form.controls.parts.value) { + layout = layout.addPart(part.id, { + relativeTo: part.relativeTo!.relativeTo, + align: part.relativeTo!.align!, + ratio: part.relativeTo!.ratio!, + }, {activate: part.options?.activate}); + } + + // Add views. + for (const view of this.form.controls.views.value) { + layout = layout.addView(view.id, { + partId: view.options.partId, + position: view.options.position, + activateView: view.options.activateView, + activatePart: view.options.activatePart, + cssClass: view.options.cssClass, + }); + } + + // Add navigations. + for (const viewNavigation of this.form.controls.viewNavigations.value) { + layout = layout.navigateView(viewNavigation.id, viewNavigation.commands, { + hint: viewNavigation.extras?.hint, + state: viewNavigation.extras?.state, + cssClass: viewNavigation.extras?.cssClass, + }); + } + + return layout; + }); + } + + private resetForm(): void { + if (this._settingsService.isEnabled('resetFormsOnSubmit')) { + this.form.reset(); + } + } +} diff --git a/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.html b/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.html index 7aaa11c47..6177e768a 100644 --- a/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.html +++ b/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.html @@ -13,30 +13,30 @@ - + -
+
CanMatch
- - + + - - + + - - + + diff --git a/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.ts b/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.ts index 617d84f9f..6a2956a33 100644 --- a/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.ts +++ b/apps/workbench-testing-app/src/app/layout-page/register-part-action-page/register-part-action-page.component.ts @@ -17,6 +17,9 @@ import {undefinedIfEmpty} from '../../common/undefined-if-empty.util'; import {stringifyError} from '../../common/stringify-error.util'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {Arrays} from '@scion/toolkit/util'; +import {SettingsService} from '../../settings.service'; +import {CssClassComponent} from '../../css-class/css-class.component'; +import {UUID} from '@scion/toolkit/uuid'; @Component({ selector: 'app-register-part-action-page', @@ -29,6 +32,7 @@ import {Arrays} from '@scion/toolkit/util'; AsyncPipe, ReactiveFormsModule, SciFormFieldComponent, + CssClassComponent, ], }) export default class RegisterPartActionPageComponent { @@ -36,7 +40,7 @@ export default class RegisterPartActionPageComponent { public form = this._formBuilder.group({ content: this._formBuilder.control('', {validators: Validators.required}), align: this._formBuilder.control<'start' | 'end' | ''>(''), - cssClass: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), canMatch: this._formBuilder.group({ view: this._formBuilder.control(''), part: this._formBuilder.control(''), @@ -45,7 +49,13 @@ export default class RegisterPartActionPageComponent { }); public registerError: string | false | undefined; - constructor(private _formBuilder: NonNullableFormBuilder, public workbenchService: WorkbenchService) { + protected viewList = `view-list-${UUID.randomUUID()}`; + protected partList = `part-list-${UUID.randomUUID()}`; + protected gridList = `grid-list-${UUID.randomUUID()}`; + + constructor(private _formBuilder: NonNullableFormBuilder, + private _settingsService: SettingsService, + public workbenchService: WorkbenchService) { } public onRegister(): void { @@ -74,15 +84,21 @@ export default class RegisterPartActionPageComponent { } return true; }), - cssClass: this.form.controls.cssClass.value.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.cssClass.value, }); this.registerError = false; - this.form.reset(); + this.resetForm(); } catch (error: unknown) { this.registerError = stringifyError(error); } } + + private resetForm(): void { + if (this._settingsService.isEnabled('resetFormsOnSubmit')) { + this.form.reset(); + } + } } @Component({ diff --git a/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.html b/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.html deleted file mode 100644 index e8fe2fc01..000000000 --- a/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.html +++ /dev/null @@ -1,34 +0,0 @@ -
-
- - - - - - - - - - - - - - -
- -
-
Route Data
- - - - - - - - -
- - -
diff --git a/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.scss b/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.scss deleted file mode 100644 index 48a512dc4..000000000 --- a/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.scss +++ /dev/null @@ -1,26 +0,0 @@ -:host { - display: grid; - padding: 1em; - - > form { - flex: none; - display: flex; - flex-direction: column; - gap: 1em; - - > section { - flex: none; - display: flex; - flex-direction: column; - gap: .5em; - border: 1px solid var(--sci-color-border); - border-radius: var(--sci-corner); - padding: 1em; - - > header { - margin-bottom: 1em; - font-weight: bold; - } - } - } -} diff --git a/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.ts b/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.ts deleted file mode 100644 index 51ff9dab5..000000000 --- a/apps/workbench-testing-app/src/app/layout-page/register-route-page/register-route-page.component.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 - */ - -import {Component, Type} from '@angular/core'; -import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; -import {WorkbenchRouteData} from '@scion/workbench'; -import {KeyValuePipe, NgFor, NgIf} from '@angular/common'; -import {DefaultExport, Router, Routes} from '@angular/router'; -import {SciFormFieldComponent} from '@scion/components.internal/form-field'; - -@Component({ - selector: 'app-register-route-page', - templateUrl: './register-route-page.component.html', - styleUrls: ['./register-route-page.component.scss'], - standalone: true, - imports: [ - NgIf, - NgFor, - ReactiveFormsModule, - KeyValuePipe, - SciFormFieldComponent, - ], -}) -export default class RegisterRoutePageComponent { - - public readonly componentRefs = new Map Promise>>>() - .set('view-page', () => import('../../view-page/view-page.component')) - .set('router-page', () => import('../../router-page/router-page.component')); - - public form = this._formBuilder.group({ - path: this._formBuilder.control(''), - component: this._formBuilder.control<'view-page' | 'router-page' | ''>('', Validators.required), - outlet: this._formBuilder.control(''), - routeData: this._formBuilder.group({ - title: this._formBuilder.control(''), - cssClass: this._formBuilder.control(''), - }), - }); - - constructor(private _formBuilder: NonNullableFormBuilder, private _router: Router) { - } - - public onRegister(): void { - this.replaceRouterConfig([ - ...this._router.config, - { - path: this.form.controls.path.value, - outlet: this.form.controls.outlet.value || undefined, - loadComponent: this.componentRefs.get(this.form.controls.component.value), - data: { - [WorkbenchRouteData.title]: this.form.controls.routeData.controls.title.value || undefined, - [WorkbenchRouteData.heading]: 'Workbench E2E Testpage', - [WorkbenchRouteData.cssClass]: this.form.controls.routeData.controls.cssClass.value.split(/\s+/).filter(Boolean), - }, - }, - ]); - - // Perform navigation to apply the route config change. - this._router.navigate([], {skipLocationChange: true, onSameUrlNavigation: 'reload'}).then(); - this.form.reset(); - } - - /** - * Replaces the router configuration to install or uninstall routes at runtime. - * - * Same implementation as in {@link WorkbenchAuxiliaryRoutesRegistrator}. - */ - private replaceRouterConfig(config: Routes): void { - // Note: - // - Do not use Router.resetConfig(...) which would destroy any currently routed component because copying all routes - // - Do not assign the router a new Routes object (Router.config = ...) to allow resolution of routes added during `NavigationStart` (since Angular 7.x) - // (because Angular uses a reference to the Routes object during route navigation) - const newRoutes: Routes = [...config]; - this._router.config.splice(0, this._router.config.length, ...newRoutes); - } -} diff --git a/apps/workbench-testing-app/src/app/layout-page/tables/add-parts/add-parts.component.html b/apps/workbench-testing-app/src/app/layout-page/tables/add-parts/add-parts.component.html new file mode 100644 index 000000000..245b08de9 --- /dev/null +++ b/apps/workbench-testing-app/src/app/layout-page/tables/add-parts/add-parts.component.html @@ -0,0 +1,38 @@ +
+ Part ID + RelativeTo + Align + Ratio + Activate + + + @for (partFormGroup of form.controls.parts.controls; track $index) { + + + + + + + + + + + + + } +
+ + + + + + + @for (part of partProposals; track part) { + + } + diff --git a/apps/workbench-testing-app/src/app/layout-page/tables/add-parts/add-parts.component.scss b/apps/workbench-testing-app/src/app/layout-page/tables/add-parts/add-parts.component.scss new file mode 100644 index 000000000..bd37309e2 --- /dev/null +++ b/apps/workbench-testing-app/src/app/layout-page/tables/add-parts/add-parts.component.scss @@ -0,0 +1,24 @@ +@use '@scion/components.internal/design' as sci-design; + +:host { + display: grid; + + > form { + display: grid; + grid-template-columns: 7.5em repeat(3, 1fr) min-content auto; + gap: .5em .75em; + align-items: center; + + > span.checkbox { + text-align: center; + } + + > sci-checkbox { + justify-self: center; + } + + > input, select { + @include sci-design.style-input-field(); + } + } +} diff --git a/apps/workbench-testing-app/src/app/layout-page/tables/add-parts/add-parts.component.ts b/apps/workbench-testing-app/src/app/layout-page/tables/add-parts/add-parts.component.ts new file mode 100644 index 000000000..6fe68b96d --- /dev/null +++ b/apps/workbench-testing-app/src/app/layout-page/tables/add-parts/add-parts.component.ts @@ -0,0 +1,160 @@ +/* + * 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 + */ + +import {Component, forwardRef, Input} from '@angular/core'; +import {AbstractControl, ControlValueAccessor, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, NonNullableFormBuilder, ReactiveFormsModule, ValidationErrors, Validator, Validators} from '@angular/forms'; +import {SciFormFieldComponent} from '@scion/components.internal/form-field'; +import {noop} from 'rxjs'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; +import {MAIN_AREA} from '@scion/workbench'; +import {SciMaterialIconDirective} from '@scion/components.internal/material-icon'; +import {UUID} from '@scion/toolkit/uuid'; + +@Component({ + selector: 'app-add-parts', + templateUrl: './add-parts.component.html', + styleUrls: ['./add-parts.component.scss'], + standalone: true, + imports: [ + ReactiveFormsModule, + SciFormFieldComponent, + SciCheckboxComponent, + SciMaterialIconDirective, + ], + providers: [ + {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => AddPartsComponent)}, + {provide: NG_VALIDATORS, multi: true, useExisting: forwardRef(() => AddPartsComponent)}, + ], +}) +export class AddPartsComponent implements ControlValueAccessor, Validator { + + private _cvaChangeFn: (value: PartDescriptor[]) => void = noop; + private _cvaTouchedFn: () => void = noop; + + @Input() + public requiresInitialPart = false; + + @Input({transform: arrayAttribute}) + public partProposals: string[] = []; + + protected form = this._formBuilder.group({ + parts: this._formBuilder.array; + relativeTo: FormGroup<{ + relativeTo: FormControl; + align: FormControl<'left' | 'right' | 'top' | 'bottom' | undefined>; + ratio: FormControl; + }>; + options: FormGroup<{ + activate: FormControl; + }>; + }>>([]), + }); + + protected MAIN_AREA = MAIN_AREA; + protected relativeToList = `relative-to-list-${UUID.randomUUID()}`; + protected idList = `id-list-${UUID.randomUUID()}`; + + constructor(private _formBuilder: NonNullableFormBuilder) { + this.form.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(() => { + this._cvaChangeFn(this.form.controls.parts.controls.map(partFormGroup => ({ + id: partFormGroup.controls.id.value, + relativeTo: { + relativeTo: partFormGroup.controls.relativeTo.controls.relativeTo.value, + align: partFormGroup.controls.relativeTo.controls.align.value, + ratio: partFormGroup.controls.relativeTo.controls.ratio.value, + }, + options: { + activate: partFormGroup.controls.options.controls.activate.value, + }, + }))); + this._cvaTouchedFn(); + }); + } + + protected onAddPart(): void { + this.addPart({ + id: '', + relativeTo: {}, + }); + } + + protected onRemovePart(index: number): void { + this.form.controls.parts.removeAt(index); + } + + private addPart(part: PartDescriptor, options?: {emitEvent?: boolean}): void { + const isInitialPart = this.requiresInitialPart && this.form.controls.parts.length === 0; + this.form.controls.parts.push( + this._formBuilder.group({ + id: this._formBuilder.control(part.id, Validators.required), + relativeTo: this._formBuilder.group({ + relativeTo: this._formBuilder.control({value: isInitialPart ? undefined : part.relativeTo.relativeTo, disabled: isInitialPart}), + align: this._formBuilder.control<'left' | 'right' | 'top' | 'bottom' | undefined>({value: isInitialPart ? undefined : part.relativeTo.align, disabled: isInitialPart}, isInitialPart ? Validators.nullValidator : Validators.required), + ratio: this._formBuilder.control({value: isInitialPart ? undefined : part.relativeTo.ratio, disabled: isInitialPart}), + }), + options: this._formBuilder.group({ + activate: part.options?.activate, + }), + }), {emitEvent: options?.emitEvent ?? true}); + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public writeValue(parts: PartDescriptor[] | undefined | null): void { + this.form.controls.parts.clear({emitEvent: false}); + parts?.forEach(part => this.addPart(part, {emitEvent: false})); + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public registerOnChange(fn: any): void { + this._cvaChangeFn = fn; + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public registerOnTouched(fn: any): void { + this._cvaTouchedFn = fn; + } + + /** + * Method implemented as part of `Validator` to work with Angular forms API + * @docs-private + */ + public validate(control: AbstractControl): ValidationErrors | null { + return this.form.controls.parts.valid ? null : {valid: false}; + } +} + +export interface PartDescriptor { + id: string | MAIN_AREA; + relativeTo: { + relativeTo?: string; + align?: 'left' | 'right' | 'top' | 'bottom'; + ratio?: number; + }; + options?: { + activate?: boolean; + }; +} + +function arrayAttribute(proposals: string[] | null | undefined): string[] { + return proposals ?? []; +} diff --git a/apps/workbench-testing-app/src/app/layout-page/tables/add-views/add-views.component.html b/apps/workbench-testing-app/src/app/layout-page/tables/add-views/add-views.component.html new file mode 100644 index 000000000..f2d5d3c15 --- /dev/null +++ b/apps/workbench-testing-app/src/app/layout-page/tables/add-views/add-views.component.html @@ -0,0 +1,32 @@ +
+ View ID + Part ID + Position + CSS Class(es) + Activate View + Activate Part + + + @for (viewFormGroup of form.controls.views.controls; track $index) { + + + + + + + + + + + + + + + } + + + + @for (part of partProposals; track part) { + + } + diff --git a/apps/workbench-testing-app/src/app/layout-page/tables/add-views/add-views.component.scss b/apps/workbench-testing-app/src/app/layout-page/tables/add-views/add-views.component.scss new file mode 100644 index 000000000..09725d00b --- /dev/null +++ b/apps/workbench-testing-app/src/app/layout-page/tables/add-views/add-views.component.scss @@ -0,0 +1,24 @@ +@use '@scion/components.internal/design' as sci-design; + +:host { + display: grid; + + > form { + display: grid; + grid-template-columns: 7.5em 1fr 5em 10em min-content min-content auto; + gap: .5em .75em; + align-items: center; + + > span.checkbox { + text-align: center; + } + + > sci-checkbox { + justify-self: center; + } + + > input { + @include sci-design.style-input-field(); + } + } +} diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.ts b/apps/workbench-testing-app/src/app/layout-page/tables/add-views/add-views.component.ts similarity index 50% rename from apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.ts rename to apps/workbench-testing-app/src/app/layout-page/tables/add-views/add-views.component.ts index b422fd118..9caf41bef 100644 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.ts +++ b/apps/workbench-testing-app/src/app/layout-page/tables/add-views/add-views.component.ts @@ -9,89 +9,96 @@ */ import {Component, forwardRef, Input} from '@angular/core'; -import {CommonModule} from '@angular/common'; import {noop} from 'rxjs'; import {AbstractControl, ControlValueAccessor, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, NonNullableFormBuilder, ReactiveFormsModule, ValidationErrors, Validator, Validators} from '@angular/forms'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; -import {PerspectivePagePartEntry} from '../perspective-page-parts/perspective-page-parts.component'; import {SciMaterialIconDirective} from '@scion/components.internal/material-icon'; +import {CssClassComponent} from '../../../css-class/css-class.component'; +import {UUID} from '@scion/toolkit/uuid'; @Component({ - selector: 'app-perspective-page-views', - templateUrl: './perspective-page-views.component.html', - styleUrls: ['./perspective-page-views.component.scss'], + selector: 'app-add-views', + templateUrl: './add-views.component.html', + styleUrls: ['./add-views.component.scss'], standalone: true, imports: [ - CommonModule, ReactiveFormsModule, SciCheckboxComponent, SciFormFieldComponent, SciMaterialIconDirective, + CssClassComponent, ], providers: [ - {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => PerspectivePageViewsComponent)}, - {provide: NG_VALIDATORS, multi: true, useExisting: forwardRef(() => PerspectivePageViewsComponent)}, + {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => AddViewsComponent)}, + {provide: NG_VALIDATORS, multi: true, useExisting: forwardRef(() => AddViewsComponent)}, ], }) -export class PerspectivePageViewsComponent implements ControlValueAccessor, Validator { +export class AddViewsComponent implements ControlValueAccessor, Validator { - private _cvaChangeFn: (value: PerspectivePageViewEntry[]) => void = noop; + private _cvaChangeFn: (value: ViewDescriptor[]) => void = noop; private _cvaTouchedFn: () => void = noop; - @Input() - public partEntries: PerspectivePagePartEntry[] = []; + @Input({transform: arrayAttribute}) + public partProposals: string[] = []; - public form = this._formBuilder.group({ + protected form = this._formBuilder.group({ views: this._formBuilder.array; - partId: FormControl; - position: FormControl; - activateView: FormControl; - activatePart: FormControl; + options: FormGroup<{ + partId: FormControl; + position: FormControl; + cssClass: FormControl; + activateView: FormControl; + activatePart: FormControl; + }>; }>>([]), }); + protected partList = `part-list-${UUID.randomUUID()}`; constructor(private _formBuilder: NonNullableFormBuilder) { this.form.valueChanges .pipe(takeUntilDestroyed()) .subscribe(() => { - const views: PerspectivePageViewEntry[] = this.form.controls.views.controls.map(viewFormGroup => ({ + this._cvaChangeFn(this.form.controls.views.controls.map(viewFormGroup => ({ id: viewFormGroup.controls.id.value, - partId: viewFormGroup.controls.partId.value, - position: viewFormGroup.controls.position.value, - activateView: viewFormGroup.controls.activateView.value, - activatePart: viewFormGroup.controls.activatePart.value, - })); - this._cvaChangeFn(views); + options: { + partId: viewFormGroup.controls.options.controls.partId.value, + position: viewFormGroup.controls.options.controls.position.value, + cssClass: viewFormGroup.controls.options.controls.cssClass.value, + activateView: viewFormGroup.controls.options.controls.activateView.value, + activatePart: viewFormGroup.controls.options.controls.activatePart.value, + }, + }))); this._cvaTouchedFn(); }); } protected onAddView(): void { - this.addViewEntry({ + this.addView({ id: '', - partId: '', + options: { + partId: '', + }, }); } - protected onClearViews(): void { - this.form.controls.views.clear(); - } - protected onRemoveView(index: number): void { this.form.controls.views.removeAt(index); } - private addViewEntry(view: PerspectivePageViewEntry, options?: {emitEvent?: boolean}): void { + private addView(view: ViewDescriptor, options?: {emitEvent?: boolean}): void { this.form.controls.views.push( this._formBuilder.group({ id: this._formBuilder.control(view.id, Validators.required), - partId: this._formBuilder.control(view.partId, Validators.required), - position: this._formBuilder.control(view.position), - activateView: this._formBuilder.control(view.activateView), - activatePart: this._formBuilder.control(view.activatePart), + options: this._formBuilder.group({ + partId: this._formBuilder.control(view.options.partId, Validators.required), + position: this._formBuilder.control(view.options.position), + cssClass: this._formBuilder.control(view.options.cssClass), + activateView: this._formBuilder.control(view.options.activateView), + activatePart: this._formBuilder.control(view.options.activatePart), + }), }), {emitEvent: options?.emitEvent ?? true}); } @@ -99,9 +106,9 @@ export class PerspectivePageViewsComponent implements ControlValueAccessor, Vali * Method implemented as part of `ControlValueAccessor` to work with Angular forms API * @docs-private */ - public writeValue(value: PerspectivePageViewEntry[] | undefined | null): void { + public writeValue(views: ViewDescriptor[] | undefined | null): void { this.form.controls.views.clear({emitEvent: false}); - value?.forEach(view => this.addViewEntry(view, {emitEvent: false})); + views?.forEach(view => this.addView(view, {emitEvent: false})); } /** @@ -129,10 +136,17 @@ export class PerspectivePageViewsComponent implements ControlValueAccessor, Vali } } -export type PerspectivePageViewEntry = { +export interface ViewDescriptor { id: string; - partId: string; - position?: number; - activateView?: boolean; - activatePart?: boolean; -}; + options: { + partId: string; + position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; + activateView?: boolean; + activatePart?: boolean; + cssClass?: string | string[]; + }; +} + +function arrayAttribute(proposals: string[] | null | undefined): string[] { + return proposals ?? []; +} diff --git a/apps/workbench-testing-app/src/app/layout-page/tables/navigate-views/navigate-views.component.html b/apps/workbench-testing-app/src/app/layout-page/tables/navigate-views/navigate-views.component.html new file mode 100644 index 000000000..22b1e1ace --- /dev/null +++ b/apps/workbench-testing-app/src/app/layout-page/tables/navigate-views/navigate-views.component.html @@ -0,0 +1,29 @@ +
+ View ID + Path + Hint + State + CSS Class(es) + + + @for (navigationFormGroup of form.controls.navigations.controls; track $index) { + + + + + + + + + + + + + } + + + + @for (view of viewProposals; track view) { + + } + diff --git a/apps/workbench-testing-app/src/app/layout-page/tables/navigate-views/navigate-views.component.scss b/apps/workbench-testing-app/src/app/layout-page/tables/navigate-views/navigate-views.component.scss new file mode 100644 index 000000000..67b063682 --- /dev/null +++ b/apps/workbench-testing-app/src/app/layout-page/tables/navigate-views/navigate-views.component.scss @@ -0,0 +1,16 @@ +@use '@scion/components.internal/design' as sci-design; + +:host { + display: grid; + + > form { + display: grid; + grid-template-columns: 7.5em 1fr 10em 14em 10em auto; + gap: .5em .75em; + align-items: center; + + > input { + @include sci-design.style-input-field(); + } + } +} diff --git a/apps/workbench-testing-app/src/app/layout-page/tables/navigate-views/navigate-views.component.ts b/apps/workbench-testing-app/src/app/layout-page/tables/navigate-views/navigate-views.component.ts new file mode 100644 index 000000000..276c75fce --- /dev/null +++ b/apps/workbench-testing-app/src/app/layout-page/tables/navigate-views/navigate-views.component.ts @@ -0,0 +1,151 @@ +/* + * 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 + */ + +import {Component, forwardRef, Input} from '@angular/core'; +import {noop} from 'rxjs'; +import {AbstractControl, ControlValueAccessor, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, NonNullableFormBuilder, ReactiveFormsModule, ValidationErrors, Validator, Validators} from '@angular/forms'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; +import {SciFormFieldComponent} from '@scion/components.internal/form-field'; +import {SciMaterialIconDirective} from '@scion/components.internal/material-icon'; +import {Commands, ViewState} from '@scion/workbench'; +import {RouterCommandsComponent} from '../../../router-commands/router-commands.component'; +import {NavigationStateComponent} from '../../../navigation-state/navigation-state.component'; +import {CssClassComponent} from '../../../css-class/css-class.component'; +import {UUID} from '@scion/toolkit/uuid'; + +@Component({ + selector: 'app-navigate-views', + templateUrl: './navigate-views.component.html', + styleUrls: ['./navigate-views.component.scss'], + standalone: true, + imports: [ + ReactiveFormsModule, + SciCheckboxComponent, + SciFormFieldComponent, + SciMaterialIconDirective, + RouterCommandsComponent, + NavigationStateComponent, + CssClassComponent, + ], + providers: [ + {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => NavigateViewsComponent)}, + {provide: NG_VALIDATORS, multi: true, useExisting: forwardRef(() => NavigateViewsComponent)}, + ], +}) +export class NavigateViewsComponent implements ControlValueAccessor, Validator { + + private _cvaChangeFn: (value: NavigationDescriptor[]) => void = noop; + private _cvaTouchedFn: () => void = noop; + + @Input({transform: arrayAttribute}) + public viewProposals: string[] = []; + + protected form = this._formBuilder.group({ + navigations: this._formBuilder.array; + commands: FormControl; + extras: FormGroup<{ + hint: FormControl; + state: FormControl; + cssClass: FormControl; + }>; + }>>([]), + }); + protected viewList = `view-list-${UUID.randomUUID()}`; + + constructor(private _formBuilder: NonNullableFormBuilder) { + this.form.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(() => { + this._cvaChangeFn(this.form.controls.navigations.controls.map(navigationFormGroup => ({ + id: navigationFormGroup.controls.id.value, + commands: navigationFormGroup.controls.commands.value, + extras: ({ + hint: navigationFormGroup.controls.extras.controls.hint.value || undefined, + state: navigationFormGroup.controls.extras.controls.state.value, + cssClass: navigationFormGroup.controls.extras.controls.cssClass.value, + }), + }))); + this._cvaTouchedFn(); + }); + } + + protected onAddNavigation(): void { + this.addNavigation({ + id: '', + commands: [], + }); + } + + protected onRemoveNavigation(index: number): void { + this.form.controls.navigations.removeAt(index); + } + + private addNavigation(navigation: NavigationDescriptor, options?: {emitEvent?: boolean}): void { + this.form.controls.navigations.push( + this._formBuilder.group({ + id: this._formBuilder.control(navigation.id, Validators.required), + commands: this._formBuilder.control(navigation.commands), + extras: this._formBuilder.group({ + hint: this._formBuilder.control(navigation.extras?.hint), + state: this._formBuilder.control(navigation.extras?.state), + cssClass: this._formBuilder.control(undefined), + }), + }), {emitEvent: options?.emitEvent ?? true}); + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public writeValue(navigations: NavigationDescriptor[] | undefined | null): void { + this.form.controls.navigations.clear({emitEvent: false}); + navigations?.forEach(navigation => this.addNavigation(navigation, {emitEvent: false})); + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public registerOnChange(fn: any): void { + this._cvaChangeFn = fn; + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public registerOnTouched(fn: any): void { + this._cvaTouchedFn = fn; + } + + /** + * Method implemented as part of `Validator` to work with Angular forms API + * @docs-private + */ + public validate(control: AbstractControl): ValidationErrors | null { + return this.form.controls.navigations.valid ? null : {valid: false}; + } +} + +export interface NavigationDescriptor { + id: string; + commands: Commands; + extras?: { + hint?: string; + state?: ViewState; + cssClass?: string | string[]; + }; +} + +function arrayAttribute(proposals: string[] | null | undefined): string[] { + return proposals ?? []; +} diff --git a/apps/workbench-testing-app/src/app/menu/menu-item.ts b/apps/workbench-testing-app/src/app/menu/menu-item.ts index eb431edc6..bc9f7a497 100644 --- a/apps/workbench-testing-app/src/app/menu/menu-item.ts +++ b/apps/workbench-testing-app/src/app/menu/menu-item.ts @@ -29,7 +29,7 @@ export class MenuItem { */ public checked?: boolean; /** - * Specifies CSS class(es) to be added to the menu item, useful in end-to-end tests for locating the menu item. + * Specifies CSS class(es) to add to the menu item, e.g., to locate the menu item in tests. */ public cssClass?: string | string[]; diff --git a/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html b/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html index a944492d9..298063e64 100644 --- a/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html +++ b/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.html @@ -47,7 +47,7 @@
- +
diff --git a/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.ts b/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.ts index 205c4664b..e4ea65f7b 100644 --- a/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.ts +++ b/apps/workbench-testing-app/src/app/message-box-opener-page/message-box-opener-page.component.ts @@ -17,6 +17,7 @@ import {KeyValueEntry, SciKeyValueFieldComponent} from '@scion/components.intern import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; import {MessageBoxPageComponent} from '../message-box-page/message-box-page.component'; +import {CssClassComponent} from '../css-class/css-class.component'; @Component({ selector: 'app-message-box-opener-page', @@ -29,6 +30,7 @@ import {MessageBoxPageComponent} from '../message-box-page/message-box-page.comp SciFormFieldComponent, SciKeyValueFieldComponent, SciCheckboxComponent, + CssClassComponent, ], }) export default class MessageBoxOpenerPageComponent { @@ -43,7 +45,7 @@ export default class MessageBoxOpenerPageComponent { modality: this._formBuilder.control<'application' | 'view' | ''>(''), contentSelectable: this._formBuilder.control(false), inputs: this._formBuilder.array>([]), - cssClass: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), }), }); @@ -71,7 +73,7 @@ export default class MessageBoxOpenerPageComponent { modality: this.form.controls.options.controls.modality.value || undefined, contentSelectable: this.form.controls.options.controls.contentSelectable.value || undefined, inputs: SciKeyValueFieldComponent.toDictionary(this.form.controls.options.controls.inputs) ?? undefined, - cssClass: this.form.controls.options.controls.cssClass.value.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.options.controls.cssClass.value, }; if (this.isUseComponent()) { diff --git a/apps/workbench-testing-app/src/app/navigation-state/navigation-state.component.html b/apps/workbench-testing-app/src/app/navigation-state/navigation-state.component.html new file mode 100644 index 000000000..37df287d0 --- /dev/null +++ b/apps/workbench-testing-app/src/app/navigation-state/navigation-state.component.html @@ -0,0 +1 @@ + diff --git a/apps/workbench-testing-app/src/app/navigation-state/navigation-state.component.scss b/apps/workbench-testing-app/src/app/navigation-state/navigation-state.component.scss new file mode 100644 index 000000000..85980ba98 --- /dev/null +++ b/apps/workbench-testing-app/src/app/navigation-state/navigation-state.component.scss @@ -0,0 +1,10 @@ +@use '@scion/components.internal/design' as sci-design; + +:host { + display: inline-grid; + + > input { + @include sci-design.style-input-field(); + min-width: 0; + } +} diff --git a/apps/workbench-testing-app/src/app/navigation-state/navigation-state.component.ts b/apps/workbench-testing-app/src/app/navigation-state/navigation-state.component.ts new file mode 100644 index 000000000..87cc7645e --- /dev/null +++ b/apps/workbench-testing-app/src/app/navigation-state/navigation-state.component.ts @@ -0,0 +1,84 @@ +/* + * 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 + */ + +import {Component, forwardRef} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR, NonNullableFormBuilder, ReactiveFormsModule} from '@angular/forms'; +import {ViewState} from '@scion/workbench'; +import {noop} from 'rxjs'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; + +@Component({ + selector: 'app-navigation-state', + templateUrl: './navigation-state.component.html', + styleUrls: ['./navigation-state.component.scss'], + standalone: true, + imports: [ + ReactiveFormsModule, + ], + providers: [ + {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => NavigationStateComponent)}, + ], +}) +export class NavigationStateComponent implements ControlValueAccessor { + + private _cvaChangeFn: (state: ViewState | undefined) => void = noop; + private _cvaTouchedFn: () => void = noop; + + protected formControl = this._formBuilder.control(''); + + constructor(private _formBuilder: NonNullableFormBuilder) { + this.formControl.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(() => { + this._cvaChangeFn(this.parse(this.formControl.value)); + this._cvaTouchedFn(); + }); + } + + private parse(stringified: string): ViewState | undefined { + if (!stringified.length) { + return undefined; + } + const state: ViewState = {}; + for (const match of stringified.matchAll(/(?[^=;]+)=(?[^;]+)/g)) { + const {key, value} = match.groups!; + state[key] = value; + } + return state; + } + + private stringify(state: ViewState | null | undefined): string { + return Object.entries(state ?? {}).map(([key, value]) => `${key}=${value}`).join(';'); + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public writeValue(state: ViewState | undefined | null): void { + this.formControl.setValue(this.stringify(state), {emitEvent: false}); + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public registerOnChange(fn: any): void { + this._cvaChangeFn = fn; + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public registerOnTouched(fn: any): void { + this._cvaTouchedFn = fn; + } +} diff --git a/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.html b/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.html index 5f5026033..9e650334e 100644 --- a/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.html +++ b/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.html @@ -31,8 +31,8 @@ - - + + @@ -42,7 +42,7 @@ - +
diff --git a/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.ts b/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.ts index 7e8902a4c..682ad2f9c 100644 --- a/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.ts +++ b/apps/workbench-testing-app/src/app/notification-opener-page/notification-opener-page.component.ts @@ -16,6 +16,8 @@ import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {stringifyError} from '../common/stringify-error.util'; import {NotificationPageComponent} from '../notification-page/notification-page.component'; +import {CssClassComponent} from '../css-class/css-class.component'; +import {UUID} from '@scion/toolkit/uuid'; @Component({ selector: 'app-notification-opener-page', @@ -27,6 +29,7 @@ import {NotificationPageComponent} from '../notification-page/notification-page. ReactiveFormsModule, SciFormFieldComponent, SciCheckboxComponent, + CssClassComponent, ], }) export default class NotificationOpenerPageComponent { @@ -42,9 +45,11 @@ export default class NotificationOpenerPageComponent { duration: this._formBuilder.control<'short' | 'medium' | 'long' | 'infinite' | '' | number>(''), group: this._formBuilder.control(''), useGroupInputReducer: this._formBuilder.control(false), - cssClass: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), }); + public durationList = `duration-list-${UUID.randomUUID()}`; + constructor(private _formBuilder: NonNullableFormBuilder, private _notificationService: NotificationService) { } @@ -59,7 +64,7 @@ export default class NotificationOpenerPageComponent { duration: this.parseDurationFromUI(), group: this.form.controls.group.value || undefined, groupInputReduceFn: this.isUseGroupInputReducer() ? concatInput : undefined, - cssClass: this.form.controls.cssClass.value.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.cssClass.value, }); } catch (error) { diff --git a/apps/workbench-testing-app/src/app/notification-page/notification-page.component.html b/apps/workbench-testing-app/src/app/notification-page/notification-page.component.html index 4558845ef..ab2aae4f5 100644 --- a/apps/workbench-testing-app/src/app/notification-page/notification-page.component.html +++ b/apps/workbench-testing-app/src/app/notification-page/notification-page.component.html @@ -20,8 +20,8 @@ - - + + @@ -30,6 +30,6 @@ - +
diff --git a/apps/workbench-testing-app/src/app/notification-page/notification-page.component.ts b/apps/workbench-testing-app/src/app/notification-page/notification-page.component.ts index c32df828e..63166c98c 100644 --- a/apps/workbench-testing-app/src/app/notification-page/notification-page.component.ts +++ b/apps/workbench-testing-app/src/app/notification-page/notification-page.component.ts @@ -17,6 +17,8 @@ import {StringifyPipe} from '../common/stringify.pipe'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {filter} from 'rxjs/operators'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; +import {CssClassComponent} from '../css-class/css-class.component'; +import {UUID} from '@scion/toolkit/uuid'; @Component({ selector: 'app-notification-page', @@ -29,6 +31,7 @@ import {SciFormFieldComponent} from '@scion/components.internal/form-field'; StringifyPipe, SciFormFieldComponent, SciViewportComponent, + CssClassComponent, ], }) export class NotificationPageComponent { @@ -37,9 +40,11 @@ export class NotificationPageComponent { title: this._formBuilder.control(''), severity: this._formBuilder.control<'info' | 'warn' | 'error' | undefined>(undefined), duration: this._formBuilder.control<'short' | 'medium' | 'long' | 'infinite' | number | undefined>(undefined), - cssClass: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), }); + public durationList = `duration-list-${UUID.randomUUID()}`; + constructor(public notification: Notification>, private _formBuilder: NonNullableFormBuilder) { this.form.controls.title.valueChanges .pipe(takeUntilDestroyed()) @@ -68,7 +73,7 @@ export class NotificationPageComponent { this.form.controls.cssClass.valueChanges .pipe(takeUntilDestroyed()) .subscribe(cssClass => { - this.notification.setCssClass(cssClass.split(/\s+/).filter(Boolean)); + this.notification.setCssClass(cssClass ?? []); }); } diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.html b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.html deleted file mode 100644 index 622f11c47..000000000 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.html +++ /dev/null @@ -1,46 +0,0 @@ -
- -
- Part ID - RelativeTo - Align - Ratio - Activate - -
- - - - -
- - - - - - - - - - - - - - - -
-
-
- - - - - - - - diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.scss b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.scss deleted file mode 100644 index 300f80dce..000000000 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.scss +++ /dev/null @@ -1,22 +0,0 @@ -:host { - > form { - display: flex; - flex-direction: column; - gap: 1em; - - div.parts { - display: grid; - grid-template-columns: repeat(4, 1fr) min-content auto; - gap: .5em .75em; - align-items: center; - - > span.checkbox { - text-align: center; - } - - > sci-checkbox { - justify-self: center; - } - } - } -} diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.ts b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.ts deleted file mode 100644 index 51ab2d63e..000000000 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-parts/perspective-page-parts.component.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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 - */ - -import {Component, forwardRef} from '@angular/core'; -import {CommonModule} from '@angular/common'; -import {AbstractControl, ControlValueAccessor, FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, NonNullableFormBuilder, ReactiveFormsModule, ValidationErrors, Validator, Validators} from '@angular/forms'; -import {SciFormFieldComponent} from '@scion/components.internal/form-field'; -import {noop} from 'rxjs'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; -import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; -import {MAIN_AREA} from '@scion/workbench'; -import {SciMaterialIconDirective} from '@scion/components.internal/material-icon'; - -@Component({ - selector: 'app-perspective-page-parts', - templateUrl: './perspective-page-parts.component.html', - styleUrls: ['./perspective-page-parts.component.scss'], - standalone: true, - imports: [ - CommonModule, - ReactiveFormsModule, - SciFormFieldComponent, - SciCheckboxComponent, - SciMaterialIconDirective - ], - providers: [ - {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => PerspectivePagePartsComponent)}, - {provide: NG_VALIDATORS, multi: true, useExisting: forwardRef(() => PerspectivePagePartsComponent)}, - ], -}) -export class PerspectivePagePartsComponent implements ControlValueAccessor, Validator { - - private _cvaChangeFn: (value: PerspectivePagePartEntry[]) => void = noop; - private _cvaTouchedFn: () => void = noop; - - public form = this._formBuilder.group({ - parts: this._formBuilder.array; - relativeTo: FormControl; - align: FormControl<'left' | 'right' | 'top' | 'bottom' | undefined>; - ratio: FormControl; - activate: FormControl; - }>>([]), - }); - - public MAIN_AREA = MAIN_AREA; - - constructor(private _formBuilder: NonNullableFormBuilder) { - this.form.valueChanges - .pipe(takeUntilDestroyed()) - .subscribe(() => { - const parts: PerspectivePagePartEntry[] = this.form.controls.parts.controls.map(partFormGroup => ({ - id: partFormGroup.controls.id.value, - relativeTo: partFormGroup.controls.relativeTo.value, - align: partFormGroup.controls.align.value, - ratio: partFormGroup.controls.ratio.value, - activate: partFormGroup.controls.activate.value, - })); - this._cvaChangeFn(parts); - this._cvaTouchedFn(); - }); - } - - protected onAddPart(): void { - this.addPartEntry({ - id: '', - }); - } - - protected onClearParts(): void { - this.form.controls.parts.clear(); - } - - protected onRemovePart(index: number): void { - this.form.controls.parts.removeAt(index); - } - - private addPartEntry(part: PerspectivePagePartEntry, options?: {emitEvent?: boolean}): void { - const first = this.form.controls.parts.length === 0; - this.form.controls.parts.push( - this._formBuilder.group({ - id: this._formBuilder.control(part.id, Validators.required), - relativeTo: this._formBuilder.control({value: first ? undefined : part.relativeTo, disabled: first}), - align: this._formBuilder.control<'left' | 'right' | 'top' | 'bottom' | undefined>({value: first ? undefined : part.align, disabled: first}, first ? Validators.nullValidator : Validators.required), - ratio: this._formBuilder.control({value: first ? undefined : part.ratio, disabled: first}), - activate: part.activate, - }), {emitEvent: options?.emitEvent ?? true}); - } - - /** - * Method implemented as part of `ControlValueAccessor` to work with Angular forms API - * @docs-private - */ - public writeValue(value: PerspectivePagePartEntry[] | undefined | null): void { - this.form.controls.parts.clear({emitEvent: false}); - value?.forEach(part => this.addPartEntry(part, {emitEvent: false})); - } - - /** - * Method implemented as part of `ControlValueAccessor` to work with Angular forms API - * @docs-private - */ - public registerOnChange(fn: any): void { - this._cvaChangeFn = fn; - } - - /** - * Method implemented as part of `ControlValueAccessor` to work with Angular forms API - * @docs-private - */ - public registerOnTouched(fn: any): void { - this._cvaTouchedFn = fn; - } - - /** - * Method implemented as part of `Validator` to work with Angular forms API - * @docs-private - */ - public validate(control: AbstractControl): ValidationErrors | null { - return this.form.controls.parts.valid ? null : {valid: false}; - } -} - -export type PerspectivePagePartEntry = { - id: string | MAIN_AREA; - relativeTo?: string; - align?: 'left' | 'right' | 'top' | 'bottom'; - ratio?: number; - activate?: boolean; -}; diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.html b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.html deleted file mode 100644 index 2dd52047a..000000000 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.html +++ /dev/null @@ -1,37 +0,0 @@ -
- -
- View ID - Part ID - Position - Activate View - Activate Part - -
- - - - -
- - - - - - - - - - - - - - - -
-
-
- - - - diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.scss b/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.scss deleted file mode 100644 index 07e3a3274..000000000 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page-views/perspective-page-views.component.scss +++ /dev/null @@ -1,22 +0,0 @@ -:host { - > form { - display: flex; - flex-direction: column; - gap: 1em; - - div.views { - display: grid; - grid-template-columns: repeat(3, 1fr) min-content min-content auto; - gap: .5em .75em; - align-items: center; - - > span.checkbox { - text-align: center; - } - - > sci-checkbox { - justify-self: center; - } - } - } -} diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.html b/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.html deleted file mode 100644 index a8c88d53a..000000000 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.html +++ /dev/null @@ -1,33 +0,0 @@ -
-
- - - - - - - - - -
- -
- -
- -
- -
- - -
- - - Success - - - - {{registerError}} - diff --git a/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.ts b/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.ts deleted file mode 100644 index 1eb218a5b..000000000 --- a/apps/workbench-testing-app/src/app/perspective-page/perspective-page.component.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 - */ - -import {Component} from '@angular/core'; -import {FormGroup, NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; -import {WorkbenchLayout, WorkbenchLayoutFactory, WorkbenchLayoutFn, WorkbenchService} from '@scion/workbench'; -import {stringifyError} from '../common/stringify-error.util'; -import {KeyValueEntry, SciKeyValueFieldComponent} from '@scion/components.internal/key-value-field'; -import {SciFormFieldComponent} from '@scion/components.internal/form-field'; -import {NgIf} from '@angular/common'; -import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; -import {PerspectivePagePartEntry, PerspectivePagePartsComponent} from './perspective-page-parts/perspective-page-parts.component'; -import {PerspectivePageViewEntry, PerspectivePageViewsComponent} from './perspective-page-views/perspective-page-views.component'; - -@Component({ - selector: 'app-perspective-page', - templateUrl: './perspective-page.component.html', - styleUrls: ['./perspective-page.component.scss'], - standalone: true, - imports: [ - NgIf, - ReactiveFormsModule, - SciFormFieldComponent, - SciCheckboxComponent, - SciKeyValueFieldComponent, - PerspectivePagePartsComponent, - PerspectivePageViewsComponent, - ], -}) -export default class PerspectivePageComponent { - - public form = this._formBuilder.group({ - id: this._formBuilder.control('', Validators.required), - transient: this._formBuilder.control(undefined), - data: this._formBuilder.array>([]), - parts: this._formBuilder.control([], Validators.required), - views: this._formBuilder.control([]), - }); - public registerError: string | false | undefined; - - constructor(private _formBuilder: NonNullableFormBuilder, private _workbenchService: WorkbenchService) { - } - - public async onRegister(): Promise { - try { - await this._workbenchService.registerPerspective({ - id: this.form.controls.id.value, - transient: this.form.controls.transient.value || undefined, - data: SciKeyValueFieldComponent.toDictionary(this.form.controls.data) ?? undefined, - layout: this.createLayout(), - }); - this.registerError = false; - this.form.reset(); - this.form.setControl('data', this._formBuilder.array>([])); - } - catch (error) { - this.registerError = stringifyError(error); - } - } - - private createLayout(): WorkbenchLayoutFn { - // Capture form values, since the `layout` function is evaluated independently of the form life-cycle - const [initialPart, ...parts] = this.form.controls.parts.value; - const views = this.form.controls.views.value; - - return (factory: WorkbenchLayoutFactory): WorkbenchLayout => { - let layout = factory.addPart(initialPart.id, {activate: initialPart.activate}); - for (const part of parts) { - layout = layout.addPart(part.id, {relativeTo: part.relativeTo, align: part.align!, ratio: part.ratio}, {activate: part.activate}); - } - - for (const view of views) { - layout = layout.addView(view.id, {partId: view.partId, position: view.position, activateView: view.activateView, activatePart: view.activatePart}); - } - return layout; - }; - } -} diff --git a/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.html b/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.html index 15a632371..163160202 100644 --- a/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.html +++ b/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.html @@ -26,7 +26,7 @@ - + diff --git a/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.ts b/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.ts index e142a46d4..97162d2e8 100644 --- a/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.ts +++ b/apps/workbench-testing-app/src/app/popup-opener-page/popup-opener-page.component.ts @@ -10,7 +10,7 @@ import {Component, ElementRef, Type, ViewChild} from '@angular/core'; import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; -import {PopupService, PopupSize} from '@scion/workbench'; +import {PopupService, PopupSize, ViewId} from '@scion/workbench'; import {PopupPageComponent} from '../popup-page/popup-page.component'; import FocusTestPageComponent from '../test-pages/focus-test-page/focus-test-page.component'; import {map, startWith} from 'rxjs/operators'; @@ -28,6 +28,7 @@ import InputFieldTestPageComponent from '../test-pages/input-field-test-page/inp import DialogOpenerPageComponent from '../dialog-opener-page/dialog-opener-page.component'; import {Dictionaries} from '@scion/toolkit/util'; import {parseTypedString} from '../common/parse-typed-value.util'; +import {CssClassComponent} from '../css-class/css-class.component'; @Component({ selector: 'app-popup-opener-page', @@ -42,6 +43,7 @@ import {parseTypedString} from '../common/parse-typed-value.util'; SciAccordionItemDirective, SciCheckboxComponent, PopupPositionLabelPipe, + CssClassComponent, ], }) export default class PopupOpenerPageComponent { @@ -57,9 +59,9 @@ export default class PopupOpenerPageComponent { width: this._formBuilder.control(undefined), height: this._formBuilder.control(undefined), }), - contextualViewId: this._formBuilder.control(''), + contextualViewId: this._formBuilder.control(''), align: this._formBuilder.control<'east' | 'west' | 'north' | 'south' | ''>(''), - cssClass: this._formBuilder.control(''), + cssClass: this._formBuilder.control(undefined), input: this._formBuilder.control(''), closeStrategy: this._formBuilder.group({ onFocusLost: this._formBuilder.control(true), @@ -94,7 +96,7 @@ export default class PopupOpenerPageComponent { input: this.form.controls.input.value || undefined, anchor: this.form.controls.anchor.controls.position.value === 'element' ? this._openButton : this._popupOrigin$, align: this.form.controls.align.value || undefined, - cssClass: this.form.controls.cssClass.value.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.cssClass.value, closeStrategy: { onFocusLost: this.form.controls.closeStrategy.controls.onFocusLost.value, onEscape: this.form.controls.closeStrategy.controls.onEscape.value, diff --git a/apps/workbench-testing-app/src/app/router-commands/router-commands.component.html b/apps/workbench-testing-app/src/app/router-commands/router-commands.component.html new file mode 100644 index 000000000..b3aa2e4f6 --- /dev/null +++ b/apps/workbench-testing-app/src/app/router-commands/router-commands.component.html @@ -0,0 +1,7 @@ + + + + @for (route of routes; track route) { + + } + diff --git a/apps/workbench-testing-app/src/app/router-commands/router-commands.component.scss b/apps/workbench-testing-app/src/app/router-commands/router-commands.component.scss new file mode 100644 index 000000000..85980ba98 --- /dev/null +++ b/apps/workbench-testing-app/src/app/router-commands/router-commands.component.scss @@ -0,0 +1,10 @@ +@use '@scion/components.internal/design' as sci-design; + +:host { + display: inline-grid; + + > input { + @include sci-design.style-input-field(); + min-width: 0; + } +} diff --git a/apps/workbench-testing-app/src/app/router-commands/router-commands.component.ts b/apps/workbench-testing-app/src/app/router-commands/router-commands.component.ts new file mode 100644 index 000000000..5389deb88 --- /dev/null +++ b/apps/workbench-testing-app/src/app/router-commands/router-commands.component.ts @@ -0,0 +1,114 @@ +/* + * 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 + */ + +import {Component, forwardRef} from '@angular/core'; +import {PRIMARY_OUTLET, Router, Routes} from '@angular/router'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR, NonNullableFormBuilder, ReactiveFormsModule} from '@angular/forms'; +import {UUID} from '@scion/toolkit/uuid'; +import {Commands} from '@scion/workbench'; +import {noop} from 'rxjs'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; + +@Component({ + selector: 'app-router-commands', + templateUrl: './router-commands.component.html', + styleUrls: ['./router-commands.component.scss'], + standalone: true, + imports: [ + ReactiveFormsModule, + ], + providers: [ + {provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => RouterCommandsComponent)}, + ], +}) +export class RouterCommandsComponent implements ControlValueAccessor { + + private _cvaChangeFn: (commands: Commands) => void = noop; + private _cvaTouchedFn: () => void = noop; + + protected routes: Routes; + protected routeList = `route-list-${UUID.randomUUID()}`; + protected formControl = this._formBuilder.control(''); + + constructor(private _router: Router, private _formBuilder: NonNullableFormBuilder) { + this.routes = this._router.config; + + this.formControl.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(() => { + this._cvaChangeFn(this.parse(this.formControl.value)); + this._cvaTouchedFn(); + }); + } + + private parse(value: string): Commands { + if (value === '') { + return []; + } + if (value === '/') { + return ['/']; + } + + const urlTree = this._router.parseUrl(value); + const segmentGroup = urlTree.root.children[PRIMARY_OUTLET]; + if (!segmentGroup) { + return []; // path syntax error + } + + const commands = new Array(); + segmentGroup.segments.forEach(segment => { + if (segment.path) { + commands.push(segment.path); + } + if (Object.keys(segment.parameters).length) { + commands.push(segment.parameters); + } + }); + + if (value.startsWith('/')) { + commands.unshift('/'); + } + + return commands; + } + + private stringify(commands: Commands | null | undefined): string { + if (!commands || !commands.length) { + return ''; + } + + const urlTree = this._router.createUrlTree(commands); + return urlTree.root.children[PRIMARY_OUTLET].segments.join('/'); + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public writeValue(commands: Commands | undefined | null): void { + this.formControl.setValue(this.stringify(commands), {emitEvent: false}); + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public registerOnChange(fn: any): void { + this._cvaChangeFn = fn; + } + + /** + * Method implemented as part of `ControlValueAccessor` to work with Angular forms API + * @docs-private + */ + public registerOnTouched(fn: any): void { + this._cvaTouchedFn = fn; + } +} diff --git a/apps/workbench-testing-app/src/app/router-page/router-page.component.html b/apps/workbench-testing-app/src/app/router-page/router-page.component.html index e31ab2eff..5bc829787 100644 --- a/apps/workbench-testing-app/src/app/router-page/router-page.component.html +++ b/apps/workbench-testing-app/src/app/router-page/router-page.component.html @@ -3,14 +3,7 @@
Routing Command
- - - - - - - - + @@ -22,25 +15,31 @@
Navigation Extras
- - + + + + + + - - + + - - - + + + @@ -61,7 +60,7 @@ - + @@ -71,11 +70,15 @@ - + + Navigate via Router Link + diff --git a/apps/workbench-testing-app/src/app/router-page/router-page.component.ts b/apps/workbench-testing-app/src/app/router-page/router-page.component.ts index 6a486f2fb..cd53d050e 100644 --- a/apps/workbench-testing-app/src/app/router-page/router-page.component.ts +++ b/apps/workbench-testing-app/src/app/router-page/router-page.component.ts @@ -11,15 +11,18 @@ import {Component, Injector} from '@angular/core'; import {FormGroup, NonNullableFormBuilder, ReactiveFormsModule} from '@angular/forms'; import {WorkbenchNavigationExtras, WorkbenchRouter, WorkbenchRouterLinkDirective, WorkbenchService, WorkbenchView} from '@scion/workbench'; -import {Params, PRIMARY_OUTLET, Router, Routes} from '@angular/router'; import {coerceNumberProperty} from '@angular/cdk/coercion'; -import {BehaviorSubject, Observable, share} from 'rxjs'; -import {map} from 'rxjs/operators'; import {AsyncPipe, NgFor, NgIf, NgTemplateOutlet} from '@angular/common'; import {KeyValueEntry, SciKeyValueFieldComponent} from '@scion/components.internal/key-value-field'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; +import {SettingsService} from '../settings.service'; +import {stringifyError} from '../common/stringify-error.util'; +import {RouterCommandsComponent} from '../router-commands/router-commands.component'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {parseTypedObject} from '../common/parse-typed-value.util'; +import {CssClassComponent} from '../css-class/css-class.component'; +import {UUID} from '@scion/toolkit/uuid'; @Component({ selector: 'app-router-page', @@ -36,102 +39,93 @@ import {parseTypedObject} from '../common/parse-typed-value.util'; SciFormFieldComponent, SciKeyValueFieldComponent, SciCheckboxComponent, + RouterCommandsComponent, + CssClassComponent, ], }) export default class RouterPageComponent { - public form = this._formBuilder.group({ - path: this._formBuilder.control(''), - matrixParams: this._formBuilder.array>([]), + protected form = this._formBuilder.group({ + commands: this._formBuilder.control([]), state: this._formBuilder.array>([]), target: this._formBuilder.control(''), - blankPartId: this._formBuilder.control(''), - insertionIndex: this._formBuilder.control(''), + hint: this._formBuilder.control(''), + partId: this._formBuilder.control(''), + position: this._formBuilder.control(''), queryParams: this._formBuilder.array>([]), activate: this._formBuilder.control(undefined), close: this._formBuilder.control(undefined), - cssClass: this._formBuilder.control(undefined), + cssClass: this._formBuilder.control(undefined), viewContext: this._formBuilder.control(true), }); - public navigateError: string | undefined; + protected navigateError: string | undefined; - public routerLinkCommands$: Observable; - public navigationExtras$: Observable; - public routes: Routes; + protected nullViewInjector: Injector; + protected extras: WorkbenchNavigationExtras = {}; - public nullViewInjector: Injector; + protected targetList = `target-list-${UUID.randomUUID()}`; + protected partList = `part-list-${UUID.randomUUID()}`; + protected positionList = `position-list-${UUID.randomUUID()}`; constructor(private _formBuilder: NonNullableFormBuilder, injector: Injector, - private _router: Router, private _wbRouter: WorkbenchRouter, - public workbenchService: WorkbenchService) { - this.routerLinkCommands$ = this.form.valueChanges - .pipe( - map(() => this.constructNavigationCommands()), - share({connector: () => new BehaviorSubject(this.constructNavigationCommands())}), - ); - - this.navigationExtras$ = this.form.valueChanges - .pipe( - map(() => this.constructNavigationExtras()), - share({connector: () => new BehaviorSubject(this.constructNavigationExtras())}), - ); - - this.routes = this._router.config - .filter(route => !route.outlet || route.outlet === PRIMARY_OUTLET) - .filter(route => !route.path?.startsWith('~')); // microfrontend route prefix - + private _settingsService: SettingsService, + protected workbenchService: WorkbenchService) { this.nullViewInjector = Injector.create({ parent: injector, providers: [ {provide: WorkbenchView, useValue: undefined}, ], }); + + this.form.valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(() => { + this.extras = this.readExtrasFromUI(); + }); } - public onRouterNavigate(): void { + protected onRouterNavigate(): void { this.navigateError = undefined; - const commands: any[] = this.constructNavigationCommands(); - const extras: WorkbenchNavigationExtras = this.constructNavigationExtras(); - - this._wbRouter.navigate(commands, extras) + this._wbRouter.navigate(this.form.controls.commands.value, this.extras) .then(success => success ? Promise.resolve() : Promise.reject('Navigation failed')) - .catch(error => this.navigateError = error); + .then(() => this.resetForm()) + .catch(error => this.navigateError = stringifyError(error)); } - private constructNavigationCommands(): any[] { - const matrixParams: Params | null = SciKeyValueFieldComponent.toDictionary(this.form.controls.matrixParams); - const path = this.form.controls.path.value; - const commands: any[] = path === '' ? [] : path.split('/'); - - // When tokenizing the path into segments, an empty segment is created for the leading slash (if any). - if (path.startsWith('/')) { - commands[0] = '/'; - } - - return commands.concat(matrixParams ? matrixParams : []); + protected onRouterLinkNavigate(): void { + this.resetForm(); } - private constructNavigationExtras(): WorkbenchNavigationExtras { + private readExtrasFromUI(): WorkbenchNavigationExtras { return { queryParams: SciKeyValueFieldComponent.toDictionary(this.form.controls.queryParams), activate: this.form.controls.activate.value, close: this.form.controls.close.value, target: this.form.controls.target.value || undefined, - blankPartId: this.form.controls.blankPartId.value || undefined, - blankInsertionIndex: coerceInsertionIndex(this.form.controls.insertionIndex.value), + hint: this.form.controls.hint.value || undefined, + partId: this.form.controls.partId.value || undefined, + position: coercePosition(this.form.controls.position.value), state: parseTypedObject(SciKeyValueFieldComponent.toDictionary(this.form.controls.state)) ?? undefined, - cssClass: this.form.controls.cssClass.value?.split(/\s+/).filter(Boolean), + cssClass: this.form.controls.cssClass.value, }; } + + private resetForm(): void { + if (this._settingsService.isEnabled('resetFormsOnSubmit')) { + this.form.reset(); + this.form.setControl('queryParams', this._formBuilder.array>([])); + this.form.setControl('state', this._formBuilder.array>([])); + } + } } -function coerceInsertionIndex(value: any): number | 'start' | 'end' | undefined { +function coercePosition(value: any): number | 'start' | 'end' | undefined { if (value === '') { return undefined; } - if (value === 'start' || value === 'end' || value === undefined) { + if (value === 'start' || value === 'end' || value === 'before-active-view' || value === 'after-active-view' || value === undefined) { return value; } return coerceNumberProperty(value); diff --git a/apps/workbench-testing-app/src/app/settings.service.ts b/apps/workbench-testing-app/src/app/settings.service.ts index e391cbca8..603526727 100644 --- a/apps/workbench-testing-app/src/app/settings.service.ts +++ b/apps/workbench-testing-app/src/app/settings.service.ts @@ -55,6 +55,10 @@ export class SettingsService { * Settings of the workbench testing application. */ const SETTINGS = { + resetFormsOnSubmit: { + default: true, + storageKey: 'scion.workbench.testing-app.settings.reset-forms-on-submit', + }, logAngularChangeDetectionCycles: { default: environment.logAngularChangeDetectionCycles, storageKey: 'scion.workbench.testing-app.settings.log-angular-change-detection-cycles', diff --git a/apps/workbench-testing-app/src/app/start-page/start-page.component.html b/apps/workbench-testing-app/src/app/start-page/start-page.component.html index d0193992c..cb4fb71ac 100644 --- a/apps/workbench-testing-app/src/app/start-page/start-page.component.html +++ b/apps/workbench-testing-app/src/app/start-page/start-page.component.html @@ -1,8 +1,8 @@ -Welcome to the internal test app of SCION Workbench and SCION Workbench Client. -We use this app to run our e2e tests and experiment with features. +Welcome to the SCION Workbench Playground. +We use this application to experiment with features and run our end-to-end tests. @@ -10,8 +10,9 @@
+ (click)="onViewOpen(route.path!, $event)" + [ngClass]="route.data![WorkbenchRouteData.cssClass]" + href=""> {{route.data![WorkbenchRouteData.title]}} {{route.data![WorkbenchRouteData.heading]}} diff --git a/apps/workbench-testing-app/src/app/start-page/start-page.component.ts b/apps/workbench-testing-app/src/app/start-page/start-page.component.ts index 94306878a..1da7e9e3c 100644 --- a/apps/workbench-testing-app/src/app/start-page/start-page.component.ts +++ b/apps/workbench-testing-app/src/app/start-page/start-page.component.ts @@ -8,11 +8,11 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Optional, ViewChild} from '@angular/core'; -import {WorkbenchModuleConfig, WorkbenchRouteData, WorkbenchRouterLinkDirective, WorkbenchView} from '@scion/workbench'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, HostListener, Optional, ViewChild} from '@angular/core'; +import {WorkbenchConfig, WorkbenchRouteData, WorkbenchRouter, WorkbenchRouterLinkDirective, WorkbenchService, WorkbenchView} from '@scion/workbench'; import {Capability, IntentClient, ManifestService} from '@scion/microfrontend-platform'; import {Observable, of} from 'rxjs'; -import {WorkbenchCapabilities, WorkbenchPopupService, WorkbenchRouter, WorkbenchViewCapability} from '@scion/workbench-client'; +import {WorkbenchCapabilities, WorkbenchPopupService as WorkbenchClientPopupService, WorkbenchRouter as WorkbenchClientRouter, WorkbenchViewCapability} from '@scion/workbench-client'; import {filterArray, sortArray} from '@scion/toolkit/operators'; import {NavigationEnd, PRIMARY_OUTLET, Route, Router, Routes} from '@angular/router'; import {filter} from 'rxjs/operators'; @@ -51,6 +51,9 @@ export default class StartPageComponent { @ViewChild(SciFilterFieldComponent) private _filterField!: SciFilterFieldComponent; + @HostBinding('attr.data-partid') + public partId: string | undefined; + public filterControl = this._formBuilder.control(''); public workbenchViewRoutes$: Observable; public microfrontendViewCapabilities$: Observable | undefined; @@ -58,34 +61,36 @@ export default class StartPageComponent { public WorkbenchRouteData = WorkbenchRouteData; - constructor(@Optional() private _view: WorkbenchView, // not available on entry point page - @Optional() private _workbenchClientRouter: WorkbenchRouter, // not available when starting the workbench standalone - @Optional() private _workbenchPopupService: WorkbenchPopupService, // not available when starting the workbench standalone - @Optional() private _intentClient: IntentClient, // not available when starting the workbench standalone - @Optional() private _manifestService: ManifestService, // not available when starting the workbench standalone + constructor(@Optional() private _view: WorkbenchView | null, // not available on entry point page + @Optional() private _workbenchClientRouter: WorkbenchClientRouter | null, // not available when starting the workbench standalone + @Optional() private _intentClient: IntentClient | null, // not available when starting the workbench standalone + @Optional() private _manifestService: ManifestService | null, // not available when starting the workbench standalone + @Optional() private _workbenchClientPopupService: WorkbenchClientPopupService | null, // not available when starting the workbench standalone + private _workbenchService: WorkbenchService, + private _workbenchRouter: WorkbenchRouter, private _formBuilder: NonNullableFormBuilder, router: Router, cd: ChangeDetectorRef, - workbenchModuleConfig: WorkbenchModuleConfig) { + workbenchConfig: WorkbenchConfig) { // Read workbench views to be pinned to the start page. this.workbenchViewRoutes$ = of(router.config) .pipe(filterArray(route => { - if ((!route.outlet || route.outlet === PRIMARY_OUTLET) && route.data) { - return route.data['pinToStartPage'] === true; + if ((!route.outlet || route.outlet === PRIMARY_OUTLET)) { + return route.data?.['pinToStartPage'] === true; } return false; })); - if (workbenchModuleConfig.microfrontendPlatform) { + if (workbenchConfig.microfrontendPlatform) { // Read microfrontend views to be pinned to the start page. - this.microfrontendViewCapabilities$ = this._manifestService.lookupCapabilities$({type: WorkbenchCapabilities.View}) + this.microfrontendViewCapabilities$ = this._manifestService!.lookupCapabilities$({type: WorkbenchCapabilities.View}) .pipe( filterArray(viewCapability => 'pinToStartPage' in viewCapability.properties && !!viewCapability.properties['pinToStartPage']), filterArray(viewCapability => !isTestCapability(viewCapability)), sortArray((a, b) => a.metadata!.appSymbolicName.localeCompare(b.metadata!.appSymbolicName)), ); // Read test capabilities to be pinned to the start page. - this.testCapabilities$ = this._manifestService.lookupCapabilities$() + this.testCapabilities$ = this._manifestService!.lookupCapabilities$() .pipe( filterArray(capability => !!capability.properties && 'pinToStartPage' in capability.properties && !!capability.properties['pinToStartPage']), filterArray(viewCapability => isTestCapability(viewCapability)), @@ -95,12 +100,21 @@ export default class StartPageComponent { this.markForCheckOnUrlChange(router, cd); this.installFilterFieldDisplayTextSynchronizer(); + this.memoizeContextualPart(); + } + + public onViewOpen(path: string, event: MouseEvent): void { + event.preventDefault(); // Prevent href navigation imposed by accessibility rules + this._workbenchRouter.navigate([path], { + target: event.ctrlKey ? 'blank' : this._view?.id ?? 'blank', + activate: !event.ctrlKey, + }).then(); } public onMicrofrontendViewOpen(viewCapability: WorkbenchViewCapability, event: MouseEvent): void { event.preventDefault(); // Prevent href navigation imposed by accessibility rules - this._workbenchClientRouter.navigate(viewCapability.qualifier, { - target: event.ctrlKey ? 'blank' : this._view?.id, + this._workbenchClientRouter!.navigate(viewCapability.qualifier, { + target: event.ctrlKey ? 'blank' : this._view?.id ?? 'blank', activate: !event.ctrlKey, }).then(); } @@ -110,15 +124,15 @@ export default class StartPageComponent { // TODO [#343] Remove switch-case after fixed issue https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/343 switch (testCapability.type) { case WorkbenchCapabilities.View: { - await this._workbenchClientRouter.navigate(testCapability.qualifier!, {target: this._view?.id}); + await this._workbenchClientRouter!.navigate(testCapability.qualifier!, {target: this._view?.id}); break; } case WorkbenchCapabilities.Popup: { - await this._workbenchPopupService.open(testCapability.qualifier!, {anchor: event}); + await this._workbenchClientPopupService!.open(testCapability.qualifier!, {anchor: event}); break; } default: { - await this._intentClient.publish({type: testCapability.type, qualifier: testCapability.qualifier}); + await this._intentClient!.publish({type: testCapability.type, qualifier: testCapability.qualifier}); break; } } @@ -158,6 +172,17 @@ export default class StartPageComponent { }); } + /** + * Memoizes the part in which this component is displayed. + */ + private memoizeContextualPart(): void { + this._workbenchService.layout$ + .pipe(takeUntilDestroyed()) + .subscribe(() => { + this.partId = this._view?.part.id ?? this._workbenchService.parts.filter(part => part.active).sort(a => a.isInMainArea ? -1 : 1).at(0)?.id; + }); + } + public selectViewCapabilityText = (viewCapability: WorkbenchViewCapability): string | undefined => { return viewCapability.properties!.title; }; diff --git a/apps/workbench-testing-app/src/app/test-pages/angular-router-test-page/angular-router-test-page.component.html b/apps/workbench-testing-app/src/app/test-pages/angular-router-test-page/angular-router-test-page.component.html index b91b8ab04..99a9cd87b 100644 --- a/apps/workbench-testing-app/src/app/test-pages/angular-router-test-page/angular-router-test-page.component.html +++ b/apps/workbench-testing-app/src/app/test-pages/angular-router-test-page/angular-router-test-page.component.html @@ -1,7 +1,7 @@
- + diff --git a/apps/workbench-testing-app/src/app/test-pages/angular-router-test-page/angular-router-test-page.component.ts b/apps/workbench-testing-app/src/app/test-pages/angular-router-test-page/angular-router-test-page.component.ts index b6bc05ffe..87f5c287e 100644 --- a/apps/workbench-testing-app/src/app/test-pages/angular-router-test-page/angular-router-test-page.component.ts +++ b/apps/workbench-testing-app/src/app/test-pages/angular-router-test-page/angular-router-test-page.component.ts @@ -12,7 +12,9 @@ import {Component} from '@angular/core'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {Router} from '@angular/router'; import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; +import {RouterCommandsComponent} from '../../router-commands/router-commands.component'; import {stringifyError} from '../../common/stringify-error.util'; +import {SettingsService} from '../../settings.service'; import {Commands} from '@scion/workbench'; @Component({ @@ -22,32 +24,41 @@ import {Commands} from '@scion/workbench'; standalone: true, imports: [ SciFormFieldComponent, + RouterCommandsComponent, ReactiveFormsModule, ], }) export default class AngularRouterTestPageComponent { public form = this._formBuilder.group({ - path: this._formBuilder.control('', Validators.required), + commands: this._formBuilder.control([], Validators.required), outlet: this._formBuilder.control('', Validators.required), }); public navigateError: string | undefined; - constructor(private _router: Router, private _formBuilder: NonNullableFormBuilder) { + constructor(private _router: Router, + private _settingsService: SettingsService, + private _formBuilder: NonNullableFormBuilder) { } public onNavigate(): void { const commands: Commands = [{ outlets: { - [this.form.controls.outlet.value]: [this.form.controls.path.value], + [this.form.controls.outlet.value]: this.form.controls.commands.value, }, }]; this.navigateError = undefined; this._router.navigate(commands) .then(success => success ? Promise.resolve() : Promise.reject('Navigation failed')) - .then(() => this.form.reset()) + .then(() => this.resetForm()) .catch(error => this.navigateError = stringifyError(error)); } + + private resetForm(): void { + if (this._settingsService.isEnabled('resetFormsOnSubmit')) { + this.form.reset(); + } + } } diff --git a/apps/workbench-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.html b/apps/workbench-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.html index 2958909b1..dac957f23 100644 --- a/apps/workbench-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.html +++ b/apps/workbench-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.html @@ -3,8 +3,8 @@ - - + + diff --git a/apps/workbench-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.ts b/apps/workbench-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.ts index 7e4e84fca..ab1f5760e 100644 --- a/apps/workbench-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.ts +++ b/apps/workbench-testing-app/src/app/test-pages/bulk-navigation-test-page/bulk-navigation-test-page.component.ts @@ -12,6 +12,7 @@ import {Component} from '@angular/core'; import {WorkbenchRouter} from '@scion/workbench'; import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/forms'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; +import {CssClassComponent} from '../../css-class/css-class.component'; @Component({ selector: 'app-bulk-navigation-test-page', @@ -21,13 +22,14 @@ import {SciFormFieldComponent} from '@scion/components.internal/form-field'; imports: [ SciFormFieldComponent, ReactiveFormsModule, + CssClassComponent, ], }) export default class BulkNavigationTestPageComponent { public form = this._formBuilder.group({ viewCount: this._formBuilder.control(1, Validators.required), - cssClass: this._formBuilder.control('', Validators.required), + cssClass: this._formBuilder.control(undefined, Validators.required), }); constructor(private _formBuilder: NonNullableFormBuilder, private _router: WorkbenchRouter) { diff --git a/apps/workbench-testing-app/src/app/test-pages/view-move-dialog-test-page/view-move-dialog-test-page.component.html b/apps/workbench-testing-app/src/app/test-pages/view-move-dialog-test-page/view-move-dialog-test-page.component.html index c54dd726e..8ba5cfec9 100644 --- a/apps/workbench-testing-app/src/app/test-pages/view-move-dialog-test-page/view-move-dialog-test-page.component.html +++ b/apps/workbench-testing-app/src/app/test-pages/view-move-dialog-test-page/view-move-dialog-test-page.component.html @@ -1,8 +1,8 @@
- - + + diff --git a/apps/workbench-testing-app/src/app/test-pages/view-move-dialog-test-page/view-move-dialog-test-page.component.ts b/apps/workbench-testing-app/src/app/test-pages/view-move-dialog-test-page/view-move-dialog-test-page.component.ts index 85246913e..a4c2268b8 100644 --- a/apps/workbench-testing-app/src/app/test-pages/view-move-dialog-test-page/view-move-dialog-test-page.component.ts +++ b/apps/workbench-testing-app/src/app/test-pages/view-move-dialog-test-page/view-move-dialog-test-page.component.ts @@ -13,6 +13,7 @@ import {NonNullableFormBuilder, ReactiveFormsModule, Validators} from '@angular/ import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {WORKBENCH_ID, WorkbenchDialog, WorkbenchDialogActionDirective, WorkbenchView} from '@scion/workbench'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {UUID} from '@scion/toolkit/uuid'; @Component({ selector: 'app-view-move-dialog-test-page', @@ -36,9 +37,11 @@ export class ViewMoveDialogTestPageComponent { region: this._formBuilder.control<'north' | 'south' | 'west' | 'east' | ''>(''), }); + public targetList = `target-list-${UUID.randomUUID()}`; + constructor(private _formBuilder: NonNullableFormBuilder, private _dialog: WorkbenchDialog) { this._dialog.title = 'Move view'; - this.requirePartIfMovingToPart(); + this.requirePartIfMovingToExistingWindow(); } public onOk(): void { @@ -61,7 +64,7 @@ export class ViewMoveDialogTestPageComponent { /** * Makes the part a required field if not moving the view to a new window. */ - private requirePartIfMovingToPart(): void { + private requirePartIfMovingToExistingWindow(): void { this.form.controls.workbenchId.valueChanges .pipe(takeUntilDestroyed()) .subscribe(target => { diff --git a/apps/workbench-testing-app/src/app/view-info-dialog/view-info-dialog.component.html b/apps/workbench-testing-app/src/app/view-info-dialog/view-info-dialog.component.html index 017b0457e..7cfdec7ad 100644 --- a/apps/workbench-testing-app/src/app/view-info-dialog/view-info-dialog.component.html +++ b/apps/workbench-testing-app/src/app/view-info-dialog/view-info-dialog.component.html @@ -3,6 +3,10 @@ {{view.id}} + + {{view.alternativeId}} + + {{view.part.id}} @@ -15,6 +19,10 @@ {{view.heading}} + + {{view.navigationHint}} + + {{view.urlSegments | appJoin:'/'}} diff --git a/apps/workbench-testing-app/src/app/view-info-dialog/view-info-dialog.component.ts b/apps/workbench-testing-app/src/app/view-info-dialog/view-info-dialog.component.ts index 2c5507071..18750bad9 100644 --- a/apps/workbench-testing-app/src/app/view-info-dialog/view-info-dialog.component.ts +++ b/apps/workbench-testing-app/src/app/view-info-dialog/view-info-dialog.component.ts @@ -46,7 +46,7 @@ export class ViewInfoDialogComponent implements OnInit { public ngOnInit(): void { const route = this._router.routerState.root.children.find(route => route.outlet === this.view.id); - this.route = route && resolveActualRoute(route); + this.route = route && resolveEffectiveRoute(route); } public onClose(): void { @@ -54,6 +54,6 @@ export class ViewInfoDialogComponent implements OnInit { } } -function resolveActualRoute(route: ActivatedRoute): ActivatedRoute { - return route.firstChild ? resolveActualRoute(route.firstChild) : route; +function resolveEffectiveRoute(route: ActivatedRoute): ActivatedRoute { + return route.firstChild ? resolveEffectiveRoute(route.firstChild) : route; } diff --git a/apps/workbench-testing-app/src/app/view-page/view-page.component.html b/apps/workbench-testing-app/src/app/view-page/view-page.component.html index 536c8f191..ea7e98681 100644 --- a/apps/workbench-testing-app/src/app/view-page/view-page.component.html +++ b/apps/workbench-testing-app/src/app/view-page/view-page.component.html @@ -3,17 +3,25 @@ {{view.id}} - - {{view.part.id}} + + {{view.alternativeId}} - - {{uuid}} + + {{view.part.id}} {{view.urlSegments | appJoin:'/'}} + + + {{view.navigationHint}} + + + + {{uuid}} +
@@ -46,15 +54,19 @@ - + - + + + + + - +
@@ -73,7 +85,7 @@ diff --git a/apps/workbench-testing-app/src/app/view-page/view-page.component.ts b/apps/workbench-testing-app/src/app/view-page/view-page.component.ts index 0b0a0dfab..642929c8a 100644 --- a/apps/workbench-testing-app/src/app/view-page/view-page.component.ts +++ b/apps/workbench-testing-app/src/app/view-page/view-page.component.ts @@ -8,8 +8,8 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {Component} from '@angular/core'; -import {WorkbenchModule, WorkbenchRouteData, WorkbenchStartup, WorkbenchView} from '@scion/workbench'; +import {Component, inject} from '@angular/core'; +import {CanClose, WorkbenchMessageBoxService, WorkbenchPartActionDirective, WorkbenchRouteData, WorkbenchStartup, WorkbenchView} from '@scion/workbench'; import {Observable} from 'rxjs'; import {map, startWith} from 'rxjs/operators'; import {ActivatedRoute} from '@angular/router'; @@ -25,6 +25,7 @@ import {SciCheckboxComponent} from '@scion/components.internal/checkbox'; import {SciFormFieldComponent} from '@scion/components.internal/form-field'; import {SciAccordionComponent, SciAccordionItemDirective} from '@scion/components.internal/accordion'; import {AppendParamDataTypePipe} from '../common/append-param-data-type.pipe'; +import {CssClassComponent} from '../css-class/css-class.component'; @Component({ selector: 'app-view-page', @@ -38,7 +39,6 @@ import {AppendParamDataTypePipe} from '../common/append-param-data-type.pipe'; AsyncPipe, FormsModule, ReactiveFormsModule, - WorkbenchModule, SciFormFieldComponent, SciCheckboxComponent, SciAccordionComponent, @@ -47,14 +47,20 @@ import {AppendParamDataTypePipe} from '../common/append-param-data-type.pipe'; NullIfEmptyPipe, JoinPipe, AppendParamDataTypePipe, + CssClassComponent, + WorkbenchPartActionDirective, ], }) -export default class ViewPageComponent { +export default class ViewPageComponent implements CanClose { public uuid = UUID.randomUUID(); public partActions$: Observable; - public partActionsFormControl = this._formBuilder.control(''); - public cssClassFormControl = this._formBuilder.control(''); + + public formControls = { + partActions: this._formBuilder.control(''), + cssClass: this._formBuilder.control(''), + confirmClosing: this._formBuilder.control(false), + }; public WorkbenchRouteData = WorkbenchRouteData; @@ -66,7 +72,7 @@ export default class ViewPageComponent { throw Error('[LifecycleError] Component constructed before the workbench startup completed!'); // Do not remove as required by `startup.e2e-spec.ts` in [#1] } - this.partActions$ = this.partActionsFormControl.valueChanges + this.partActions$ = this.formControls.partActions.valueChanges .pipe( map(() => this.parsePartActions()), startWith(this.parsePartActions()), @@ -76,13 +82,30 @@ export default class ViewPageComponent { this.installCssClassUpdater(); } + public async canClose(): Promise { + if (!this.formControls.confirmClosing.value) { + return true; + } + + const action = await inject(WorkbenchMessageBoxService).open('Do you want to close this view?', { + actions: {yes: 'Yes', no: 'No', error: 'Throw Error'}, + cssClass: ['e2e-close-view', this.view.id], + modality: 'application', + }); + + if (action === 'error') { + throw Error(`[CanCloseSpecError] Error in CanLoad of view '${this.view.id}'.`); + } + return action === 'yes'; + } + private parsePartActions(): WorkbenchPartActionDescriptor[] { - if (!this.partActionsFormControl.value) { + if (!this.formControls.partActions.value) { return []; } try { - return Arrays.coerce(JSON.parse(this.partActionsFormControl.value)); + return Arrays.coerce(JSON.parse(this.formControls.partActions.value)); } catch { return []; @@ -103,10 +126,10 @@ export default class ViewPageComponent { } private installCssClassUpdater(): void { - this.cssClassFormControl.valueChanges + this.formControls.cssClass.valueChanges .pipe(takeUntilDestroyed()) .subscribe(cssClasses => { - this.view.cssClass = cssClasses.split(/\s+/).filter(Boolean); + this.view.cssClass = cssClasses; }); } } diff --git a/apps/workbench-testing-app/src/app/workbench.config.ts b/apps/workbench-testing-app/src/app/workbench.config.ts index d511c5dff..943d1474d 100644 --- a/apps/workbench-testing-app/src/app/workbench.config.ts +++ b/apps/workbench-testing-app/src/app/workbench.config.ts @@ -11,12 +11,12 @@ import {WorkbenchStartupQueryParams} from './workbench/workbench-startup-query-params'; import {environment} from '../environments/environment'; import {Perspectives} from './workbench.perspectives'; -import {WorkbenchModuleConfig} from '@scion/workbench'; +import {WorkbenchConfig} from '@scion/workbench'; /** * Configures SCION Workbench for the testing application. */ -export const workbenchModuleConfig: WorkbenchModuleConfig = { +export const workbenchConfig: WorkbenchConfig = { startup: { launcher: WorkbenchStartupQueryParams.launcher(), }, diff --git a/apps/workbench-testing-app/src/app/workbench.perspectives.ts b/apps/workbench-testing-app/src/app/workbench.perspectives.ts index 9040e7014..544967bf8 100644 --- a/apps/workbench-testing-app/src/app/workbench.perspectives.ts +++ b/apps/workbench-testing-app/src/app/workbench.perspectives.ts @@ -8,8 +8,8 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {ROUTES} from '@angular/router'; -import {MAIN_AREA, WorkbenchLayout, WorkbenchLayoutFactory, WorkbenchPerspectiveDefinition, WorkbenchRouteData} from '@scion/workbench'; +import {Routes, ROUTES} from '@angular/router'; +import {canMatchWorkbenchView, MAIN_AREA, WorkbenchLayout, WorkbenchLayoutFactory, WorkbenchPerspectiveDefinition, WorkbenchRouteData} from '@scion/workbench'; import {WorkbenchStartupQueryParams} from './workbench/workbench-startup-query-params'; import {EnvironmentProviders, makeEnvironmentProviders} from '@angular/core'; @@ -67,21 +67,21 @@ export const Perspectives = { provide: ROUTES, multi: true, useValue: [ - {path: '', outlet: 'navigator', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Navigator'}}, - {path: '', outlet: 'package-explorer', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Package Explorer'}}, - {path: '', outlet: 'git-repositories', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Git Repositories'}}, - {path: '', outlet: 'console', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Console'}}, - {path: '', outlet: 'problems', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Problems'}}, - {path: '', outlet: 'search', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Search'}}, - {path: '', outlet: 'outline', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Outline'}}, - {path: '', outlet: 'debug', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Debug'}}, - {path: '', outlet: 'expressions', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Expressions'}}, - {path: '', outlet: 'breakpoints', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Breakpoints'}}, - {path: '', outlet: 'variables', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Variables'}}, - {path: '', outlet: 'servers', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Servers'}}, - {path: '', outlet: 'progress', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Progress'}}, - {path: '', outlet: 'git-staging', loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Git Staging'}}, - ], + {path: '', canMatch: [canMatchWorkbenchView('navigator')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Navigator'}}, + {path: '', canMatch: [canMatchWorkbenchView('package-explorer')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Package Explorer'}}, + {path: '', canMatch: [canMatchWorkbenchView('git-repositories')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Git Repositories'}}, + {path: '', canMatch: [canMatchWorkbenchView('console')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Console'}}, + {path: '', canMatch: [canMatchWorkbenchView('problems')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Problems'}}, + {path: '', canMatch: [canMatchWorkbenchView('search')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Search'}}, + {path: '', canMatch: [canMatchWorkbenchView('outline')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Outline'}}, + {path: '', canMatch: [canMatchWorkbenchView('debug')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Debug'}}, + {path: '', canMatch: [canMatchWorkbenchView('expressions')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Expressions'}}, + {path: '', canMatch: [canMatchWorkbenchView('breakpoints')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Breakpoints'}}, + {path: '', canMatch: [canMatchWorkbenchView('variables')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Variables'}}, + {path: '', canMatch: [canMatchWorkbenchView('servers')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Servers'}}, + {path: '', canMatch: [canMatchWorkbenchView('progress')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Progress'}}, + {path: '', canMatch: [canMatchWorkbenchView('git-staging')], loadComponent: () => import('./view-page/view-page.component'), data: {[WorkbenchRouteData.title]: 'Git Staging'}}, + ] satisfies Routes, }, ]); }, @@ -106,6 +106,15 @@ function provideDeveloperPerspectiveLayout(factory: WorkbenchLayoutFactory): Wor .addView('search', {partId: 'bottom'}) .addView('progress', {partId: 'bottom'}) .addView('outline', {partId: 'right'}) + .navigateView('package-explorer', [], {hint: 'package-explorer'}) + .navigateView('navigator', [], {hint: 'navigator'}) + .navigateView('git-repositories', [], {hint: 'git-repositories'}) + .navigateView('problems', [], {hint: 'problems'}) + .navigateView('git-staging', [], {hint: 'git-staging'}) + .navigateView('console', [], {hint: 'console'}) + .navigateView('search', [], {hint: 'search'}) + .navigateView('progress', [], {hint: 'progress'}) + .navigateView('outline', [], {hint: 'outline'}) .activateView('package-explorer') .activateView('git-repositories') .activateView('console') @@ -127,7 +136,14 @@ function provideDebugPerspectiveLayout(factory: WorkbenchLayoutFactory): Workben .addView('variables', {partId: 'right'}) .addView('expressions', {partId: 'right'}) .addView('breakpoints', {partId: 'right'}) - .addPart('findArea', {align: 'right', ratio: .25}) + .navigateView('debug', [], {hint: 'debug'}) + .navigateView('package-explorer', [], {hint: 'package-explorer'}) + .navigateView('servers', [], {hint: 'servers'}) + .navigateView('console', [], {hint: 'console'}) + .navigateView('problems', [], {hint: 'problems'}) + .navigateView('variables', [], {hint: 'variables'}) + .navigateView('expressions', [], {hint: 'expressions'}) + .navigateView('breakpoints', [], {hint: 'breakpoints'}) .activateView('debug') .activateView('console') .activateView('variables'); diff --git a/apps/workbench-testing-app/src/app/workbench.provider.ts b/apps/workbench-testing-app/src/app/workbench.provider.ts deleted file mode 100644 index dc65c3fef..000000000 --- a/apps/workbench-testing-app/src/app/workbench.provider.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 - */ - -import {WorkbenchModule, WorkbenchModuleConfig} from '@scion/workbench'; -import {EnvironmentProviders, importProvidersFrom} from '@angular/core'; - -/** - * Provides a set of DI providers to set up SCION Workbench. - */ -export function provideWorkbench(config: WorkbenchModuleConfig): EnvironmentProviders { - return importProvidersFrom(WorkbenchModule.forRoot(config)); -} diff --git a/apps/workbench-testing-app/src/app/workbench/workbench.component.html b/apps/workbench-testing-app/src/app/workbench/workbench.component.html index b642395b6..608f17027 100644 --- a/apps/workbench-testing-app/src/app/workbench/workbench.component.html +++ b/apps/workbench-testing-app/src/app/workbench/workbench.component.html @@ -1,6 +1,6 @@ - diff --git a/apps/workbench-testing-app/src/app/workbench/workbench.component.ts b/apps/workbench-testing-app/src/app/workbench/workbench.component.ts index 79b62307b..c0fda1cd8 100644 --- a/apps/workbench-testing-app/src/app/workbench/workbench.component.ts +++ b/apps/workbench-testing-app/src/app/workbench/workbench.component.ts @@ -15,7 +15,7 @@ import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {combineLatest} from 'rxjs'; import {AsyncPipe, NgIf} from '@angular/common'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; -import {WorkbenchDialogService, WorkbenchModule, WorkbenchPart, WorkbenchRouter, WorkbenchService, WorkbenchView} from '@scion/workbench'; +import {WorkbenchComponent as ScionWorkbenchComponent, WorkbenchDialogService, WorkbenchPart, WorkbenchPartActionDirective, WorkbenchRouter, WorkbenchRouterLinkDirective, WorkbenchService, WorkbenchView, WorkbenchViewMenuItemDirective} from '@scion/workbench'; import {SciMaterialIconDirective} from '@scion/components.internal/material-icon'; import {ViewMoveDialogTestPageComponent} from '../test-pages/view-move-dialog-test-page/view-move-dialog-test-page.component'; import {ViewInfoDialogComponent} from '../view-info-dialog/view-info-dialog.component'; @@ -28,8 +28,11 @@ import {ViewInfoDialogComponent} from '../view-info-dialog/view-info-dialog.comp imports: [ NgIf, AsyncPipe, - WorkbenchModule, SciMaterialIconDirective, + ScionWorkbenchComponent, + WorkbenchPartActionDirective, + WorkbenchRouterLinkDirective, + WorkbenchViewMenuItemDirective, ], }) export class WorkbenchComponent implements OnDestroy { diff --git a/docs/site/announcements.md b/docs/site/announcements.md index f87ac33e5..541edfb9f 100644 --- a/docs/site/announcements.md +++ b/docs/site/announcements.md @@ -27,7 +27,7 @@ We have added microfrontend support to the SCION Workbench. SCION Workbench has built-in microfrontend support from the [SCION Microfrontend Platform][link-scion-microfrontend-platform], a lightweight library for embedding microfrontends. Microfrontends embedded as views can interact seamlessly with the workbench using the [SCION Workbench Client][link-scion-workbench-client] or communicate with other microfrontends via the SCION Microfrontend Platform. Any web application can be integrated as a workbench view. Likewise, a workbench view can embed further microfrontends, and so on. - Documentation and step-by-step instructions are still missing, but the JSDoc is quite detailed. See [WorkbenchModuleConfig][link-workbench-module-config.ts] on how to enable microfrontend support in the workbench. In the meantime, for micro apps that want to interact with the workbench, we refer you to the [published TypeDoc][link-scion-workbench-client-api]. + Documentation and step-by-step instructions are still missing, but the JSDoc is quite detailed. See [WorkbenchConfig][link-workbench-config.ts] on how to enable microfrontend support in the workbench. In the meantime, for micro apps that want to interact with the workbench, we refer you to the [published TypeDoc][link-scion-workbench-client-api]. - **2020-11: Deletion of the SCION Application Platform**\ We have deleted the SCION application platform from our Git repository and deprecated respective NPM modules. This project is discontinued and will no longer be maintained. Its documentation is still online. The following NPM modules are deprecated: `@scion/workbench-application-platform`, `@scion/workbench-application-platform.api`, `@scion/workbench-application.core`, `@scion/workbench-application.angular`, `@scion/mouse-dispatcher`, `@scion/dimension` (moved to `@scion/toolkit`), `@scion/viewport` (moved to `@scion/toolkit`). @@ -46,8 +46,8 @@ On the way to a true workbench layout, we deprecate activities to introduce the [link-scion-microfrontend-platform]: https://github.com/SchweizerischeBundesbahnen/scion-microfrontend-platform/blob/master/README.md [link-scion-workbench-client]: https://www.npmjs.com/package/@scion/workbench-client -[link-scion-workbench-client-api]: https://scion-workbench-client-api.vercel.app -[link-workbench-module-config.ts]: https://github.com/SchweizerischeBundesbahnen/scion-workbench/blob/master/projects/scion/workbench/src/lib/workbench-module-config.ts +[link-scion-workbench-client-api]: https://scion-workbench-client-api.vercel.app +[link-workbench-config.ts]: https://github.com/SchweizerischeBundesbahnen/scion-workbench/blob/master/projects/scion/workbench/src/lib/workbench-config.ts [menu-home]: /README.md [menu-projects-overview]: /docs/site/projects-overview.md diff --git a/docs/site/features.md b/docs/site/features.md index f96ecd053..fca95eeef 100644 --- a/docs/site/features.md +++ b/docs/site/features.md @@ -12,29 +12,29 @@ This page gives you an overview of existing and planned workbench features. Deve [![][planned]](#) Planned       [![][deprecated]](#) Deprecated -| Feature |Category|Status|Note -|-------------------------|-|-|-| -| Workbench Layout |layout|[![][done]](#)|Layout for the flexible arrangement of views side-by-side or stacked, all personalizable by the user via drag & drop. -| Activity Layout |layout|[![][progress]](#)|Compact presentation of views around the main area, similar to activities known from Visual Studio Code or IntelliJ. -| Perspective |layout|[![][done]](#)|Multiple layouts, called perspectives, are supported. Perspectives can be switched with one perspective active at a time. Perspectives share the same main area, if any. [#305](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/305). -| View |layout|[![][done]](#)|Visual component for displaying content stacked or side-by-side in the workbench layout. -| Multi-Window |layout|[![][done]](#)|Views can be opened in new browser windows. -| Part Actions |layout|[![][done]](#)|Actions that are displayed in the tabbar of a part. Actions can stick to a view, so they are only visible if the view is active. -| View Context Menu |layout|[![][done]](#)|A viewtab has a context menu. By default, the workbench adds some workbench-specific menu items to the context menu, such as for closing other views. Custom menu items can be added to the context menu as well. -| Persistent Navigation |navigation|[![][done]](#)|The arrangement of the views is added to the browser URL or local storage, enabling persistent navigation. -| Start Page |layout|[![][done]](#)|A start page can be used to display content when all views are closed. -| Microfrontend Support |microfrontend|[![][done]](#)|Microfrontends can be opened in views. Embedded microfrontends can interact with the workbench using a framework-angostic workbench API. The documentation is still missing. [#304](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/304). -| Theming |customization|[![][done]](#)|An application can define a custom theme to change the default look of the SCION Workbench. [#110](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/110) -| Responsive Design |layout|[![][planned]](#)|The workbench adapts its layout to the current display size and device. [#112](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/112) -| Electron/Edge Webview 2 |env|[![][planned]](#)|The workbench can be used in desktop applications built with [Electron](https://www.electronjs.org/) and/or [Microsoft Edge WebView2](https://docs.microsoft.com/en-us/microsoft-edge/webview2/) to support window arrangements. [#306](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/306) -| Localization (l10n) |env|[![][planned]](#)|The workbench allows the localization of built-in texts such as texts in context menus and manifest entries. [#255](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/255) -| Browser Support |env|[![][planned]](#)|The workbench works with most modern browsers. As of now, the workbench is optimized and tested on browsers based on the Chromium rendering engine (Google Chrome, Microsoft Edge). However, the workbench should work fine on other modern browsers as well. [#111](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/111) -| Dialog |control|[![][progress]](#)|Content can be displayed in a modal dialog. A dialog can be view or application modal. Multiple dialogs are stacked. -| Message Box |control|[![][done]](#)|Content can be displayed in a modal message box. A message box can be view or application modal. Multiple message boxes are stacked. -| Notification Ribbon |control|[![][done]](#)|Notifications can be displayed to the user. Notifications slide in in the upper-right corner. Multiple notifications are displayed one below the other. -| Popup |control|[![][done]](#)|Content can be displayed in a popup overlay. A popup does not block the application. -| Developer guide |doc|[![][planned]](#)|Developer Guide describing the workbench layout, its conceptsm fundamental APIs and built-in microfrontend support. [#304](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/304) -| Tab |customization|[![][done]](#)|The built-in viewtab can be replaced with a custom viewtab implementation, e.g., to add additional functionality. +| Feature | Category | Status | Note +|-------------------------|---------------|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Workbench Layout | layout | [![][done]](#) | Layout for the flexible arrangement of views side-by-side or stacked, all personalizable by the user via drag & drop. +| Activity Layout | layout | [![][progress]](#) | Compact presentation of views around the main area, similar to activities known from Visual Studio Code or IntelliJ. +| Perspective | layout | [![][done]](#) | Multiple layouts, called perspectives, are supported. Perspectives can be switched. Only one perspective is active at a time. Perspectives share the same main area, if any. [#305](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/305). +| View | layout | [![][done]](#) | Visual component for displaying content stacked or side-by-side in the workbench layout. +| Multi-Window | layout | [![][done]](#) | Views can be opened in new browser windows. +| Part Actions | layout | [![][done]](#) | Actions that are displayed in the tabbar of a part. Actions can stick to a view, so they are only visible if the view is active. +| View Context Menu | layout | [![][done]](#) | A viewtab has a context menu. By default, the workbench adds some workbench-specific menu items to the context menu, such as for closing other views. Custom menu items can be added to the context menu as well. +| Persistent Navigation | navigation | [![][done]](#) | The arrangement of the views is added to the browser URL or local storage, enabling persistent navigation. +| Start Page | layout | [![][done]](#) | A start page can be used to display content when all views are closed. +| Microfrontend Support | microfrontend | [![][done]](#) | Microfrontends can be opened in views. Embedded microfrontends can interact with the workbench using a framework-angostic workbench API. The documentation is still missing. [#304](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/304). +| Theming | customization | [![][done]](#) | An application can define a custom theme to change the default look of the SCION Workbench. [#110](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/110) +| Responsive Design | layout | [![][planned]](#) | The workbench adapts its layout to the current display size and device. [#112](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/112) +| Electron/Edge Webview 2 | env | [![][planned]](#) | The workbench can be used in desktop applications built with [Electron](https://www.electronjs.org/) and/or [Microsoft Edge WebView2](https://docs.microsoft.com/en-us/microsoft-edge/webview2/) to support window arrangements. [#306](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/306) +| Localization (l10n) | env | [![][planned]](#) | The workbench allows the localization of built-in texts such as texts in context menus and manifest entries. [#255](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/255) +| Browser Support | env | [![][planned]](#) | The workbench works with most modern browsers. As of now, the workbench is optimized and tested on browsers based on the Chromium rendering engine (Google Chrome, Microsoft Edge). However, the workbench should work fine on other modern browsers as well. [#111](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/111) +| Dialog | control | [![][done]](#) | Content can be displayed in a modal dialog. A dialog can be view or application modal. Multiple dialogs are stacked. +| Message Box | control | [![][done]](#) | Content can be displayed in a modal message box. A message box can be view or application modal. Multiple message boxes are stacked. +| Notification Ribbon | control | [![][done]](#) | Notifications can be displayed to the user. Notifications slide in in the upper-right corner. Multiple notifications are displayed one below the other. +| Popup | control | [![][done]](#) | Content can be displayed in a popup overlay. A popup does not block the application. +| Developer guide | doc | [![][planned]](#) | Developer Guide describing the workbench layout, its conceptsm fundamental APIs and built-in microfrontend support. [#304](https://github.com/SchweizerischeBundesbahnen/scion-workbench/issues/304) +| Tab | customization | [![][done]](#) | The built-in viewtab can be replaced with a custom viewtab implementation, e.g., to add additional functionality. [done]: /docs/site/images/icon-done.svg [progress]: /docs/site/images/icon-in-progress.svg diff --git a/docs/site/getting-started.md b/docs/site/getting-started.md index fbc70a88a..ccfe20e18 100644 --- a/docs/site/getting-started.md +++ b/docs/site/getting-started.md @@ -5,9 +5,9 @@ ## [SCION Workbench][menu-home] > Getting Started -We will create a simple todo list app to introduce you to the SCION Workbench. This short tutorial helps to install the SCION Workbench and explains how to arrange and open views. +We will create a simple TODO app to introduce you to the SCION Workbench. This short tutorial helps to install the SCION Workbench and explains how to arrange and open views. -The application lists todos on the left side. When the user clicks a todo, a new view opens displaying the todo. Different todos open a different view. To open a todo multiple times, the Ctrl key can be pressed. The user can size and arrange views by drag and drop. +The application lists TODOs on the left side. When the user clicks a TODO, a new view opens displaying the TODO. Different TODOs open a different view. To open a TODO multiple times, the Ctrl key can be pressed. The user can size and arrange views by drag and drop. *** - After you complete this guide, the application will look like this: https://scion-workbench-getting-started.vercel.app. @@ -21,7 +21,7 @@ The application lists todos on the left side. When the user clicks a todo, a new Run the following command to create a new Angular application. ```console -ng new workbench-getting-started --routing=false --style=scss --skip-tests +ng new workbench-getting-started --routing=false --style=scss --ssr=false --skip-tests ``` @@ -39,31 +39,24 @@ npm install @scion/workbench @scion/workbench-client @scion/toolkit @scion/compo
- Import SCION Workbench Module + Register SCION Workbench Providers
-Open `app.module.ts` and import the `WorkbenchModule` and `BrowserAnimationsModule`. Added lines are marked with `[+]`. +Open `app.config.ts` and register SCION Workbench providers. Added lines are marked with `[+]`. ```ts - import {NgModule} from '@angular/core'; - import {AppComponent} from './app.component'; -[+] import {WorkbenchModule} from '@scion/workbench'; -[+] import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -[+] import {RouterModule} from '@angular/router'; - import {BrowserModule} from '@angular/platform-browser'; - - @NgModule({ - declarations: [AppComponent], - imports: [ -[+] WorkbenchModule.forRoot(), -[+] RouterModule.forRoot([]), - BrowserModule, -[+] BrowserAnimationsModule, + import {ApplicationConfig} from '@angular/core'; +[+] import {provideWorkbench} from '@scion/workbench'; +[+] import {provideRouter} from '@angular/router'; +[+] import {provideAnimations} from '@angular/platform-browser/animations'; + + export const appConfig: ApplicationConfig = { + providers: [ +[+] provideWorkbench(), +[+] provideRouter([]), // required by the SCION Workbench +[+] provideAnimations(), // required by the SCION Workbench ], - bootstrap: [AppComponent], - }) - export class AppModule { - } + }; ```
@@ -74,7 +67,7 @@ Open `app.module.ts` and import the `WorkbenchModule` and `BrowserAnimationsModu Open `app.component.html` and change it as follows: ```html - + ``` The workbench itself does not position nor lay out the `` component. Depending on your requirements, you may want the workbench to fill the entire page viewport or only parts of it, for example, if you have a header, footer, or navigation panel. @@ -109,10 +102,10 @@ Also, download the workbench icon font from
here. +In this step, we will create the TODO list and place it to the left of the main area. We will use the `TodoService` to get some sample TODOs. You can download the `todo.service.ts` file from here. -1. Create a new standalone component using the Angluar CLI. +1. Create a new component using the Angular CLI. ```console - ng generate component todos --standalone --skip-tests + ng generate component todos --skip-tests ``` 2. Open `todos.component.ts` and change it as follows. @@ -184,14 +170,12 @@ In this step, we will create a component to display the todos. We will use the ` import {Component} from '@angular/core'; [+] import {WorkbenchRouterLinkDirective, WorkbenchView} from '@scion/workbench'; [+] import {TodoService} from '../todo.service'; - [+] import {NgFor} from '@angular/common'; @Component({ selector: 'app-todos', templateUrl: './todos.component.html', standalone: true, imports: [ - [+] NgFor, [+] WorkbenchRouterLinkDirective, ], }) @@ -204,111 +188,90 @@ In this step, we will create a component to display the todos. We will use the ` [+] } } ``` + In the constructor, we inject the view handle `WorkbenchView`. Using this handle, we can interact with the view, for example, set the title or make the view non-closable. We also inject a reference to the `TodoService` to iterate over the todos in the template. - In the constructor, we inject the view handle `WorkbenchView`. Using this handle, we can interact with the view, for example, set the title or make the view non-closable. We also inject a reference to the `TodoService` to iterate over the todos in the template. - - > Do not forget to export the component by default to simplify route registration. + We also change the component to be exported by default, making it easier to register the route for the component. 3. Open `todos.component.html` and change it as follows: ```html
    -
  1. - {{todo.task}} -
  2. + @for (todo of todoService.todos; track todo.id) { +
  3. + {{todo.task}} +
  4. + }
``` - For each todo, we create a link. When the user clicks on a link, a new view with the respective todo will open. In a next step we will create the todo component and register it under the route `/todos/:id`. Note that we are using the `wbRouterLink` and not the `routerLink` directive. The `wbRouterLink` directive is the Workbench equivalent of the Angular Router link, which enables us to target views. + For each TODO, we create a link. When the user clicks on a link, a new view with the TODO will open. In a next step we will create the TODO component and register it under the route `/todos/:id`. -4. Register a route in `app.module.ts` for the component. + > Note that we are using the `wbRouterLink` and not the `routerLink` directive. The `wbRouterLink` directive is the Workbench equivalent of the Angular Router link to navigate views. By default, `wbRouterLink` navigates the current view. In this example, however, we want to open the `todo` component in a new view or, if already open, activate it. Therefore, we set the target to `auto`. +4. Register a route in `app.config.ts` for the component. ```ts - import {NgModule} from '@angular/core'; - import {AppComponent} from './app.component'; - import {WorkbenchModule} from '@scion/workbench'; - import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; - import {RouterModule} from '@angular/router'; - import {BrowserModule} from '@angular/platform-browser'; + import {ApplicationConfig} from '@angular/core'; + import {provideWorkbench} from '@scion/workbench'; + import {provideRouter} from '@angular/router'; + import {provideAnimations} from '@angular/platform-browser/animations'; - @NgModule({ - declarations: [AppComponent], - imports: [ - WorkbenchModule.forRoot(), - RouterModule.forRoot([ + export const appConfig: ApplicationConfig = { + providers: [ + provideWorkbench(), + provideRouter([ {path: '', loadComponent: () => import('./welcome/welcome.component')}, - [+] {path: '', outlet: 'todos', loadComponent: () => import('./todos/todos.component')}, + [+] {path: 'todos', loadComponent: () => import('./todos/todos.component')}, ]), - BrowserModule, - BrowserAnimationsModule, + provideAnimations(), ], - bootstrap: [AppComponent], - }) - export class AppModule { - } + }; ``` - We create an empty path secondary route. The route object for a secondary route has an outlet property. Its value refers to the view in the workbench layout. In our example, we name the outlet `todos`. In the next step, we will add a view named `todos` to the workbench layout. - - - -
- Display Todos on the Left Side -
- -In this step, we will define a simple workbench layout that displays the todos component as a view on the left to the main area. - -Open `app.module.ts` and pass `WorkbenchModule.forRoot()` a configuration object with the initial workbench layout. - -```ts - import {NgModule} from '@angular/core'; - import {AppComponent} from './app.component'; -[+] import {MAIN_AREA, WorkbenchLayoutFactory, WorkbenchModule} from '@scion/workbench'; - import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; - import {RouterModule} from '@angular/router'; - import {BrowserModule} from '@angular/platform-browser'; - - @NgModule({ - declarations: [AppComponent], - imports: [ - WorkbenchModule.forRoot({ -[+] layout: (factory: WorkbenchLayoutFactory) => factory -[+] .addPart(MAIN_AREA) -[+] .addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) -[+] .addView('todos', {partId: 'left', activateView: true}), - }), - RouterModule.forRoot([ - {path: '', loadComponent: () => import('./welcome/welcome.component')}, - {path: '', outlet: 'todos', loadComponent: () => import('./todos/todos.component')}, - ]), - BrowserModule, - BrowserAnimationsModule, - ], - bootstrap: [AppComponent], - }) - export class AppModule { - } -``` +5. Add the TODO list to the workbench layout. -We define the initial arrangement of views by specifying a layout function. The function is passed a factory to create the layout. + Open `app.config.ts` and configure the workbench with the initial layout. -> The workbench layout is a grid of parts. Parts are aligned relative to each other. A part is a stack of views. Content is displayed in views. -> The layout can be divided into a main and a peripheral area, with the main area as the primary place for opening views. The peripheral area arranges parts around the main area to provide navigation or context-sensitive assistance to support the user's workflow. Defining a main area is optional and recommended for applications requiring a dedicated and maximizable area for user interaction. - -In this example, we create a layout with two parts, the main area and a part left to it. We name the left part `left` and align it to the left of the main area. We want it to take up 25% of the available space. Next, we add the todos view to the part. We name the view `todos`, the same name we used in the previous step where we created the secondary route for the view. This is how we link a view to a route. - -Open a browser to http://localhost:4200. You should see the todo list left to the main area. However, when you click on a todo, you will get an error because we have not registered the route yet. + ```ts + import {ApplicationConfig} from '@angular/core'; + import {provideWorkbench} from '@scion/workbench'; + import {provideRouter} from '@angular/router'; + import {provideAnimations} from '@angular/platform-browser/animations'; + [+] import {MAIN_AREA, WorkbenchLayoutFactory} from '@scion/workbench'; + + export const appConfig: ApplicationConfig = { + providers: [ + provideWorkbench({ + [+] layout: (factory: WorkbenchLayoutFactory) => factory + [+] .addPart(MAIN_AREA) + [+] .addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) + [+] .addView('todos', {partId: 'left', activateView: true}) + [+] .navigateView('todos', ['todos']) + }), + provideRouter([ + {path: '', loadComponent: () => import('./welcome/welcome.component')}, + {path: 'todos', loadComponent: () => import('./todos/todos.component')}, + ]), + provideAnimations(), + ], + }; + ``` + + In the above code snippet, we create a layout with two parts, the main area and a part left to it. We align the `left` part to the left of the main area. We want it to take up 25% of the available space. Next, we add the `todos` view to the left part. Finally, we navigate the `todos` view to the `todos` component. + + For detailed explanations on defining the workbench layout, refer to [Defining the initial workbench layout][link-how-to-define-initial-workbench-layout]. + + Open a browser to http://localhost:4200. You should see the TODO list left to the main area.
Create Todo Component
-In this step, we will create a component to open a todo in a view. +In this step, we will create a component to open a TODO in a view. -1. Create a new standalone component using the Angluar CLI. +1. Create a new component using the Angular CLI. ```console - ng generate component todo --standalone --skip-tests + ng generate component todo --skip-tests ``` 2. Open `todo.component.ts` and change it as follows. @@ -318,7 +281,7 @@ In this step, we will create a component to open a todo in a view. [+] import {Todo, TodoService} from '../todo.service'; [+] import {ActivatedRoute} from '@angular/router'; [+] import {map, Observable, tap} from 'rxjs'; - [+] import {AsyncPipe, DatePipe, formatDate, NgIf} from '@angular/common'; + [+] import {AsyncPipe, DatePipe, formatDate} from '@angular/common'; @Component({ selector: 'app-todo', @@ -326,7 +289,7 @@ In this step, we will create a component to open a todo in a view. styleUrls: ['./todo.component.scss'], standalone: true, imports: [ - [+] AsyncPipe, NgIf, DatePipe, + [+] AsyncPipe, DatePipe, ], }) [+] export default class TodoComponent { @@ -347,26 +310,26 @@ In this step, we will create a component to open a todo in a view. } ``` - As with the todo list component, we change the component to be exported by default, making it easier to register the route for the component. + As with the TODO list component, we change the component to be exported by default, making it easier to register the route for the component. - In the constructor, we inject the `ActivatedRoute` to read the id of the todo that we want to display in the view. We also inject the `TodoService` to look up the todo. As a side effect, after looking up the todo, we set the title and heading of the view. + In the constructor, we inject the `ActivatedRoute` to read the id of the TODO that we want to display in the view. We also inject the `TodoService` to look up the TODO. As a side effect, after looking up the TODO, we set the title and heading of the view. - In the next step, we will subscribe to the observable in the template. + In the next step, we will subscribe to the observable in the template. 3. Open `todo.component.html` and change it as follows. ```html - + @if (todo$ | async; as todo) { Task:{{todo.task}} Due Date:{{todo.dueDate | date:'short'}} Notes:{{todo.notes}} - + } ``` - Using Angular's `async` pipe, we subscribe to the `todo$` observable and assign its emitted value to the template variable `todo`. Then, we render the todo. + Using Angular's `async` pipe, we subscribe to the `todo$` observable and assign its emitted value to the template variable `todo`. Then, we render the TODO. -4. Open `todo.component.scss` and add the following content. +4. Open `todo.component.scss` and add the following styles. - Next, we add some CSS to get a tabular presentation of the todo. + Next, we add some CSS to get a tabular presentation of the TODO. ```css :host { @@ -378,51 +341,48 @@ In this step, we will create a component to open a todo in a view. } ``` -5. Register a route in `app.module.ts` for the component. +5. Register a route in `app.config.ts` for the component. - Finally, we need to register a route for the component. Unlike the todo list component, we do not create a secondary route, but a primary route with a path, in our example `todos/:id`. We can then navigate to this component in a view using the `WorkbenchRouter` or `wbRouterLink`. + Finally, we need to register a route for the component. We can then navigate to this component in a view using the `WorkbenchRouter` or `wbRouterLink`. - ```ts - import {NgModule} from '@angular/core'; - import {AppComponent} from './app.component'; - import {WorkbenchModule} from '@scion/workbench'; - import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; - import {RouterModule} from '@angular/router'; - import {BrowserModule} from '@angular/platform-browser'; - - @NgModule({ - declarations: [AppComponent], - imports: [ - WorkbenchModule.forRoot({ - layout: (factory: WorkbenchLayoutFactory) => factory - .addPart(MAIN_AREA) - .addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) - .addView('todos', {partId: 'left', activateView: true}), - }), - RouterModule.forRoot([ - {path: '', loadComponent: () => import('./welcome/welcome.component')}, - {path: '', outlet: 'todos', loadComponent: () => import('./todos/todos.component')}, - [+] {path: 'todos/:id', loadComponent: () => import('./todo/todo.component')}, - ]), - BrowserModule, - BrowserAnimationsModule, - ], - bootstrap: [AppComponent], - }) - export class AppModule { - } - ``` - - Below the code from the previous step how we open the todo view using the `wbRouterLink` directive. - ```html -
    -
  1. - {{todo.task}} -
  2. -
- ``` - - Open a browser to http://localhost:4200. You should see the todo list left to the main area. When you click on a todo, a new view opens displaying the todo. Different todos open a different view. To open a todo multiple times, also press the Ctrl key. + ```ts + import {ApplicationConfig} from '@angular/core'; + import {provideWorkbench} from '@scion/workbench'; + import {provideRouter} from '@angular/router'; + import {provideAnimations} from '@angular/platform-browser/animations'; + import {MAIN_AREA, WorkbenchLayoutFactory} from '@scion/workbench'; + + export const appConfig: ApplicationConfig = { + providers: [ + provideWorkbench({ + layout: (factory: WorkbenchLayoutFactory) => factory + .addPart(MAIN_AREA) + .addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) + .addView('todos', {partId: 'left', activateView: true}) + .navigateView('todos', ['todos']) + }), + provideRouter([ + {path: '', loadComponent: () => import('./welcome/welcome.component')}, + {path: 'todos', loadComponent: () => import('./todos/todos.component')}, + [+] {path: 'todos/:id', loadComponent: () => import('./todo/todo.component')}, + ]), + provideAnimations(), + ], + }; + ``` + + Below the code from the previous step how we open the TODO view using the `wbRouterLink` directive. + ```html +
    + @for (todo of todoService.todos; track todo.id) { +
  1. + {{todo.task}} +
  2. + } +
+ ``` + + Open a browser to http://localhost:4200. You should see the TODO list left to the main area. When you click on a TODO, a new view opens displaying the TODO. Different TODOs open a different view. To open a TODO multiple times, also press the Ctrl key.
@@ -431,9 +391,11 @@ In this step, we will create a component to open a todo in a view.
This short guide has introduced you to the basics of SCION Workbench. For more advanced topics, please refer to our [How-To][link-how-to] guides. - + +[link-how-to-define-initial-workbench-layout]: /docs/site/howto/how-to-define-initial-layout.md + [menu-home]: /README.md [menu-projects-overview]: /docs/site/projects-overview.md [menu-changelog]: /docs/site/changelog.md diff --git a/docs/site/howto/how-to-close-view.md b/docs/site/howto/how-to-close-view.md index 8bfb40413..ba2d58cfa 100644 --- a/docs/site/howto/how-to-close-view.md +++ b/docs/site/howto/how-to-close-view.md @@ -7,28 +7,39 @@ ### How to close a view -A view can be closed via navigation, the view's handle `WorkbenchView`, or the `WorkbenchService`. +A view can be closed via the view's handle `WorkbenchView`, the `WorkbenchService`, or the `WorkbenchRouter`. -#### Closing the view using its handle +#### Closing a view using its handle Inject `WorkbenchView` handle and invoke the `close` method. ```ts +import {inject} from '@angular/core'; +import {WorkbenchView} from '@scion/workbench'; + inject(WorkbenchView).close(); ``` #### Closing view(s) using the `WorkbenchService` -Inject `WorkbenchService` and invoke `close`, passing the identifies of the views to close. +Inject `WorkbenchService` and invoke `closeViews`, passing the ids of the views to close. ```ts +import {inject} from '@angular/core'; +import {WorkbenchService} from '@scion/workbench'; + inject(WorkbenchService).closeViews('view.1', 'view.2'); ``` -#### Closing view(s) via navigation +#### Closing view(s) via `WorkbenchRouter` -Views can be closed by performing a navigation with the `close` flag set in the navigation extras. Views matching the path will be closed. The path supports the asterisk wildcard segment (`*`) to match view(s) with any value in that segment. To close a specific view, set a view `target` instead of a path. +The router supports for closing views matching the routing commands by setting `close` in navigation extras. + +Matrix parameters do not affect view resolution. The path supports the asterisk wildcard segment (`*`) to match views with any value in a segment. To close a specific view, set a view target instead of a path. ```ts +import {inject} from '@angular/core'; +import {WorkbenchRouter} from '@scion/workbench'; + inject(WorkbenchRouter).navigate(['path/*/view'], {close: true}); ``` diff --git a/docs/site/howto/how-to-configure-start-page.md b/docs/site/howto/how-to-configure-start-page.md index 40497de02..5b9e1a952 100644 --- a/docs/site/howto/how-to-configure-start-page.md +++ b/docs/site/howto/how-to-configure-start-page.md @@ -11,9 +11,16 @@ A start page can be used to display content when all views are closed. To display a start page, register an empty path route, as follows: ```ts -RouterModule.forRoot([ - {path: '', loadComponent: () => import('./start-page/start-page.component')}, -]); +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter} from '@angular/router'; + +bootstrapApplication(AppComponent, { + providers: [ + provideRouter([ + {path: '', loadComponent: () => import('./start-page/start-page.component')}, + ]), + ], +}); ``` ### How to configure a start page per perspective @@ -21,20 +28,28 @@ RouterModule.forRoot([ If working with perspectives, configure a different start page per perspective by testing for the active perspective in the `canMatch` route handler. ```ts -RouterModule.forRoot([ - // Match this route only if 'perspective A' is active. - { - path: '', - loadComponent: () => import('./perspective-a/start-page.component'), - canMatch: [() => inject(WorkbenchService).getPerspective('perspective-a')?.active] - }, - // Match this route only if 'perspective B' is active. - { - path: '', - loadComponent: () => import('./perspective-b/start-page.component'), - canMatch: [() => inject(WorkbenchService).getPerspective('perspective-b')?.active] - }, -]); +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter} from '@angular/router'; +import {WorkbenchService} from '@scion/workbench'; + +bootstrapApplication(AppComponent, { + providers: [ + provideRouter([ + // Match this route only if 'perspective A' is active. + { + path: '', + loadComponent: () => import('./perspective-a/start-page.component'), + canMatch: [() => inject(WorkbenchService).getPerspective('perspective-a')?.active], + }, + // Match this route only if 'perspective B' is active. + { + path: '', + loadComponent: () => import('./perspective-b/start-page.component'), + canMatch: [() => inject(WorkbenchService).getPerspective('perspective-b')?.active], + }, + ]), + ], +}); ``` [menu-how-to]: /docs/site/howto/how-to.md diff --git a/docs/site/howto/how-to-define-initial-layout.md b/docs/site/howto/how-to-define-initial-layout.md index f2ec42a94..0b9a1bbcd 100644 --- a/docs/site/howto/how-to-define-initial-layout.md +++ b/docs/site/howto/how-to-define-initial-layout.md @@ -7,39 +7,60 @@ The workbench layout is a grid of parts. Parts are aligned relative to each other. A part is a stack of views. Content is displayed in views. -The layout can be divided into a main and a peripheral area, with the main area as the primary place for opening views. The peripheral area arranges parts around the main area to provide navigation or context-sensitive assistance to support the user's workflow. Defining a main area is optional and recommended for applications requiring a dedicated and maximizable area for user interaction. +The workbench layout can be divided into a main and a peripheral area, with the main area as the primary place for opening views. The peripheral area arranges parts around the main area to provide navigation or context-sensitive assistance to support the user's workflow. Defining a main area is optional and recommended for applications requiring a dedicated and maximizable area for user interaction. -### How to define an initial layout +### How to define the initial workbench layout -Arranging views in the workbench layout requires two steps. +Define the workbench layout by registering a layout function in the workbench config. The workbench will invoke this function with a factory to create the layout. The layout is immutable, so each modification creates a new instance. Use the instance for further modifications and finally return it. -
- 1. Define the layout via workbench config -
+Start by adding the first part. From there, you can gradually add more parts and align them relative to each other. Next, add views to the layout, specifying to which part to add the views. The final step is to navigate the views. A view can be navigated to any route. ```ts -import {MAIN_AREA, WorkbenchLayoutFactory, WorkbenchModule} from '@scion/workbench'; - -WorkbenchModule.forRoot({ - layout: (factory: WorkbenchLayoutFactory) => factory - .addPart(MAIN_AREA) - .addPart('topLeft', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) - .addPart('bottomLeft', {relativeTo: 'topLeft', align: 'bottom', ratio: .5}) - .addPart('bottom', {align: 'bottom', ratio: .3}) - .addView('navigator', {partId: 'topLeft', activateView: true}) - .addView('explorer', {partId: 'topLeft'}) - .addView('console', {partId: 'bottom', activateView: true}) - .addView('problems', {partId: 'bottom'}) - .addView('search', {partId: 'bottom'}) +import {bootstrapApplication} from '@angular/platform-browser'; +import {MAIN_AREA, provideWorkbench, WorkbenchLayoutFactory} from '@scion/workbench'; + +bootstrapApplication(AppComponent, { + providers: [ + provideWorkbench({ + layout: (factory: WorkbenchLayoutFactory) => factory + // Add parts to the layout. + .addPart(MAIN_AREA) + .addPart('topLeft', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) + .addPart('bottomLeft', {relativeTo: 'topLeft', align: 'bottom', ratio: .5}) + .addPart('bottom', {align: 'bottom', ratio: .3}) + + // Add views to the layout. + .addView('navigator', {partId: 'topLeft'}) + .addView('explorer', {partId: 'bottomLeft'}) + .addView('console', {partId: 'bottom'}) + .addView('problems', {partId: 'bottom'}) + .addView('search', {partId: 'bottom'}) + + // Navigate views. + .navigateView('navigator', ['path/to/navigator']) + .navigateView('explorer', ['path/to/explorer']) + .navigateView('console', [], {hint: 'console'}) // Set hint to differentiate between routes with an empty path. + .navigateView('problems', [], {hint: 'problems'}) // Set hint to differentiate between routes with an empty path. + .navigateView('search', ['path/to/search']) + + // Decide which views to activate. + .activateView('navigator') + .activateView('explorer') + .activateView('console') + }), + ], }); ``` + +> The layout function can call `inject` to get any required dependencies. + The above code snippet defines the following layout. ```plain +--------+----------------+ -| top | main area | +| top | | | left | | -|--------+ | +|--------+ main area | | bottom | | | left | | +--------+----------------+ @@ -47,26 +68,36 @@ The above code snippet defines the following layout. +-------------------------+ ``` -A layout is defined through a layout function in the workbench config. The function is passed a factory to create the layout. The layout has methods to modify it. Each modification creates a new layout instance that can be used for further modifications. - -> The function can call `inject` to get required dependencies, if any. -
- -
- 2. Register the routes for views added to the layout -
+The above layout requires the following routes. ```ts -RouterModule.forRoot([ - {path: '', outlet: 'navigator', loadComponent: () => import('./navigator/navigator.component')}, - {path: '', outlet: 'explorer', loadComponent: () => import('./explorer/explorer.component')}, - {path: '', outlet: 'console', loadComponent: () => import('./console/console.component')}, - {path: '', outlet: 'problems', loadComponent: () => import('./problems/problems.component')}, - {path: '', outlet: 'search', loadComponent: () => import('./search/search.component')}, -]); +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter} from '@angular/router'; +import {canMatchWorkbenchView} from '@scion/workbench'; + +bootstrapApplication(AppComponent, { + providers: [ + provideRouter([ + // Navigator View + {path: 'path/to/navigator', loadComponent: () => import('./navigator/navigator.component')}, + // Explorer View + {path: 'path/to/explorer', loadComponent: () => import('./explorer/explorer.component')}, + // Search View + {path: 'path/to/search', loadComponent: () => import('./search/search.component')}, + // Console View + {path: '', canMatch: [canMatchWorkbenchView('console')], loadComponent: () => import('./console/console.component')}, + // Problems View + {path: '', canMatch: [canMatchWorkbenchView('problems')], loadComponent: () => import('./problems/problems.component')}, + ]), + ], +}); ``` -A route for a view in the initial layout must be a secondary route with an empty path. The outlet refers to the view in the layout. Because the path is empty, no outlet needs to be added to the URL. -
+ +> To avoid cluttering the initial URL, we recommend navigating the views of the initial layout to empty path routes and using a navigation hint to differentiate. + +> Use the `canMatchWorkbenchView` guard to match a route only when navigating a view with a particular hint. + +> Use the `canMatchWorkbenchView` guard and pass `false` to never match a route for a workbench view, e.g., to exclude the application root path, if any, necessary when navigating views to the empty path route. [menu-how-to]: /docs/site/howto/how-to.md diff --git a/docs/site/howto/how-to-install-workbench.md b/docs/site/howto/how-to-install-workbench.md index 218793b9c..904d5c1ce 100644 --- a/docs/site/howto/how-to-install-workbench.md +++ b/docs/site/howto/how-to-install-workbench.md @@ -22,32 +22,43 @@ npm install @scion/workbench @scion/workbench-client @scion/toolkit @scion/compo
- Import SCION Workbench module + Register SCION Workbench Providers
-Open `app.module.ts` and import the `WorkbenchModule`. The lines to be added are marked with `[+]`. +Open `app.config.ts` and register SCION Workbench providers. ```ts - import {NgModule} from '@angular/core'; - import {AppComponent} from './app.component'; -[+] import {WorkbenchModule} from '@scion/workbench'; -[+] import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; - import {RouterModule} from '@angular/router'; - import {BrowserModule} from '@angular/platform-browser'; - - @NgModule({ - declarations: [AppComponent], - imports: [ -[+] WorkbenchModule.forRoot(), - RouterModule.forRoot([]), - BrowserModule, -[+] BrowserAnimationsModule, - ], - bootstrap: [AppComponent], - }) - export class AppModule { - } +import {ApplicationConfig} from '@angular/core'; +import {provideRouter} from '@angular/router'; +import {provideAnimations} from '@angular/platform-browser/animations'; +import {provideWorkbench} from '@scion/workbench'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideWorkbench(), + provideRouter([]), // required by the SCION Workbench + provideAnimations(), // required by the SCION Workbench + ], +}; ``` + +If you are not using `app.config.ts`, register the SCION Workbench directly in `main.ts`. + +```ts +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter} from '@angular/router'; +import {provideAnimations} from '@angular/platform-browser/animations'; +import {provideWorkbench} from '@scion/workbench'; + +bootstrapApplication(AppComponent, { + providers: [ + provideWorkbench(), + provideRouter([]), // required by the SCION Workbench + provideAnimations(), // required by the SCION Workbench + ], +}); +``` +
@@ -57,7 +68,7 @@ Open `app.module.ts` and import the `WorkbenchModule`. The lines to be added are Open `app.component.html` and replace it with the following content: ```html - + ``` The workbench itself does not position nor lay out the `` component. Depending on your requirements, you may want the workbench to fill the entire page viewport or only parts of it, for example, if you have a header, footer, or navigation panel. @@ -89,9 +100,9 @@ Also, download the workbench icon font from SCION Workbench + +| SCION Workbench | [Projects Overview][menu-projects-overview] | [Changelog][menu-changelog] | [Contributing][menu-contributing] | [Sponsoring][menu-sponsoring] | +| --- | --- | --- | --- | --- | + +## [SCION Workbench][menu-home] > [How To Guides][menu-how-to] > View + +The view component can inject `WorkbenchView` to interact with the view, such as setting the title or closing the view. + +```ts +import {inject} from '@angular/core'; +import {WorkbenchView} from '@scion/workbench'; + +// Set the title. +inject(WorkbenchView).title = 'View title'; + +// Set the subtitle. +inject(WorkbenchView).heading = 'View Heading'; + +// Mark the view dirty. +inject(WorkbenchView).dirty = true; + +// Close the view. +inject(WorkbenchView).close(); + +// Test if the view is active. +const isActive = inject(WorkbenchView).active; + +// And more... +``` + +Some properties can also be defined on the route, such as title, heading or CSS class(es). + +```ts +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter} from '@angular/router'; +import {WorkbenchRouteData} from '@scion/workbench'; + +bootstrapApplication(AppComponent, { + providers: [ + provideRouter([ + { + path: 'path/to/view', + loadComponent: () => import('./view/view.component'), + data: { + [WorkbenchRouteData.title]: 'View Title', + [WorkbenchRouteData.heading]: 'View Heading', + [WorkbenchRouteData.cssClass]: ['class 1', 'class 2'], + }, + }, + ]), + ], +}); +```` + +[menu-how-to]: /docs/site/howto/how-to.md + +[menu-home]: /README.md +[menu-projects-overview]: /docs/site/projects-overview.md +[menu-changelog]: /docs/site/changelog.md +[menu-contributing]: /CONTRIBUTING.md +[menu-sponsoring]: /docs/site/sponsoring.md diff --git a/docs/site/howto/how-to-modify-layout.md b/docs/site/howto/how-to-modify-layout.md new file mode 100644 index 000000000..32bc49f7e --- /dev/null +++ b/docs/site/howto/how-to-modify-layout.md @@ -0,0 +1,37 @@ +SCION Workbench + +| SCION Workbench | [Projects Overview][menu-projects-overview] | [Changelog][menu-changelog] | [Contributing][menu-contributing] | [Sponsoring][menu-sponsoring] | +| --- | --- | --- | --- | --- | + +## [SCION Workbench][menu-home] > [How To Guides][menu-how-to] > Layout + +The workbench layout is a grid of parts. Parts are aligned relative to each other. A part is a stack of views. Content is displayed in views. + +### How to modify the workbench layout + +The workbench layout can be modified using the `navigate` method of the `WorkbenchRouter` by passing a function. The router will invoke this function with the current workbench layout. The layout has methods for modifying it. The layout is immutable, so each modification creates a new instance. Use the instance for further modifications and finally return it. + +The following example adds a part to the left of the main area, inserts a view and navigates it. + +```ts +import {inject} from '@angular/core'; +import {MAIN_AREA, WorkbenchRouter} from '@scion/workbench'; + +inject(WorkbenchRouter).navigate(layout => layout + .addPart('left', {relativeTo: MAIN_AREA, align: 'left'}) + .addView('navigator', {partId: 'left'}) + .navigateView('navigator', ['path/to/view']) + .activateView('navigator') +); +``` + +> The function can call `inject` to get any required dependencies. + + +[menu-how-to]: /docs/site/howto/how-to.md + +[menu-home]: /README.md +[menu-projects-overview]: /docs/site/projects-overview.md +[menu-changelog]: /docs/site/changelog.md +[menu-contributing]: /CONTRIBUTING.md +[menu-sponsoring]: /docs/site/sponsoring.md diff --git a/docs/site/howto/how-to-open-dialog.md b/docs/site/howto/how-to-open-dialog.md index 5ab318db2..2da0691e1 100644 --- a/docs/site/howto/how-to-open-dialog.md +++ b/docs/site/howto/how-to-open-dialog.md @@ -13,6 +13,9 @@ Displayed on top of other content, a dialog blocks interaction with other parts To open a dialog, inject `WorkbenchDialogService` and invoke the `open` method, passing the component to display. ```ts +import {inject} from '@angular/core'; +import {WorkbenchDialogService} from '@scion/workbench'; + const dialogService = inject(WorkbenchDialogService); dialogService.open(MyDialogComponent); @@ -24,6 +27,9 @@ A dialog can be view-modal or application-modal. A view-modal dialog blocks only By default, the calling context determines the modality of the dialog. If the dialog is opened from a view, only this view is blocked. To open the dialog with a different modality, specify the modality in the dialog options. ```ts +import {inject} from '@angular/core'; +import {WorkbenchDialogService} from '@scion/workbench'; + const dialogService = inject(WorkbenchDialogService); dialogService.open(MyDialogComponent, { @@ -31,16 +37,18 @@ dialogService.open(MyDialogComponent, { }); ``` -An application-modal dialog blocks the workbench element, still allowing interaction with elements outside the workbench element. To block the entire browser viewport, change the global modality scope setting in the workbench module configuration. +An application-modal dialog blocks the workbench element, still allowing interaction with elements outside the workbench element. To block the entire browser viewport, change the global modality scope setting in the workbench configuration. ```ts -import {WorkbenchModule} from '@scion/workbench'; - -WorkbenchModule.forRoot({ - dialog: { - modalityScope: 'viewport', - }, - ... // ommited configuration +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideWorkbench} from '@scion/workbench'; + +bootstrapApplication(AppComponent, { + providers: [ + provideWorkbench({ + dialog: {modalityScope: 'viewport'}, + }), + ], }); ``` @@ -49,6 +57,9 @@ Data can be passed to the dialog component as inputs in the dialog options. ```ts +import {inject} from '@angular/core'; +import {WorkbenchDialogService} from '@scion/workbench'; + const dialogService = inject(WorkbenchDialogService); dialogService.open(MyDialogComponent, { @@ -62,6 +73,8 @@ dialogService.open(MyDialogComponent, { Dialog inputs are available as input properties in the dialog component. ```ts +import {Component, Input} from '@angular/core'; + @Component({...}) export class MyDialogComponent { @@ -115,6 +128,9 @@ Alternatively, the dialog supports the use of a custom footer. To provide a cust The dialog component can inject the `WorkbenchDialog` handle and close the dialog, optionally passing a result to the dialog opener. ```ts +import {inject} from '@angular/core'; +import {WorkbenchDialog} from '@scion/workbench'; + // Closes the dialog. inject(WorkbenchDialog).close(); @@ -125,6 +141,9 @@ inject(WorkbenchDialog).close('some result'); Opening the dialog returns a Promise, that resolves to the result when the dialog is closed. ```ts +import {inject} from '@angular/core'; +import {WorkbenchDialog} from '@scion/workbench'; + const dialogService = inject(WorkbenchDialogService); const result = await dialogService.open(MyDialogComponent); @@ -134,6 +153,9 @@ const result = await dialogService.open(MyDialogComponent); The dialog handle can be used to specify a preferred size, displaying scrollbar(s) if the component overflows. If no size is specified, the dialog has the size of the component. ```ts +import {inject} from '@angular/core'; +import {WorkbenchDialog} from '@scion/workbench'; + // Sets a fixed size. inject(WorkbenchDialog).size.height = '500px'; inject(WorkbenchDialog).size.width = '600px'; @@ -159,6 +181,9 @@ By default, the dialog displays the title and a close button in the header. Alte The dialog component can inject the dialog handle `WorkbenchDialog` to interact with the dialog and change its default settings, such as making it non-closable, non-resizable, removing padding, and more. ```ts +import {inject} from '@angular/core'; +import {WorkbenchDialog} from '@scion/workbench'; + const dialog = inject(WorkbenchDialog); dialog.closable = false; dialog.resizable = false; diff --git a/docs/site/howto/how-to-open-message-box.md b/docs/site/howto/how-to-open-message-box.md index 70b6a18de..66ee7f514 100644 --- a/docs/site/howto/how-to-open-message-box.md +++ b/docs/site/howto/how-to-open-message-box.md @@ -14,6 +14,9 @@ Displayed on top of other content, a message box blocks interaction with other p To display a text message, inject `MessageBoxService` and invoke the `open` method, passing the text to display. ```ts +import {inject} from '@angular/core'; +import {WorkbenchMessageBoxService} from '@scion/workbench'; + inject(WorkbenchMessageBoxService).open('Lorem ipsum dolor sit amet.'); ``` @@ -23,6 +26,9 @@ To display structured content, pass a component instead of a string literal. Data can be passed to the component as inputs via the options object in the form of an object literal. Inputs are available as input properties in the component. ```ts +import {inject} from '@angular/core'; +import {WorkbenchMessageBoxService} from '@scion/workbench'; + const action = await inject(WorkbenchMessageBoxService).open(SomeComponent, { inputs: { a: '...', @@ -32,6 +38,8 @@ const action = await inject(WorkbenchMessageBoxService).open(SomeComponent, { ``` ```ts +import {Component, Input} from '@angular/core'; + @Component({...}) export class SomeComponent { @@ -50,6 +58,9 @@ Clicking a button closes the message box and resolves the Promise to the propert ```ts +import {inject} from '@angular/core'; +import {WorkbenchMessageBoxService} from '@scion/workbench'; + const action = await inject(WorkbenchMessageBoxService).open('Do you want to save changes?', { actions: { yes: 'Yes', @@ -69,6 +80,9 @@ A message box can be view-modal or application-modal. A view-modal message box b By default, the calling context determines the modality of the message box. If the message box is opened from a view, only this view is blocked. To open the message box with a different modality, specify the modality in the message box options. ```ts +import {inject} from '@angular/core'; +import {WorkbenchMessageBoxService} from '@scion/workbench'; + inject(WorkbenchMessageBoxService).open('Lorem ipsum dolor sit amet.', { modality: 'application', }); @@ -78,6 +92,9 @@ inject(WorkbenchMessageBoxService).open('Lorem ipsum dolor sit amet.', { A message can be displayed as info, warning or alert. The severity can be set via the options object. ```ts +import {inject} from '@angular/core'; +import {WorkbenchMessageBoxService} from '@scion/workbench'; + inject(WorkbenchMessageBoxService).open('Data could not be saved.', { severity: 'error', }); @@ -87,6 +104,9 @@ inject(WorkbenchMessageBoxService).open('Data could not be saved.', { A message box can have a title. The title is specified via the options object. ```ts +import {inject} from '@angular/core'; +import {WorkbenchMessageBoxService} from '@scion/workbench'; + inject(WorkbenchMessageBoxService).open('The view contains stale data.', { title: 'Stale Data', }); diff --git a/docs/site/howto/how-to-open-popup.md b/docs/site/howto/how-to-open-popup.md index 5ae636056..6f0840811 100644 --- a/docs/site/howto/how-to-open-popup.md +++ b/docs/site/howto/how-to-open-popup.md @@ -12,6 +12,9 @@ which can be either a coordinate or an HTML element. The popup moves when the an To open a popup, inject `PopupService` and invoke the `open` method, passing a `PopupConfig` options object to control the appearance of the popup. ```ts +import {inject} from '@angular/core'; +import {PopupService} from '@scion/workbench'; + const popupService = inject(PopupService); const result = await popupService.open({ @@ -25,6 +28,9 @@ To interact with the popup in the popup component, inject the popup handle `Popu ```typescript +import {inject} from '@angular/core'; +import {Popup} from '@scion/workbench'; + inject(Popup).close(); ``` diff --git a/docs/site/howto/how-to-open-view.md b/docs/site/howto/how-to-open-view.md index 21867b443..0ac231242 100644 --- a/docs/site/howto/how-to-open-view.md +++ b/docs/site/howto/how-to-open-view.md @@ -5,71 +5,125 @@ ## [SCION Workbench][menu-home] > [How To Guides][menu-how-to] > View -Similar to Angular, the workbench provides a router for view navigation. View navigation is based on Angular's routing mechanism and thus supports lazy component loading, resolvers, browser back/forward navigation, persistent navigation, and more. A view can inject `ActivatedRoute` to read path parameters, query parameters and data associated with the route. +Similar to Angular, the workbench provides a router for view navigation. View navigation is based on Angular's routing mechanism and thus supports lazy component loading, resolvers, browser back/forward navigation, persistent navigation, and more. A view can inject `ActivatedRoute` to obtain parameters passed to the navigation and/or read data associated with the route. ### How to open a view -A view is opened using the `WorkbenchRouter`. Like the Angular router, the workbench router has a `navigate` method that is passed an array of commands and optional navigation extras to control navigation. +A view is opened using the `WorkbenchRouter`. Like the Angular router, the workbench router has a `navigate` method that is passed an array of commands and optional navigation extras to control the navigation. ```ts -const wbRouter = inject(WorkbenchRouter); +import {inject} from '@angular/core'; +import {WorkbenchRouter} from '@scion/workbench'; -wbRouter.navigate(['path/to/view']); +inject(WorkbenchRouter).navigate(['path/to/view']); ``` -Navigation is absolute unless providing a `relativeTo` route in navigation extras. +The navigation is absolute unless providing a `relativeTo` route in navigation extras. ```ts -const wbRouter = inject(WorkbenchRouter); +import {inject} from '@angular/core'; +import {WorkbenchRouter} from '@scion/workbench'; +import {ActivatedRoute} from '@angular/router'; + const relativeTo = inject(ActivatedRoute); -// Relative navigation -wbRouter.navigate(['../path/to/view'], {relativeTo}); +// Navigate relative to a route. +inject(WorkbenchRouter).navigate(['../path/to/view'], {relativeTo}); ``` -The navigation can be passed additional data in the form of matrix params. Matrix params do not affect route or view resolution. The view can read matrix params from `ActivatedRoute.params`. +The navigation can be passed additional data in the form of matrix params and/or state. The view can read matrix params from `ActivatedRoute.params`, and state from `WorkbenchView.state`. ```ts -const wbRouter = inject(WorkbenchRouter); -const relativeTo = inject(ActivatedRoute); +import {inject} from '@angular/core'; +import {WorkbenchRouter} from '@scion/workbench'; +// Pass matrix parameters to the view. const matrixParams = {param1: 'value1', param2: 'value2'}; -wbRouter.navigate(['path/to/view', matrixParams]); +inject(WorkbenchRouter).navigate(['path/to/view', matrixParams]); + +// Pass state to the view. +inject(WorkbenchRouter).navigate(['path/to/view'], { + state: {some: 'state'} +}); ``` +Navigational state is stored in the browser's session history, supporting back/forward navigation, but is lost on page reload. Therefore, a view must be able to restore its state without relying on navigational state. + ### How to control the navigation target -By default, the router opens a new view if no view is found that matches the specified path. Matrix parameters do not affect view resolution. If a view matching the path is already open, it will be navigated instead of opening a new view, e.g., to bring it to the front or update matrix parameters. +By default, the router navigates existing views that match the path, or opens a new view otherwise. Matrix params do not affect view resolution. The default behavior can be overridden by specifying a `target` via navigation extras. -|Target|Explanation|Default| -|-|-|-| -|`auto`|Navigates existing view(s) that match the path, or opens a new view otherwise. Matrix params do not affect view resolution.|yes| -|`blank`|Navigates in a new view.|| -|``|Navigates the specified view. If already opened, replaces it, or opens a new view otherwise.|| +| Target | Explanation | Default | +|------------|---------------------------------------------------------------------------------------------------------------------------|---------| +| `auto` | Navigates existing views that match the path, or opens a new view otherwise. Matrix params do not affect view resolution. | yes | +| `blank` | Navigates in a new view. | | +| `` | Navigates the specified view. If already opened, replaces it, or opens a new view otherwise. | | + +### How to differentiate between routes with an identical path +The workbench router supports passing a hint to the navigation to differentiate between routes with an identical path. + +For example, views of the initial layout or a perspective are usually navigated to the empty path route to avoid cluttering the URL, +requiring a navigation hint to differentiate between the routes. + +Like the path, a hint affects view resolution. If set, the router will only navigate views with an equivalent hint, or if not set, views without a hint. + +The following example defines two empty path routes, using the `canMatchWorkbenchView` guard to match if navigating with a specific hint. + +```ts +import {inject} from '@angular/core'; +import {canMatchWorkbenchView, WorkbenchRouter} from '@scion/workbench'; +import {provideRouter} from '@angular/router'; +import {bootstrapApplication} from '@angular/platform-browser'; + +// Navigate to the empty path route, passing a hint to select the route of `OutlineComponent`. +inject(WorkbenchRouter).navigate([], {hint: 'outline'}); + +// Navigate to the empty path route, passing a hint to select the route of `NavigatorComponent`. +inject(WorkbenchRouter).navigate([], {hint: 'navigator'}); + +// Routes +bootstrapApplication(AppComponent, { + providers: [ + provideRouter([ + // Outline View + {path: '', canMatch: [canMatchWorkbenchView('outline')], component: OutlineComponent}, + // Navigator View + {path: '', canMatch: [canMatchWorkbenchView('navigator')], component: NavigatorComponent}, + ]), + ], +}); +``` ### How to navigate in a template The workbench provides the `wbRouterLink` directive for navigation in a template. The `wbRouterLink` directive is the workbench equivalent of the Angular `routerLink`. +Use this directive to navigate the current view. If the user presses the CTRL key (Mac: ⌘, Windows: ⊞), this directive will open a new view. + ```html Link ``` - -If in the context of a view in the main area and CTRL (Mac: ⌘, Windows: ⊞) key is not pressed, by default, navigation replaces the content of the current view. Override this default behavior by setting a view target strategy in navigation extras. +You can override the default behavior by setting an explicit navigation target in navigation extras. ```html Link ``` -By default, navigation is relative to the currently activated route, if any. Prepend the path with a forward slash `/` to navigate absolutely, or set `relativeTo` property in navigational extras to `null`. +By default, navigation is relative to the currently activated route, if any. + +Prepend the path with a forward slash `/` to navigate absolutely, or set `relativeTo` property in navigational extras to `null`. + +```html +Link +``` *** #### Related Links: - [Learn how to provide a view.][link-how-to-provide-view] -- [Learn how to define an initial layout.][link-how-to-define-initial-layout] +- [Learn how to define the initial workbench layout.][link-how-to-define-initial-workbench-layout] *** [link-how-to-provide-view]: /docs/site/howto/how-to-provide-view.md -[link-how-to-define-initial-layout]: /docs/site/howto/how-to-define-initial-layout.md +[link-how-to-define-initial-workbench-layout]: /docs/site/howto/how-to-define-initial-layout.md [menu-how-to]: /docs/site/howto/how-to.md [menu-home]: /README.md diff --git a/docs/site/howto/how-to-perspective.md b/docs/site/howto/how-to-perspective.md index ad1b07737..6807f43f7 100644 --- a/docs/site/howto/how-to-perspective.md +++ b/docs/site/howto/how-to-perspective.md @@ -32,11 +32,12 @@ export class PerspectivesComponent { ``` ```html - +@for (perspective of workbenchService.perspectives$ | async; track perspective) { + +} ``` *** diff --git a/docs/site/howto/how-to-prevent-view-closing.md b/docs/site/howto/how-to-prevent-view-closing.md index 8b6e9f665..25f8b0369 100644 --- a/docs/site/howto/how-to-prevent-view-closing.md +++ b/docs/site/howto/how-to-prevent-view-closing.md @@ -5,28 +5,23 @@ ## [SCION Workbench][menu-home] > [How To Guides][menu-how-to] > View -### How to prevent a view from being closed +### How to prevent a view from closing -The closing of a view can be intercepted by implementing the `WorkbenchViewPreDestroy` lifecycle hook in the view component. The `onWorkbenchViewPreDestroy` method is called when the view is about to be closed. Return `true` to continue closing or `false` otherwise. Alternatively, you can return a Promise or Observable to perform an asynchronous operation such as displaying a message box. +A view can implement the `CanClose` interface to intercept or prevent the closing. + +The `canClose` method is called when the view is about to close. Return `true` to close the view or `false` to prevent closing. Instead of a `boolean`, the method can return a `Promise` or an `Observable` to perform an asynchronous operation, such as displaying a message box. The following snippet asks the user whether to save changes. ```ts -@Component({}) -export class ViewComponent implements WorkbenchViewPreDestroy { - - constructor(private view: WorkbenchView, private messageBoxService: MessageBoxService) { - } +import {Component, inject} from '@angular/core'; +import {CanClose, WorkbenchMessageBoxService} from '@scion/workbench'; - public async onWorkbenchViewPreDestroy(): Promise { - if (!this.view.dirty) { - return true; - } +@Component({...}) +class ViewComponent implements CanClose { - const messageBoxService = inject(MessageBoxService); - const action = await messageBoxService.open({ - content: 'Do you want to save changes?', - severity: 'info', + public async canClose(): Promise { + const action = await inject(WorkbenchMessageBoxService).open('Do you want to save changes?', { actions: { yes: 'Yes', no: 'No', diff --git a/docs/site/howto/how-to-open-initial-view.md b/docs/site/howto/how-to-provide-not-found-page.md similarity index 52% rename from docs/site/howto/how-to-open-initial-view.md rename to docs/site/howto/how-to-provide-not-found-page.md index e0fc2d456..6af95691f 100644 --- a/docs/site/howto/how-to-open-initial-view.md +++ b/docs/site/howto/how-to-provide-not-found-page.md @@ -5,23 +5,21 @@ ## [SCION Workbench][menu-home] > [How To Guides][menu-how-to] > View -### How to open an initial view in the main area -The workbench supports listening for the opened views. If the number of views is zero or drops to zero, perform a navigation to open the initial view. Also, consider configuring the initial view as non-closable. +The workbench displays a "Not Found Page" if no route matches the requested URL. This happens when navigating to a route that does not exist or when loading the application, and the routes have changed since the last use. + +The built-in "Not Found Page" can be replaced, e.g., to localize the page, as follows: ```ts -@Component({selector: 'app-root'}) -export class AppComponent { - - constructor(workbenchService: WorkbenchService, wbRouter: WorkbenchRouter) { - workbenchService.views$ - .pipe(takeUntilDestroyed()) - .subscribe(views => { - if (views.length === 0) { - wbRouter.navigate(['path/to/view']); - } - }); - } -} +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideWorkbench} from '@scion/workbench'; + +bootstrapApplication(AppComponent, { + providers: [ + provideWorkbench({ + pageNotFoundComponent: YourPageNotFoundComponent, + }), + ], +}); ``` [menu-how-to]: /docs/site/howto/how-to.md diff --git a/docs/site/howto/how-to-provide-part-action.md b/docs/site/howto/how-to-provide-part-action.md index b04a92eef..b3915f32e 100644 --- a/docs/site/howto/how-to-provide-part-action.md +++ b/docs/site/howto/how-to-provide-part-action.md @@ -32,9 +32,11 @@ Add a `` to an HTML template and decorate it with the `wbPartAction As an alternative to modeling an action in HTML templates, actions can be contributed programmatically using the `WorkbenchService.registerPartAction` method. The content is specified in the form of a CDK portal, i.e., a component portal or a template portal. ```ts -const workbenchService = inject(WorkbenchService); +import {inject} from '@angular/core'; +import {WorkbenchService} from '@scion/workbench'; +import {ComponentPortal} from '@angular/cdk/portal'; -workbenchService.registerPartAction({ +inject(WorkbenchService).registerPartAction({ portal: new ComponentPortal(YourComponent), align: 'end', }); @@ -56,6 +58,9 @@ The action can be configured with a `canMatch` function to match a specific part The following function contributes the action only to parts in the perspective 'MyPerspective' located in the main area. ```ts +import {inject} from '@angular/core'; +import {CanMatchPartFn, WorkbenchPart, WorkbenchService} from '@scion/workbench'; + public canMatch: CanMatchPartFn = (part: WorkbenchPart): boolean => { if (!inject(WorkbenchService).getPerspective('MyPerspective')?.active) { return false; diff --git a/docs/site/howto/how-to-provide-perspective.md b/docs/site/howto/how-to-provide-perspective.md index 894eb38a7..998e8ece0 100644 --- a/docs/site/howto/how-to-provide-perspective.md +++ b/docs/site/howto/how-to-provide-perspective.md @@ -7,65 +7,104 @@ A perspective is a named workbench layout. Multiple perspectives are supported. Perspectives can be switched. Only one perspective is active at a time. Perspectives share the same main area, if any. +The workbench layout is a grid of parts. Parts are aligned relative to each other. A part is a stack of views. Content is displayed in views. + ### How to provide a perspective -Providing a perspective requires two steps. +Perspectives are registered similarly to [Defining the initial workbench layout][link-how-to-define-initial-workbench-layout] via the configuration passed to `provideWorkbench()`. However, an array of perspective definitions is passed instead of a single workbench layout. A perspective must have a unique identity and define a workbench layout. Optionally, data can be associated with the perspective via data dictionary, e.g., to associate an icon, label or tooltip with the perspective. -
- 1. Register the perspective via workbench config -
+Define the perspective's layout by registering a layout function in the perspective definition. The workbench will invoke this function with a factory to create the layout. The layout is immutable, so each modification creates a new instance. Use the instance for further modifications and finally return it. -Perspectives are registered similarly to [Defining an initial layout][link-how-to-define-initial-layout] via the configuration passed to `WorkbenchModule.forRoot()`. However, an array of perspective definitions is passed instead of a single layout. A perspective must have a unique identity and define a layout. Optionally, data can be associated with the perspective via data dictionary, e.g., to associate an icon, label or tooltip with the perspective. +Start by adding the first part. From there, you can gradually add more parts and align them relative to each other. Next, add views to the layout, specifying to which part to add the views. The final step is to navigate the views. A view can be navigated to any route. ```ts -import {MAIN_AREA, WorkbenchLayoutFactory, WorkbenchModule} from '@scion/workbench'; - -WorkbenchModule.forRoot({ - layout: { - perspectives: [ - { - id: 'admin', - layout: (factory: WorkbenchLayoutFactory) => factory - .addPart(MAIN_AREA) - .addPart('topLeft', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) - .addPart('bottomLeft', {relativeTo: 'topLeft', align: 'bottom', ratio: .5}) - .addPart('bottom', {align: 'bottom', ratio: .3}) - .addView('navigator', {partId: 'topLeft', activateView: true}) - .addView('explorer', {partId: 'topLeft'}) - .addView('outline', {partId: 'bottomLeft', activateView: true}) - .addView('console', {partId: 'bottom', activateView: true}) - .addView('problems', {partId: 'bottom'}) - .addView('search', {partId: 'bottom'}), - data: { - label: 'Administrator', - }, - }, - { - id: 'manager', - layout: (factory: WorkbenchLayoutFactory) => factory - .addPart(MAIN_AREA) - .addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom', ratio: .3}) - .addView('explorer', {partId: 'bottom', activateView: true}) - .addView('navigator', {partId: 'bottom'}) - .addView('outline', {partId: 'bottom'}) - .addView('search', {partId: 'bottom'}), - data: { - label: 'Manager', - }, +import {bootstrapApplication} from '@angular/platform-browser'; +import {MAIN_AREA, provideWorkbench, WorkbenchLayoutFactory} from '@scion/workbench'; + +bootstrapApplication(AppComponent, { + providers: [ + provideWorkbench({ + layout: { + perspectives: [ + { + id: 'admin', + layout: (factory: WorkbenchLayoutFactory) => factory + // Add parts to the layout. + .addPart(MAIN_AREA) + .addPart('topLeft', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) + .addPart('bottomLeft', {relativeTo: 'topLeft', align: 'bottom', ratio: .5}) + .addPart('bottom', {align: 'bottom', ratio: .3}) + + // Add views to the layout. + .addView('navigator', {partId: 'topLeft'}) + .addView('explorer', {partId: 'topLeft'}) + .addView('outline', {partId: 'bottomLeft'}) + .addView('console', {partId: 'bottom'}) + .addView('problems', {partId: 'bottom'}) + .addView('search', {partId: 'bottom'}) + + // Navigate views. + .navigateView('navigator', ['path/to/navigator']) + .navigateView('explorer', ['path/to/explorer']) + .navigateView('outline', [], {hint: 'outline'}) // Set hint to differentiate between routes with an empty path. + .navigateView('console', [], {hint: 'console'}) // Set hint to differentiate between routes with an empty path. + .navigateView('problems', [], {hint: 'problems'}) // Set hint to differentiate between routes with an empty path. + .navigateView('search', ['path/to/search']) + + // Decide which views to activate. + .activateView('navigator') + .activateView('outline') + .activateView('console'), + data: { + label: 'Administrator', + }, + }, + { + id: 'manager', + layout: (factory: WorkbenchLayoutFactory) => factory + // Add parts to the layout. + .addPart(MAIN_AREA) + .addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom', ratio: .3}) + + // Add views to the layout. + .addView('navigator', {partId: 'bottom'}) + .addView('explorer', {partId: 'bottom'}) + .addView('outline', {partId: 'bottom'}) + .addView('search', {partId: 'bottom'}) + + // Navigate views. + .navigateView('navigator', ['path/to/navigator']) + .navigateView('explorer', ['path/to/explorer']) + .navigateView('outline', [], {hint: 'outline'}) // Set hint to differentiate between routes with an empty path. + .navigateView('search', ['path/to/search']) + + // Decide which views to activate. + .activateView('explorer'), + data: { + label: 'Manager', + }, + }, + ], + initialPerspective: 'manager', }, - ], - initialPerspective: 'manager', - }, + }), + ], }); ``` -The perspective 'admin' defines the following layout. +> The layout function can call `inject` to get any required dependencies. + +> A `canActivate` function can be configured to determine if the perspective can be activated, for example based on the user's permissions. + +> The initial perspective can be set via `initialPerspective` property which accepts a string literal or a function for more advanced use cases. + +The perspective `admin` defines the following layout. ```plain +--------+----------------+ -| top | main area | +| top | | | left | | -|--------+ | +|--------+ main area | | bottom | | | left | | +--------+----------------+ @@ -73,48 +112,52 @@ The perspective 'admin' defines the following layout. +-------------------------+ ``` -The perspective 'manager' defines the following layout. +The perspective `manager` defines the following layout. ```plain +-------------------------+ -| main area | | | +| main area | | | +-------------------------+ | bottom | +-------------------------+ ``` -The workbench layout is a grid of parts. Parts are aligned relative to each other. A part is a stack of views. Content is displayed in views. - -A layout is defined through a layout function in the workbench config. The function is passed a factory to create the layout. The layout has methods to modify it. Each modification creates a new layout instance that can be used for further modifications. - -> The function can call `inject` to get required dependencies, if any. - -Optionally, a `canActivate` function can be configured with a perspective descriptor to determine whether the perspective can be activated, for example based on the user's permissions. The initial activated perspective can be set via `initialPerspective` property which accepts a string literal or a function for more advanced use cases. -
- -
- 2. Register the routes for views added to the perspectives -
+The above perspectives require the following routes. -Routes for views added to the perspective layouts must be registered via the router module, as follows: - ```ts -RouterModule.forRoot([ - {path: '', outlet: 'navigator', loadComponent: () => import('./navigator/navigator.component')}, - {path: '', outlet: 'explorer', loadComponent: () => import('./explorer/explorer.component')}, - {path: '', outlet: 'outline', loadComponent: () => import('./outline/outline.component')}, - {path: '', outlet: 'console', loadComponent: () => import('./console/console.component')}, - {path: '', outlet: 'problems', loadComponent: () => import('./problems/problems.component')}, - {path: '', outlet: 'search', loadComponent: () => import('./search/search.component')}, -]); +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter} from '@angular/router'; +import {canMatchWorkbenchView} from '@scion/workbench'; + +bootstrapApplication(AppComponent, { + providers: [ + provideRouter([ + // Navigator View + {path: 'path/to/navigator', loadComponent: () => import('./navigator/navigator.component')}, + // Explorer View + {path: 'path/to/explorer', loadComponent: () => import('./explorer/explorer.component')}, + // Outline View + {path: '', canMatch: [canMatchWorkbenchView('outline')], loadComponent: () => import('./outline/outline.component')}, + // Console View + {path: '', canMatch: [canMatchWorkbenchView('console')], loadComponent: () => import('./console/console.component')}, + // Problems View + {path: '', canMatch: [canMatchWorkbenchView('problems')], loadComponent: () => import('./problems/problems.component')}, + // Search View + {path: 'path/to/search', loadComponent: () => import('./search/search.component')}, + ]), + ], +}); ``` -A route for a view in the perspective layout must be a secondary route with an empty path. The outlet refers to the view in the layout. Because the path is empty, no outlet needs to be added to the URL. -
+> To avoid cluttering the initial URL, we recommend navigating the views of a perspective to empty path routes and using a navigation hint to differentiate. + +> Use the `canMatchWorkbenchView` guard to match a route only when navigating a view with a particular hint. + +> Use the `canMatchWorkbenchView` guard and pass `false` to never match a route for a workbench view, e.g., to exclude the application root path, if any, necessary when navigating views to the empty path route. -[link-how-to-define-initial-layout]: /docs/site/howto/how-to-define-initial-layout.md +[link-how-to-define-initial-workbench-layout]: /docs/site/howto/how-to-define-initial-layout.md [menu-how-to]: /docs/site/howto/how-to.md diff --git a/docs/site/howto/how-to-provide-view.md b/docs/site/howto/how-to-provide-view.md index 47e0d7d0c..eaaacd853 100644 --- a/docs/site/howto/how-to-provide-view.md +++ b/docs/site/howto/how-to-provide-view.md @@ -9,15 +9,23 @@ Any component can be displayed as a view. A view is a regular Angular component associated with a route. Below are some examples of common route configurations. ```ts -// Routes -RouterModule.forRoot([ - {path: 'path/to/view1', component: ViewComponent}, - {path: 'path/to/view2', loadComponent: () => import('./view/view.component')}, // lazy loaded route - {path: 'path/to/views', loadChildren: () => import('./routes')}, // lazy loaded routes -]); +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter} from '@angular/router'; + +bootstrapApplication(AppComponent, { + providers: [ + provideRouter([ + {path: 'path/to/view1', component: ViewComponent}, + {path: 'path/to/view2', loadComponent: () => import('./view/view.component')}, // lazy loaded route + {path: 'path/to/views', loadChildren: () => import('./routes')}, // lazy loaded routes + ]), + ], +}); ``` ```ts +import {Routes} from '@angular/router'; + // file: routes.ts export default [ {path: 'view3', component: ViewComponent}, @@ -28,6 +36,9 @@ export default [ Having defined the routes, views can be opened using the `WorkbenchRouter`. ```ts +import {WorkbenchRouter} from '@scion/workbench'; +import {inject} from '@angular/core'; + // Open view 1 inject(WorkbenchRouter).navigate(['/path/to/view1']); @@ -41,30 +52,41 @@ inject(WorkbenchRouter).navigate(['/path/to/views/view3']); inject(WorkbenchRouter).navigate(['/path/to/views/view4']); ``` -The workbench supports associating view-specific data with a route, such as a tile, a heading, or a CSS class. Alternatively, this data can be set in the view by injecting the view handle `WorkbenchView`. +The workbench supports associating view-specific data with a route, such as a tile, a heading, or a CSS class. ```ts -RouterModule.forRoot([ - { - path: 'path/to/view', - loadComponent: () => import('./view/view.component'), - data: { - [WorkbenchRouteData.title]: 'View Title', - [WorkbenchRouteData.heading]: 'View Heading', - [WorkbenchRouteData.cssClass]: 'e2e-view', - }, - }, -]) +import {bootstrapApplication} from '@angular/platform-browser'; +import {provideRouter} from '@angular/router'; +import {WorkbenchRouteData} from '@scion/workbench'; + +bootstrapApplication(AppComponent, { + providers: [ + provideRouter([ + { + path: 'path/to/view', + loadComponent: () => import('./view/view.component'), + data: { + [WorkbenchRouteData.title]: 'View Title', + [WorkbenchRouteData.heading]: 'View Heading', + [WorkbenchRouteData.cssClass]: ['class 1', 'class 2'], + }, + }, + ]), + ], +}); ``` +Alternatively, the above data can be set in the view by injecting the view handle `WorkbenchView`. See [How to interact with a view][how-to-interact-with-view]. + *** #### Related Links: - [Learn how to open a view.][link-how-to-open-view] -- [Learn how to define an initial layout.][link-how-to-define-initial-layout] +- [Learn how to define the initial workbench layout.][link-how-to-define-initial-workbench-layout] *** [link-how-to-open-view]: /docs/site/howto/how-to-open-view.md -[link-how-to-define-initial-layout]: /docs/site/howto/how-to-define-initial-layout.md +[link-how-to-define-initial-workbench-layout]: /docs/site/howto/how-to-define-initial-layout.md +[how-to-interact-with-view]: /docs/site/howto/how-to-interact-with-view.md [menu-how-to]: /docs/site/howto/how-to.md [menu-home]: /README.md diff --git a/docs/site/howto/how-to-set-view-title.md b/docs/site/howto/how-to-set-view-title.md deleted file mode 100644 index 6bc9d67e1..000000000 --- a/docs/site/howto/how-to-set-view-title.md +++ /dev/null @@ -1,41 +0,0 @@ -SCION Workbench - -| SCION Workbench | [Projects Overview][menu-projects-overview] | [Changelog][menu-changelog] | [Contributing][menu-contributing] | [Sponsoring][menu-sponsoring] | -| --- | --- | --- | --- | --- | - -## [SCION Workbench][menu-home] > [How To Guides][menu-how-to] > View - -### How to set a view title -Title and heading of a view can be set either in the route definition or in the view by injecting the view handle `WorkbenchView`. - -#### Set view title in route definition -Associate the route with view title and heading, as follows: - -```ts -RouterModule.forRoot([ - { - path: 'path/to/view', - loadComponent: () => import('./view/view.component'), - data: { - [WorkbenchRouteData.title]: 'View Title', - [WorkbenchRouteData.heading]: 'View Heading', - }, - }, -]) -``` - -#### Set view title in view component -Inject `WorkbenchView` and set title and heading, as follows: - -```typescript -inject(WorkbenchView).title = 'View title'; -inject(WorkbenchView).heading = 'View Heading'; -``` - -[menu-how-to]: /docs/site/howto/how-to.md - -[menu-home]: /README.md -[menu-projects-overview]: /docs/site/projects-overview.md -[menu-changelog]: /docs/site/changelog.md -[menu-contributing]: /CONTRIBUTING.md -[menu-sponsoring]: /docs/site/sponsoring.md diff --git a/docs/site/howto/how-to-show-notification.md b/docs/site/howto/how-to-show-notification.md index 54e64eae2..4086e8eea 100644 --- a/docs/site/howto/how-to-show-notification.md +++ b/docs/site/howto/how-to-show-notification.md @@ -11,6 +11,9 @@ A notification is a closable message that appears in the upper-right corner and To show a notification, inject `NotificationService` and invoke the `notify` method, passing a `Notification` options object to control the appearance of the notification, like its severity, its content and show duration. ```ts +import {inject} from '@angular/core'; +import {NotificationService} from '@scion/workbench'; + const notificationService = inject(NotificationService); notificationService.notify({ content: 'Person successfully created', diff --git a/docs/site/howto/how-to.md b/docs/site/howto/how-to.md index e82d9afda..5e04d89be 100644 --- a/docs/site/howto/how-to.md +++ b/docs/site/howto/how-to.md @@ -15,19 +15,20 @@ We are working on a comprehensive guide that explains the features and concepts - [How to install the SCION Workbench](how-to-install-workbench.md) #### Layout -- [How to define an initial layout](how-to-define-initial-layout.md) +- [How to define the initial workbench layout](how-to-define-initial-layout.md) +- [How to modify the workbench layout](how-to-modify-layout.md) - [How to provide a perspective](how-to-provide-perspective.md) +- [How to work with perspectives](how-to-perspective.md) - [How to configure a start page](how-to-configure-start-page.md) -- [How to query, switch and reset perspectives](how-to-perspective.md) #### View - [How to provide a view](how-to-provide-view.md) - [How to open a view](how-to-open-view.md) -- [How to open an initial view in the main area](how-to-open-initial-view.md) -- [How to set a view title](how-to-set-view-title.md) +- [How to interact with a view](how-to-interact-with-view.md) - [How to close a view](how-to-close-view.md) -- [How to prevent a view from being closed](how-to-prevent-view-closing.md) +- [How to prevent a view from closing](how-to-prevent-view-closing.md) - [How to provide a part action](how-to-provide-part-action.md) +- [How to provide a "Not Found Page"](how-to-provide-not-found-page.md) #### Dialog, Popup, Messagebox, Notification - [How to open a dialog](how-to-open-dialog.md) diff --git a/docs/site/overview.md b/docs/site/overview.md index a050ebb40..d66ca14c1 100644 --- a/docs/site/overview.md +++ b/docs/site/overview.md @@ -11,19 +11,21 @@ The workbench layout is a grid of parts. Parts are aligned relative to each othe The layout can be divided into a main and a peripheral area, with the main area as the primary place for opening views. The peripheral area arranges parts around the main area to provide navigation or context-sensitive assistance to support the user's workflow. Defining a main area is optional and recommended for applications requiring a dedicated and maximizable area for user interaction. -Multiple layouts, called perspectives, are supported. Perspectives can be switched with one perspective active at a time. Perspectives share the same main area, if any. +Multiple layouts, called perspectives, are supported. Perspectives can be switched. Only one perspective is active at a time. Perspectives share the same main area, if any. [](https://github.com/SchweizerischeBundesbahnen/scion-workbench/raw/master/docs/site/images/workbench-layout-parts.svg) #### Developer Experience -The SCION Workbench integrates seamlessly with Angular, leveraging familiar Angular APIs and concepts. It is designed to have minimal impact on development. Developing with the SCION Workbench is as straightforward as developing a regular Angular application. Workbench views are registered as primary routes that can be navigated using the router. Data is passed to views through navigation, either as path or matrix parameters. A view can read passed data from `ActivatedRoute`. +The SCION Workbench is built on top of Angular and is designed to have minimal impact on application development. The Workbench API is based on familiar Angular concepts, making development straightforward. -Because the workbench navigation is fully based on the Angular Router, the application can continue to leverage the rich and powerful features of the Angular Router, such as lazy component loading, resolvers, browser back/forward navigation, persistent navigation, and more. Dependency on SCION is minimal. +Any component can be opened as a view. A view is a regular Angular component associated with a route. Views are navigated using the Workbench Router. Like the Angular Router, it has a `navigate` method to open views or change the workbench layout. Data is passed to views through navigation. A view can read data from its `ActivatedRoute`. + +Because SCION Workbench uses Angular's routing mechanism to navigate and lay out views, the application can harness Angular's extensive routing capabilities, such as lazy component loading, resolvers, browser back/forward navigation, persistent navigation, and more. #### Integration into Angular SCION Workbench integrates with the Angular Router to perform layout changes and populate views, enabling persistent and backward/forward navigation. -A view is a named router outlet that is filled based on the current Angular router state. For all top-level primary routes, SCION Workbench registers view-specific secondary routes, allowing routing on a per-view basis. +A view is a named router outlet that is filled based on the current Angular router state. The SCION Workbench registers view-specific auxiliary routes for all routes, enabling routing on a per-view basis. The browser URL contains the path and arrangement of views in the main area. The arrangement of views outside the main area is passed as state to the navigation and stored in local storage. The figure below shows the browser URL when there are 3 views opened in the main area. For each view, Angular adds an auxiliary route to the URL. An auxiliary route consists of the view identifier and the path. Multiple views are separated by two slashes. diff --git a/package-lock.json b/package-lock.json index 1b81fb171..1aed092e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,15 +8,15 @@ "hasInstallScript": true, "license": "EPL-2.0", "dependencies": { - "@angular/animations": "17.0.3", - "@angular/cdk": "17.0.1", - "@angular/common": "17.0.3", - "@angular/compiler": "17.0.3", - "@angular/core": "17.0.3", - "@angular/forms": "17.0.3", - "@angular/platform-browser": "17.0.3", - "@angular/platform-browser-dynamic": "17.0.3", - "@angular/router": "17.0.3", + "@angular/animations": "17.0.6", + "@angular/cdk": "17.0.6", + "@angular/common": "17.0.6", + "@angular/compiler": "17.0.6", + "@angular/core": "17.0.6", + "@angular/forms": "17.0.6", + "@angular/platform-browser": "17.0.6", + "@angular/platform-browser-dynamic": "17.0.6", + "@angular/router": "17.0.6", "@scion/components": "17.0.0", "@scion/components.internal": "17.0.0", "@scion/microfrontend-platform": "1.2.2", @@ -26,14 +26,14 @@ "zone.js": "0.14.2" }, "devDependencies": { - "@angular-devkit/build-angular": "17.1.1", + "@angular-devkit/build-angular": "17.0.6", "@angular-eslint/builder": "17.1.0", "@angular-eslint/eslint-plugin": "17.1.0", "@angular-eslint/eslint-plugin-template": "17.1.0", "@angular-eslint/schematics": "17.1.0", "@angular-eslint/template-parser": "17.1.0", - "@angular/cli": "17.0.1", - "@angular/compiler-cli": "17.0.3", + "@angular/cli": "17.0.6", + "@angular/compiler-cli": "17.0.6", "@playwright/test": "1.40.0", "@types/jasmine": "5.1.2", "@typescript-eslint/eslint-plugin": "6.11.0", @@ -82,12 +82,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1700.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.1.tgz", - "integrity": "sha512-w84luzQNRjlt7XxX3+jyzcwBBv3gAjjvFWTjN1E5mlpDCUXgYmQ3CMowFHeu0U06HD5Sapap9p2l6GoajuZK5Q==", + "version": "0.1700.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1700.6.tgz", + "integrity": "sha512-zVpz736cBZHXcv0v2bRLfJLcykanUyEMVQXkGwZp2eygjNK1Ls9s/74o1dXd6nGdvjh6AnkzOU/vouj2dqA41g==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.1", + "@angular-devkit/core": "17.0.6", "rxjs": "7.8.1" }, "engines": { @@ -97,40 +97,42 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.1.1.tgz", - "integrity": "sha512-GchDM8H+RQNts731c+jnhDgOm0PnCS3YB12uVwRiGsaNsUMrqKnu3P0poh6AImDMPyXKnIvTWLDCMD8TDziR0A==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.0.6.tgz", + "integrity": "sha512-gYxmbvq5/nk7aVJ6JxIIW0//RM7859kMPJGPKekcCGSWkkObjqG6P5cDgJJNAjMl/IfCsG7B+xGYjr4zN8QV9g==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.1", - "@angular-devkit/architect": "0.1701.1", - "@angular-devkit/build-webpack": "0.1701.1", - "@angular-devkit/core": "17.1.1", - "@babel/core": "7.23.7", - "@babel/generator": "7.23.6", + "@angular-devkit/architect": "0.1700.6", + "@angular-devkit/build-webpack": "0.1700.6", + "@angular-devkit/core": "17.0.6", + "@babel/core": "7.23.2", + "@babel/generator": "7.23.0", "@babel/helper-annotate-as-pure": "7.22.5", "@babel/helper-split-export-declaration": "7.22.6", - "@babel/plugin-transform-async-generator-functions": "7.23.7", - "@babel/plugin-transform-async-to-generator": "7.23.3", - "@babel/plugin-transform-runtime": "7.23.7", - "@babel/preset-env": "7.23.7", - "@babel/runtime": "7.23.7", + "@babel/plugin-transform-async-generator-functions": "7.23.2", + "@babel/plugin-transform-async-to-generator": "7.22.5", + "@babel/plugin-transform-runtime": "7.23.2", + "@babel/preset-env": "7.23.2", + "@babel/runtime": "7.23.2", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "17.1.1", - "@vitejs/plugin-basic-ssl": "1.0.2", + "@ngtools/webpack": "17.0.6", + "@vitejs/plugin-basic-ssl": "1.0.1", "ansi-colors": "4.1.3", "autoprefixer": "10.4.16", "babel-loader": "9.1.3", "babel-plugin-istanbul": "6.1.1", + "browser-sync": "2.29.3", "browserslist": "^4.21.5", + "chokidar": "3.5.3", "copy-webpack-plugin": "11.0.0", "critters": "0.0.20", "css-loader": "6.8.1", - "esbuild-wasm": "0.19.11", - "fast-glob": "3.3.2", + "esbuild-wasm": "0.19.5", + "fast-glob": "3.3.1", "http-proxy-middleware": "2.0.6", "https-proxy-agent": "7.0.2", - "inquirer": "9.2.12", + "inquirer": "9.2.11", "jsonc-parser": "3.2.0", "karma-source-map-support": "1.4.0", "less": "4.2.0", @@ -139,28 +141,27 @@ "loader-utils": "3.2.1", "magic-string": "0.30.5", "mini-css-extract-plugin": "2.7.6", - "mrmime": "2.0.0", + "mrmime": "1.0.1", "open": "8.4.2", "ora": "5.4.1", "parse5-html-rewriting-stream": "7.0.0", "picomatch": "3.0.1", - "piscina": "4.2.1", - "postcss": "8.4.33", - "postcss-loader": "7.3.4", + "piscina": "4.1.0", + "postcss": "8.4.31", + "postcss-loader": "7.3.3", "resolve-url-loader": "5.0.0", "rxjs": "7.8.1", - "sass": "1.69.7", - "sass-loader": "13.3.3", + "sass": "1.69.5", + "sass-loader": "13.3.2", "semver": "7.5.4", - "source-map-loader": "5.0.0", + "source-map-loader": "4.0.1", "source-map-support": "0.5.21", - "terser": "5.26.0", + "terser": "5.24.0", "text-table": "0.2.0", "tree-kill": "1.2.2", "tslib": "2.6.2", - "undici": "6.2.1", - "vite": "5.0.12", - "watchpack": "2.4.0", + "undici": "5.27.2", + "vite": "4.5.1", "webpack": "5.89.0", "webpack-dev-middleware": "6.1.1", "webpack-dev-server": "4.15.1", @@ -173,22 +174,20 @@ "yarn": ">= 1.13.0" }, "optionalDependencies": { - "esbuild": "0.19.11" + "esbuild": "0.19.5" }, "peerDependencies": { "@angular/compiler-cli": "^17.0.0", "@angular/localize": "^17.0.0", "@angular/platform-server": "^17.0.0", "@angular/service-worker": "^17.0.0", - "@web/test-runner": "^0.18.0", - "browser-sync": "^3.0.2", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", "karma": "^6.3.0", "ng-packagr": "^17.0.0", "protractor": "^7.0.0", "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.2 <5.4" + "typescript": ">=5.2 <5.3" }, "peerDependenciesMeta": { "@angular/localize": { @@ -200,12 +199,6 @@ "@angular/service-worker": { "optional": true }, - "@web/test-runner": { - "optional": true - }, - "browser-sync": { - "optional": true - }, "jest": { "optional": true }, @@ -226,138 +219,13 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { - "version": "0.1701.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1701.1.tgz", - "integrity": "sha512-vT3ZRAIfNyIg0vJWT6umPbCKiKFCukNkxLe9kgOU0tinZKNr/LgWYaBZ92Rxxi6C3NrAnmAYjsih8x4zdyoRXw==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "17.1.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.1.1.tgz", - "integrity": "sha512-b1wd1caegc1p18nTrfPhfHQAZW1GnWWKGldq5MZ8C/nkgJbjjN8SKb1Vw7GONkOnH6KxWDAXS4i93/wdQcz4Bg==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "picomatch": "3.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@babel/core": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.7.tgz", - "integrity": "sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.23.7", - "@babel/parser": "^7.23.6", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.23.7", - "@babel/types": "^7.23.6", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/@angular-devkit/build-angular/node_modules/inquirer": { - "version": "9.2.12", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", - "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", - "dev": true, - "dependencies": { - "@ljharb/through": "^2.3.11", - "ansi-escapes": "^4.3.2", - "chalk": "^5.3.0", - "cli-cursor": "^3.1.0", - "cli-width": "^4.1.0", - "external-editor": "^3.1.0", - "figures": "^5.0.0", - "lodash": "^4.17.21", - "mute-stream": "1.0.0", - "ora": "^5.4.1", - "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=14.18.0" - } - }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1701.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1701.1.tgz", - "integrity": "sha512-YgNl/6xLmI0XdUCu/H4Jyi34BhrANCDP4N2Pz+tGwnz2+Vl8oZGLPGtKVbh/LKSAmEHk/B6GQUekSBpKWrPJoA==", + "version": "0.1700.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1700.6.tgz", + "integrity": "sha512-xT5LL92rScVjvGZO7but/YbTQ12PNilosyjDouephl+HIf2V6rwDovTsEfpLYgcrqgodh+R0X0ZCOk95+MmSBA==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1701.1", + "@angular-devkit/architect": "0.1700.6", "rxjs": "7.8.1" }, "engines": { @@ -370,52 +238,10 @@ "webpack-dev-server": "^4.0.0" } }, - "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": { - "version": "0.1701.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1701.1.tgz", - "integrity": "sha512-vT3ZRAIfNyIg0vJWT6umPbCKiKFCukNkxLe9kgOU0tinZKNr/LgWYaBZ92Rxxi6C3NrAnmAYjsih8x4zdyoRXw==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "17.1.1", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/core": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.1.1.tgz", - "integrity": "sha512-b1wd1caegc1p18nTrfPhfHQAZW1GnWWKGldq5MZ8C/nkgJbjjN8SKb1Vw7GONkOnH6KxWDAXS4i93/wdQcz4Bg==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "picomatch": "3.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, "node_modules/@angular-devkit/core": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.1.tgz", - "integrity": "sha512-UjNx9fZW0oU7UaeoB0HblYz/Nm8MWtinAe39XkY+zjECLWqKAcHPotfYjucXiky1UlBUOScIKbwjMDdEY8xkuw==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.0.6.tgz", + "integrity": "sha512-+h9VnFHof7rKzBJ5FWrbPXWzbag31QKbUGJ/mV5BYgj39vjzPNUXBW8AaScZAlATi8+tElYXjRMvM49GnuyRLg==", "dev": true, "dependencies": { "ajv": "8.12.0", @@ -440,12 +266,12 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.1.tgz", - "integrity": "sha512-bwgdGviRZC5X8Tl4QcjtIJAcC0p8yIhOyYVFrq4PWYvI+DfV9P6w3OFuoS6rwEoiIQR90+12iKBYMt1MfL/c0Q==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.0.6.tgz", + "integrity": "sha512-2g769MpazA1aOzJOm2MNGosra3kxw8CbdIQQOKkvycIzroRNgN06yHcRTDC03GADgP/CkDJ6kxwJQNG+wNFL2A==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.1", + "@angular-devkit/core": "17.0.6", "jsonc-parser": "3.2.0", "magic-string": "0.30.5", "ora": "5.4.1", @@ -556,9 +382,9 @@ } }, "node_modules/@angular/animations": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.0.3.tgz", - "integrity": "sha512-aBLVJ0HHYdIZCAXymQDP6UGuz/5oM/3uLCFVHx32vhibLByjw0jNCvy2lzmPLU5gUU6wEWX2b3ZtnzFqhmo4+A==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.0.6.tgz", + "integrity": "sha512-fic61LjLHry79c5H9UGM8Ff311MJnf9an7EukLj2aLJ3J0uadL/H9de7dDp8PaIT10DX9g+aRTIKOmF3PmmXIQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -566,13 +392,13 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.0.3" + "@angular/core": "17.0.6" } }, "node_modules/@angular/cdk": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.0.1.tgz", - "integrity": "sha512-0hrXm2D0s0/vUtDoLFRWTs75k5WY/hQmfnsaJXHeqinbE3eKOxmQxL1i7ymnMSQthEWzgRAhzS3Nfs7Alw3dQA==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.0.6.tgz", + "integrity": "sha512-ouh/QNSZqeoF7Vph2GePTpFtglhwBZ4BpQyXYjVt8dbjJCcuFtUZSeRX49oRyRvAuxn+ui9qaiBgFA7jg5H6uA==", "dependencies": { "tslib": "^2.3.0" }, @@ -586,15 +412,15 @@ } }, "node_modules/@angular/cli": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.1.tgz", - "integrity": "sha512-3iJWw+bpr/8y1ZY1m0wGfukffQVmD6DJUNubB297NCq1bJyUj+uwBuDnpIH+vidJvPBEEY+9XPJr0Jnd6+i7rg==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.0.6.tgz", + "integrity": "sha512-BLA2wDeqZManC/7MI6WvRRV+VhrwjxxB7FawLyp4xYlo0CTSOFOfeKPVRMLEnA/Ou4R5d47B+BqJTlep62pHwg==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1700.1", - "@angular-devkit/core": "17.0.1", - "@angular-devkit/schematics": "17.0.1", - "@schematics/angular": "17.0.1", + "@angular-devkit/architect": "0.1700.6", + "@angular-devkit/core": "17.0.6", + "@angular-devkit/schematics": "17.0.6", + "@schematics/angular": "17.0.6", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.1", @@ -620,9 +446,9 @@ } }, "node_modules/@angular/common": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.3.tgz", - "integrity": "sha512-AD/d1n0hNisHDhIeBsW2ERZI9ChjiOuZ3IiGwcYKmlcOHTrZTJPAh/ZMgahv24rArlNVax7bT+Ik8+sJedGcEQ==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.0.6.tgz", + "integrity": "sha512-FZtf8ol8W2V21ZDgFtcxmJ6JJKUO97QZ+wr/bosyYEryWMmn6VGrbOARhfW7BlrEgn14NdFkLb72KKtqoqRjrg==", "dependencies": { "tslib": "^2.3.0" }, @@ -630,14 +456,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.0.3", + "@angular/core": "17.0.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.3.tgz", - "integrity": "sha512-ryUcj8Vc+Q4jMrjrmsEIsGLXeWSmNE/KoTyURPCH+NWq9GBMbjv4oe0/oFSBMN2ZtRMVCvqv2Nq+Z2KRDRGB0A==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.0.6.tgz", + "integrity": "sha512-PaCNnlPcL0rvByKCBUUyLWkKJYXOrcfKlYYvcacjOzEUgZeEpekG81hMRb9u/Pz+A+M4HJSTmdgzwGP35zo8qw==", "dependencies": { "tslib": "^2.3.0" }, @@ -645,7 +471,7 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/core": "17.0.3" + "@angular/core": "17.0.6" }, "peerDependenciesMeta": { "@angular/core": { @@ -654,9 +480,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.3.tgz", - "integrity": "sha512-oj7KJBFgs6ulT1/A/xkkDHBOB0c7o9HV2Mn5pUosXBo2VgcGYeuJeXffC+mFr5FyiRO1sUanw4vSWnLzK1U0pQ==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.0.6.tgz", + "integrity": "sha512-C1Gfh9kbjYZezEMOwxnvUTHuPXa+6pk7mAfSj8e5oAO6E+wfo2dTxv1J5zxa3KYzxPYMNfF8OFvLuMKsw7lXjA==", "dev": true, "dependencies": { "@babel/core": "7.23.2", @@ -677,14 +503,14 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/compiler": "17.0.3", + "@angular/compiler": "17.0.6", "typescript": ">=5.2 <5.3" } }, "node_modules/@angular/core": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.3.tgz", - "integrity": "sha512-zY4yhPiphuktrodaM+GiP8G07qnUlmwKElLjYazeIR8A+kF51RQRpSf/pWe5M0uJIn5Oja+RdO9kzhDI9QvOcA==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.0.6.tgz", + "integrity": "sha512-QzfKRTDNgGOY9D5VxenUUz20cvPVC+uVw9xiqkDuHgGfLYVFlCAK9ymFYkdUCLTcVzJPxckP+spMpPX8nc4Aqw==", "dependencies": { "tslib": "^2.3.0" }, @@ -697,9 +523,9 @@ } }, "node_modules/@angular/forms": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.3.tgz", - "integrity": "sha512-slCUGe5nVOrA0Su9pkmgPXBVzkgh4stvVFBb6ic9/+GlmtRi8h1v5jAFhR4B0R4iaaIoF+TTpRKhZShwtFSqSg==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.0.6.tgz", + "integrity": "sha512-n/trsMtQHUBGiWz5lFaggMcMOuw0gH+96TCtHxQiUYJOdrbOemkFdGrNh3B4fGHmogWuOYJVF5FAm97WRES2XA==", "dependencies": { "tslib": "^2.3.0" }, @@ -707,16 +533,16 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.0.3", - "@angular/core": "17.0.3", - "@angular/platform-browser": "17.0.3", + "@angular/common": "17.0.6", + "@angular/core": "17.0.6", + "@angular/platform-browser": "17.0.6", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.3.tgz", - "integrity": "sha512-4SoW0yeAxgfcLIekKsvZVg/WgI5aQZyz9HGOoyBcVQ8coYoZmM9bAYQi+9zvyweqoWc+jgw72X1E8wtmMXt7Aw==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.0.6.tgz", + "integrity": "sha512-nBhWH1MKT2WswgRNIoMnmNAt0n5/fG59BanJtodW71//Aj5aIE+BuVoFgK3wmO8IMoeP4i4GXRInBXs6lUMOJw==", "dependencies": { "tslib": "^2.3.0" }, @@ -724,9 +550,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/animations": "17.0.3", - "@angular/common": "17.0.3", - "@angular/core": "17.0.3" + "@angular/animations": "17.0.6", + "@angular/common": "17.0.6", + "@angular/core": "17.0.6" }, "peerDependenciesMeta": { "@angular/animations": { @@ -735,9 +561,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.3.tgz", - "integrity": "sha512-Ab6ZeGG63z9Ilv8r4lHcmSirVaw8quRrPjDbT8cgIteHbj0SbwgDzxX0ve+fjjubFUluNSNtc6OYglWMHJ/g7Q==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.0.6.tgz", + "integrity": "sha512-5ZEmBtBkqamTaWjUXCls7G1f3xyK/ykXE7hnUV9CgGqXKrNkxblmbtOhoWdsbuIYjjdxQcAk1qtg/Rg21wcc4w==", "dependencies": { "tslib": "^2.3.0" }, @@ -745,16 +571,16 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.0.3", - "@angular/compiler": "17.0.3", - "@angular/core": "17.0.3", - "@angular/platform-browser": "17.0.3" + "@angular/common": "17.0.6", + "@angular/compiler": "17.0.6", + "@angular/core": "17.0.6", + "@angular/platform-browser": "17.0.6" } }, "node_modules/@angular/router": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.3.tgz", - "integrity": "sha512-zw31XXMqLJ1CcHxDtEl2/FTJXeRbbnLM8oHtCPzbbxTkhAlnXxSYxjds0+1IMmpzz/v9qGBhYvUt8ZfZhqDBHQ==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.0.6.tgz", + "integrity": "sha512-xW6yDxREpBOB9MoODSfIw5HwkwLK+OgK34Q6sGYs0ft9UryMoFwft+pHGAaDz2nzhA72n+Ht9B2eai78UE9jGQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -762,9 +588,9 @@ "node": "^18.13.0 || >=20.9.0" }, "peerDependencies": { - "@angular/common": "17.0.3", - "@angular/core": "17.0.3", - "@angular/platform-browser": "17.0.3", + "@angular/common": "17.0.6", + "@angular/core": "17.0.6", + "@angular/platform-browser": "17.0.6", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -775,22 +601,22 @@ "dev": true }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -842,12 +668,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.23.6", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -906,9 +732,9 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.9.tgz", - "integrity": "sha512-B2L9neXTIyPQoXDm+NtovPvG6VOLWnaXu3BIeVDWwdKFgG30oNa6CqVGiJPDWQwIAK49t9gnQI9c6K6RzabiKw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz", + "integrity": "sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -916,7 +742,7 @@ "@babel/helper-function-name": "^7.23.0", "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-replace-supers": "^7.24.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", "semver": "^6.3.1" @@ -964,9 +790,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", - "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz", + "integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1026,12 +852,12 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15" + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1069,9 +895,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", "dev": true, "engines": { "node": ">=6.9.0" @@ -1095,13 +921,13 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", + "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-member-expression-to-functions": "^7.23.0", "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { @@ -1148,9 +974,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -1189,37 +1015,38 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", + "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", "dev": true, "dependencies": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1229,12 +1056,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", - "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz", + "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1244,14 +1071,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", - "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", + "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.23.3" + "@babel/plugin-transform-optional-chaining": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -1260,22 +1087,6 @@ "@babel/core": "^7.13.0" } }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz", - "integrity": "sha512-LlRT7HgaifEpQA1ZgLVOIJZZFVPWN5iReq/7/JixwBtwcoeVGDBD53ZV28rrsLYOZs1Y/EHhA8N/Z6aazHR8cw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -1352,12 +1163,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", - "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", + "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1367,12 +1178,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", - "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", + "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1524,12 +1335,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", - "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", + "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1539,9 +1350,9 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz", - "integrity": "sha512-PdxEpL71bJp1byMG0va5gwQcXHxuEYC/BgI/e88mGTtohbZN28O5Yit0Plkkm/dBzCF/BxmbNcses1RH1T+urA==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz", + "integrity": "sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -1557,14 +1368,14 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", - "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-module-imports": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.20" + "@babel/helper-remap-async-to-generator": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -1574,12 +1385,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", - "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", + "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1589,12 +1400,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", - "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz", + "integrity": "sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1604,13 +1415,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", - "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", + "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1620,13 +1431,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", - "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", + "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.4", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { @@ -1637,17 +1448,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.23.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz", - "integrity": "sha512-yAYslGsY1bX6Knmg46RjiCiNSwJKv2IUC8qOdYKqMMr0491SXFhcHqOdRDeCRohOOIzwN/90C6mQ9qAKgrP7dg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.1.tgz", + "integrity": "sha512-ZTIe3W7UejJd3/3R4p7ScyyOoafetUShSf4kCqV0O7F/RiHxVj/wRaRnQlrGwflvcehNA8M42HkAiEDYZu2F1Q==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, @@ -1659,13 +1470,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", - "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", + "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.15" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/template": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1675,12 +1486,12 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", - "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.1.tgz", + "integrity": "sha512-ow8jciWqNxR3RYbSNVuF4U2Jx130nwnBnhRw6N6h1bOejNkABmcI5X5oz29K4alWX7vf1C+o6gtKXikzRKkVdw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1690,13 +1501,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", - "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", + "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1706,12 +1517,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", - "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", + "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1721,12 +1532,12 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", - "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", + "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3" }, "engines": { @@ -1737,13 +1548,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", - "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", + "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", "dev": true, "dependencies": { "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1753,12 +1564,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", - "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", + "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" }, "engines": { @@ -1769,12 +1580,12 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz", - "integrity": "sha512-aYH4ytZ0qSuBbpfhuofbg/e96oQ7U2w1Aw/UQmKT+1l39uEhUPoFS3fHevDc1G0OvewyDudfMKY1OulczHzWIw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", + "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { @@ -1785,14 +1596,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", - "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", + "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", "dev": true, "dependencies": { - "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-function-name": "^7.23.0", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1802,12 +1613,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", - "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", + "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-json-strings": "^7.8.3" }, "engines": { @@ -1818,12 +1629,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", - "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", + "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1833,12 +1644,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", - "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", + "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { @@ -1849,12 +1660,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", - "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", + "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1864,13 +1675,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", - "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", + "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1880,13 +1691,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", - "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", + "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-simple-access": "^7.22.5" }, "engines": { @@ -1897,14 +1708,14 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.9.tgz", - "integrity": "sha512-KDlPRM6sLo4o1FkiSlXoAa8edLXFsKKIda779fbLrvmeuc3itnjCtaO6RrtoaANsIJANj+Vk1zqbZIMhkCAHVw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", + "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { @@ -1915,13 +1726,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", - "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", + "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1947,12 +1758,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", - "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", + "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -1962,12 +1773,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", - "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", + "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "engines": { @@ -1978,12 +1789,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", - "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", + "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "engines": { @@ -1994,16 +1805,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", - "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.1.tgz", + "integrity": "sha512-XjD5f0YqOtebto4HGISLNfiNMTTs6tbkFf2TOqJlYKYmbo+mN9Dnpl4SRoofiziuOWMIyq3sZEUqLo3hLITFEA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.3", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.23.3" + "@babel/plugin-transform-parameters": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -2013,13 +1823,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", - "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", + "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20" + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1" }, "engines": { "node": ">=6.9.0" @@ -2029,12 +1839,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", - "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", + "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "engines": { @@ -2045,12 +1855,12 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", - "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.1.tgz", + "integrity": "sha512-n03wmDt+987qXwAgcBlnUUivrZBPZ8z1plL0YvgQalLm+ZE5BMhGm94jhxXtA1wzv1Cu2aaOv1BM9vbVttrzSg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, @@ -2062,12 +1872,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", - "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.1.tgz", + "integrity": "sha512-8Jl6V24g+Uw5OGPeWNKrKqXPDw2YDjLc53ojwfMcKwlEoETKU9rU0mHUtcg9JntWI/QYzGAXNWEcVHZ+fR+XXg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2077,13 +1887,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", - "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", + "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", "dev": true, "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2093,14 +1903,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", - "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.1.tgz", + "integrity": "sha512-pTHxDVa0BpUbvAgX3Gat+7cSciXqUcY9j2VZKTbSB6+VQGpNgNO9ailxTGHSXlqOnX1Hcx1Enme2+yv7VqP9bg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -2111,12 +1921,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", - "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", + "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2126,12 +1936,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", - "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", + "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "regenerator-transform": "^0.15.2" }, "engines": { @@ -2142,12 +1952,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", - "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", + "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2157,16 +1967,16 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.7.tgz", - "integrity": "sha512-fa0hnfmiXc9fq/weK34MUV0drz2pOL/vfKWvN7Qw127hiUPabFCUMgAbYWcchRzMJit4o5ARsK/s+5h0249pLw==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.2.tgz", + "integrity": "sha512-XOntj6icgzMS58jPVtQpiuF6ZFWxQiJavISGx5KGjRj+3gqZr8+N6Kx+N9BApWzgS+DOjIZfXXj0ZesenOWDyA==", "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "babel-plugin-polyfill-corejs2": "^0.4.7", - "babel-plugin-polyfill-corejs3": "^0.8.7", - "babel-plugin-polyfill-regenerator": "^0.5.4", + "babel-plugin-polyfill-corejs2": "^0.4.6", + "babel-plugin-polyfill-corejs3": "^0.8.5", + "babel-plugin-polyfill-regenerator": "^0.5.3", "semver": "^6.3.1" }, "engines": { @@ -2186,12 +1996,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", - "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", + "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2201,12 +2011,12 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", - "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", + "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" }, "engines": { @@ -2217,12 +2027,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", - "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", + "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2232,12 +2042,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", - "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", + "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2247,12 +2057,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", - "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.1.tgz", + "integrity": "sha512-CBfU4l/A+KruSUoW+vTQthwcAdwuqbpRNB8HQKlZABwHRhsdHZ9fezp4Sn18PeAlYxTNiLMlx4xUBV3AWfg1BA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2262,12 +2072,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", - "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", + "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2277,13 +2087,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", - "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", + "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2293,13 +2103,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", - "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", + "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2309,13 +2119,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", - "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", + "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.22.15", - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -2325,26 +2135,25 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.7.tgz", - "integrity": "sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.2.tgz", + "integrity": "sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-compilation-targets": "^7.23.6", + "@babel/compat-data": "^7.23.2", + "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.23.5", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.15", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.15", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-class-properties": "^7.12.13", "@babel/plugin-syntax-class-static-block": "^7.14.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.23.3", - "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", @@ -2356,58 +2165,59 @@ "@babel/plugin-syntax-private-property-in-object": "^7.14.5", "@babel/plugin-syntax-top-level-await": "^7.14.5", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.23.3", - "@babel/plugin-transform-async-generator-functions": "^7.23.7", - "@babel/plugin-transform-async-to-generator": "^7.23.3", - "@babel/plugin-transform-block-scoped-functions": "^7.23.3", - "@babel/plugin-transform-block-scoping": "^7.23.4", - "@babel/plugin-transform-class-properties": "^7.23.3", - "@babel/plugin-transform-class-static-block": "^7.23.4", - "@babel/plugin-transform-classes": "^7.23.5", - "@babel/plugin-transform-computed-properties": "^7.23.3", - "@babel/plugin-transform-destructuring": "^7.23.3", - "@babel/plugin-transform-dotall-regex": "^7.23.3", - "@babel/plugin-transform-duplicate-keys": "^7.23.3", - "@babel/plugin-transform-dynamic-import": "^7.23.4", - "@babel/plugin-transform-exponentiation-operator": "^7.23.3", - "@babel/plugin-transform-export-namespace-from": "^7.23.4", - "@babel/plugin-transform-for-of": "^7.23.6", - "@babel/plugin-transform-function-name": "^7.23.3", - "@babel/plugin-transform-json-strings": "^7.23.4", - "@babel/plugin-transform-literals": "^7.23.3", - "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", - "@babel/plugin-transform-member-expression-literals": "^7.23.3", - "@babel/plugin-transform-modules-amd": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-modules-systemjs": "^7.23.3", - "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.23.2", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.23.0", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.11", + "@babel/plugin-transform-classes": "^7.22.15", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.23.0", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.11", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-for-of": "^7.22.15", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.11", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.11", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.23.0", + "@babel/plugin-transform-modules-commonjs": "^7.23.0", + "@babel/plugin-transform-modules-systemjs": "^7.23.0", + "@babel/plugin-transform-modules-umd": "^7.22.5", "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", - "@babel/plugin-transform-new-target": "^7.23.3", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", - "@babel/plugin-transform-numeric-separator": "^7.23.4", - "@babel/plugin-transform-object-rest-spread": "^7.23.4", - "@babel/plugin-transform-object-super": "^7.23.3", - "@babel/plugin-transform-optional-catch-binding": "^7.23.4", - "@babel/plugin-transform-optional-chaining": "^7.23.4", - "@babel/plugin-transform-parameters": "^7.23.3", - "@babel/plugin-transform-private-methods": "^7.23.3", - "@babel/plugin-transform-private-property-in-object": "^7.23.4", - "@babel/plugin-transform-property-literals": "^7.23.3", - "@babel/plugin-transform-regenerator": "^7.23.3", - "@babel/plugin-transform-reserved-words": "^7.23.3", - "@babel/plugin-transform-shorthand-properties": "^7.23.3", - "@babel/plugin-transform-spread": "^7.23.3", - "@babel/plugin-transform-sticky-regex": "^7.23.3", - "@babel/plugin-transform-template-literals": "^7.23.3", - "@babel/plugin-transform-typeof-symbol": "^7.23.3", - "@babel/plugin-transform-unicode-escapes": "^7.23.3", - "@babel/plugin-transform-unicode-property-regex": "^7.23.3", - "@babel/plugin-transform-unicode-regex": "^7.23.3", - "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", + "@babel/plugin-transform-numeric-separator": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.22.15", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.11", + "@babel/plugin-transform-optional-chaining": "^7.23.0", + "@babel/plugin-transform-parameters": "^7.22.15", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.11", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.10", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.10", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.7", - "babel-plugin-polyfill-corejs3": "^0.8.7", - "babel-plugin-polyfill-regenerator": "^0.5.4", + "@babel/types": "^7.23.0", + "babel-plugin-polyfill-corejs2": "^0.4.6", + "babel-plugin-polyfill-corejs3": "^0.8.5", + "babel-plugin-polyfill-regenerator": "^0.5.3", "core-js-compat": "^3.31.0", "semver": "^6.3.1" }, @@ -2448,9 +2258,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.23.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz", - "integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -2460,33 +2270,33 @@ } }, "node_modules/@babel/template": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", - "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2494,10 +2304,25 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.23.4", @@ -2608,26 +2433,10 @@ "node": ">=16" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", - "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/android-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", - "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.5.tgz", + "integrity": "sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==", "cpu": [ "arm" ], @@ -2641,9 +2450,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", - "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz", + "integrity": "sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==", "cpu": [ "arm64" ], @@ -2657,9 +2466,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", - "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.5.tgz", + "integrity": "sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==", "cpu": [ "x64" ], @@ -2673,9 +2482,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", - "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz", + "integrity": "sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==", "cpu": [ "arm64" ], @@ -2689,9 +2498,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", - "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz", + "integrity": "sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==", "cpu": [ "x64" ], @@ -2705,9 +2514,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", - "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz", + "integrity": "sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==", "cpu": [ "arm64" ], @@ -2721,9 +2530,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", - "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz", + "integrity": "sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==", "cpu": [ "x64" ], @@ -2737,9 +2546,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", - "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz", + "integrity": "sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==", "cpu": [ "arm" ], @@ -2753,9 +2562,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", - "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz", + "integrity": "sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==", "cpu": [ "arm64" ], @@ -2769,9 +2578,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", - "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz", + "integrity": "sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==", "cpu": [ "ia32" ], @@ -2785,9 +2594,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", - "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz", + "integrity": "sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==", "cpu": [ "loong64" ], @@ -2801,9 +2610,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", - "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz", + "integrity": "sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==", "cpu": [ "mips64el" ], @@ -2817,9 +2626,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", - "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz", + "integrity": "sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==", "cpu": [ "ppc64" ], @@ -2833,9 +2642,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", - "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz", + "integrity": "sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==", "cpu": [ "riscv64" ], @@ -2849,9 +2658,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", - "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz", + "integrity": "sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==", "cpu": [ "s390x" ], @@ -2865,9 +2674,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", - "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz", + "integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==", "cpu": [ "x64" ], @@ -2881,9 +2690,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", - "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz", + "integrity": "sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==", "cpu": [ "x64" ], @@ -2897,9 +2706,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", - "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz", + "integrity": "sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==", "cpu": [ "x64" ], @@ -2913,9 +2722,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", - "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz", + "integrity": "sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==", "cpu": [ "x64" ], @@ -2929,9 +2738,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", - "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz", + "integrity": "sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==", "cpu": [ "arm64" ], @@ -2945,9 +2754,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", - "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz", + "integrity": "sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==", "cpu": [ "ia32" ], @@ -2961,9 +2770,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", - "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz", + "integrity": "sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==", "cpu": [ "x64" ], @@ -3001,9 +2810,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -3046,9 +2855,9 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -3100,22 +2909,22 @@ } }, "node_modules/@fastify/busboy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", - "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "dev": true, "engines": { "node": ">=14" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -3136,9 +2945,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "node_modules/@hutson/parse-repository-url": { @@ -3284,45 +3093,45 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { @@ -3332,9 +3141,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3342,18 +3151,18 @@ } }, "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", - "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "dev": true }, "node_modules/@ljharb/through": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.11.tgz", - "integrity": "sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==", + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", + "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" }, "engines": { "node": ">= 0.4" @@ -3450,9 +3259,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.1.1.tgz", - "integrity": "sha512-uPWEpRuAUmMBZhYMpkEHNbeI8QAgeVM5lnWM+02lK75u1+sgYy32ed+RcRvEI+8hRQcsCQ8HtR4QubgJb4TzCQ==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.0.6.tgz", + "integrity": "sha512-9Us20rqGhi8PmQBwQu6Qtww3WVV/gf2s2DbzcLclsiDtSBobzT64Z6F6E9OpAYD+c5PxlUaOghL6NXdnSNdByA==", "dev": true, "engines": { "node": "^18.13.0 || >=20.9.0", @@ -3461,7 +3270,7 @@ }, "peerDependencies": { "@angular/compiler-cli": "^17.0.0", - "typescript": ">=5.2 <5.4", + "typescript": ">=5.2 <5.3", "webpack": "^5.54.0" } }, @@ -3501,29 +3310,26 @@ } }, "node_modules/@npmcli/agent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.0.tgz", - "integrity": "sha512-2yThA1Es98orMkpSLVqlDZAMPK3jHJhifP2gnNUdk1754uZ8yI5c+ulCoVG+WlntQA6MzhrURMXjSd9Z7dJ2/Q==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", "dev": true, "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.1" + "socks-proxy-agent": "^8.0.3" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", - "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, "engines": { "node": "14 || >=16.14" } @@ -3541,9 +3347,9 @@ } }, "node_modules/@npmcli/git": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.3.tgz", - "integrity": "sha512-UZp9NwK+AynTrKvHn5k3KviW/hA5eENmFsu3iAPe7sWRt0lFUdsY/wXIYjpDFe7cdSNwOIzbObfwgt6eL5/2zw==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.5.tgz", + "integrity": "sha512-x8hXItC8OFOwdgERzRIxg0ic1lQqW6kSZFFQtZTCNYOeGb9UqzVcod02TYljI9UBl4RtfcyQ0A7ygmcGFvEqWw==", "dev": true, "dependencies": { "@npmcli/promise-spawn": "^7.0.0", @@ -3569,13 +3375,10 @@ } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", - "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, "engines": { "node": "14 || >=16.14" } @@ -3620,10 +3423,110 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@npmcli/package-json": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.0.2.tgz", + "integrity": "sha512-LmW+tueGSK+FCM3OpcKtwKKo3igpefh6HHiw23sGd8OdJ8l0GrfGfVdGOFVtJRMaXVnvI1RUdEPlB9VUln5Wbw==", + "dev": true, + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", + "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/normalize-package-data": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", + "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/@npmcli/promise-spawn": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.0.tgz", - "integrity": "sha512-wBqcGsMELZna0jDblGd7UXgOby45TQaMWmbFwWX+SEotk4HV6zG2t6rT9siyLhPk4P6YYqgfL1UO8nMWDBVJXQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz", + "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==", "dev": true, "dependencies": { "which": "^4.0.0" @@ -3656,16 +3559,25 @@ "node": "^16.13.0 || >=18.0.0" } }, + "node_modules/@npmcli/redact": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-1.1.0.tgz", + "integrity": "sha512-PfnWuOkQgu7gCbnSsAisaX7hKOdZ4wSAhAzH3/ph5dSGau52kCRrMMGbiSQLwyTZpgldkZ49b0brkOr1AzGBHQ==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/@npmcli/run-script": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.2.tgz", - "integrity": "sha512-Omu0rpA8WXvcGeY6DDzyRoY1i5DkCBkzyJ+m2u7PD6quzb0TvSqdIPOkTn8ZBOj7LbbcbMfZ3c5skwSu6m8y2w==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", + "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==", "dev": true, "dependencies": { "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", "@npmcli/promise-spawn": "^7.0.0", "node-gyp": "^10.0.0", - "read-package-json-fast": "^3.0.0", "which": "^4.0.0" }, "engines": { @@ -3955,12 +3867,12 @@ } }, "node_modules/@rollup/plugin-json": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.0.1.tgz", - "integrity": "sha512-RgVfl5hWMkxN1h/uZj8FVESvPuBJ/uf6ly6GTj0GONnkfoBN5KC0MSz+PN2OLDgYXMhtG0mWpTrkiOjoxAIevw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", "dev": true, "dependencies": { - "@rollup/pluginutils": "^5.0.1" + "@rollup/pluginutils": "^5.1.0" }, "engines": { "node": ">=14.0.0" @@ -4000,9 +3912,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.5.tgz", - "integrity": "sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", @@ -4034,9 +3946,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.4.1.tgz", - "integrity": "sha512-Ss4suS/sd+6xLRu+MLCkED2mUrAyqHmmvZB+zpzZ9Znn9S8wCkTQCJaQ8P8aHofnvG5L16u9MVnJjCqioPErwQ==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.2.tgz", + "integrity": "sha512-ahxSgCkAEk+P/AVO0vYr7DxOD3CwAQrT0Go9BJyGQ9Ef0QxVOfjDZMiF4Y2s3mLyPrjonchIMH/tbWHucJMykQ==", "cpu": [ "arm" ], @@ -4047,9 +3959,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.4.1.tgz", - "integrity": "sha512-sRSkGTvGsARwWd7TzC8LKRf8FiPn7257vd/edzmvG4RIr9x68KBN0/Ek48CkuUJ5Pj/Dp9vKWv6PEupjKWjTYA==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.2.tgz", + "integrity": "sha512-lAarIdxZWbFSHFSDao9+I/F5jDaKyCqAPMq5HqnfpBw8dKDiCaaqM0lq5h1pQTLeIqueeay4PieGR5jGZMWprw==", "cpu": [ "arm64" ], @@ -4060,9 +3972,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.4.1.tgz", - "integrity": "sha512-nz0AiGrrXyaWpsmBXUGOBiRDU0wyfSXbFuF98pPvIO8O6auQsPG6riWsfQqmCCC5FNd8zKQ4JhgugRNAkBJ8mQ==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.2.tgz", + "integrity": "sha512-SWsr8zEUk82KSqquIMgZEg2GE5mCSfr9sE/thDROkX6pb3QQWPp8Vw8zOq2GyxZ2t0XoSIUlvHDkrf5Gmf7x3Q==", "cpu": [ "arm64" ], @@ -4073,9 +3985,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.4.1.tgz", - "integrity": "sha512-Ogqvf4/Ve/faMaiPRvzsJEqajbqs00LO+8vtrPBVvLgdw4wBg6ZDXdkDAZO+4MLnrc8mhGV6VJAzYScZdPLtJg==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.2.tgz", + "integrity": "sha512-o/HAIrQq0jIxJAhgtIvV5FWviYK4WB0WwV91SLUnsliw1lSAoLsmgEEgRWzDguAFeUEUUoIWXiJrPqU7vGiVkA==", "cpu": [ "x64" ], @@ -4086,9 +3998,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.4.1.tgz", - "integrity": "sha512-9zc2tqlr6HfO+hx9+wktUlWTRdje7Ub15iJqKcqg5uJZ+iKqmd2CMxlgPpXi7+bU7bjfDIuvCvnGk7wewFEhCg==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.2.tgz", + "integrity": "sha512-nwlJ65UY9eGq91cBi6VyDfArUJSKOYt5dJQBq8xyLhvS23qO+4Nr/RreibFHjP6t+5ap2ohZrUJcHv5zk5ju/g==", "cpu": [ "arm" ], @@ -4099,9 +4011,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.4.1.tgz", - "integrity": "sha512-phLb1fN3rq2o1j1v+nKxXUTSJnAhzhU0hLrl7Qzb0fLpwkGMHDem+o6d+ZI8+/BlTXfMU4kVWGvy6g9k/B8L6Q==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.2.tgz", + "integrity": "sha512-Pg5TxxO2IVlMj79+c/9G0LREC9SY3HM+pfAwX7zj5/cAuwrbfj2Wv9JbMHIdPCfQpYsI4g9mE+2Bw/3aeSs2rQ==", "cpu": [ "arm64" ], @@ -4112,9 +4024,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.4.1.tgz", - "integrity": "sha512-M2sDtw4tf57VPSjbTAN/lz1doWUqO2CbQuX3L9K6GWIR5uw9j+ROKCvvUNBY8WUbMxwaoc8mH9HmmBKsLht7+w==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.2.tgz", + "integrity": "sha512-cAOTjGNm84gc6tS02D1EXtG7tDRsVSDTBVXOLbj31DkwfZwgTPYZ6aafSU7rD/4R2a34JOwlF9fQayuTSkoclA==", "cpu": [ "arm64" ], @@ -4124,10 +4036,49 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.2.tgz", + "integrity": "sha512-4RyT6v1kXb7C0fn6zV33rvaX05P0zHoNzaXI/5oFHklfKm602j+N4mn2YvoezQViRLPnxP8M1NaY4s/5kXO5cw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.2.tgz", + "integrity": "sha512-KNUH6jC/vRGAKSorySTyc/yRYlCwN/5pnMjXylfBniwtJx5O7X17KG/0efj8XM3TZU7raYRXJFFReOzNmL1n1w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.2.tgz", + "integrity": "sha512-xPV4y73IBEXToNPa3h5lbgXOi/v0NcvKxU0xejiFw6DtIYQqOTMhZ2DN18/HrrP0PmiL3rGtRG9gz1QE8vFKXQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.4.1.tgz", - "integrity": "sha512-mHIlRLX+hx+30cD6c4BaBOsSqdnCE4ok7/KDvjHYAHoSuveoMMxIisZFvcLhUnyZcPBXDGZTuBoalcuh43UfQQ==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.2.tgz", + "integrity": "sha512-QBhtr07iFGmF9egrPOWyO5wciwgtzKkYPNLVCFZTmr4TWmY0oY2Dm/bmhHjKRwZoGiaKdNcKhFtUMBKvlchH+Q==", "cpu": [ "x64" ], @@ -4138,9 +4089,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.4.1.tgz", - "integrity": "sha512-tB+RZuDi3zxFx7vDrjTNGVLu2KNyzYv+UY8jz7e4TMEoAj7iEt8Qk6xVu6mo3pgjnsHj6jnq3uuRsHp97DLwOA==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.2.tgz", + "integrity": "sha512-8zfsQRQGH23O6qazZSFY5jP5gt4cFvRuKTpuBsC1ZnSWxV8ZKQpPqOZIUtdfMOugCcBvFGRa1pDC/tkf19EgBw==", "cpu": [ "x64" ], @@ -4151,9 +4102,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.4.1.tgz", - "integrity": "sha512-Hdn39PzOQowK/HZzYpCuZdJC91PE6EaGbTe2VCA9oq2u18evkisQfws0Smh9QQGNNRa/T7MOuGNQoLeXhhE3PQ==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.2.tgz", + "integrity": "sha512-H4s8UjgkPnlChl6JF5empNvFHp77Jx+Wfy2EtmYPe9G22XV+PMuCinZVHurNe8ggtwoaohxARJZbaH/3xjB/FA==", "cpu": [ "arm64" ], @@ -4164,9 +4115,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.4.1.tgz", - "integrity": "sha512-tLpKb1Elm9fM8c5w3nl4N1eLTP4bCqTYw9tqUBxX8/hsxqHO3dxc2qPbZ9PNkdK4tg4iLEYn0pOUnVByRd2CbA==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.2.tgz", + "integrity": "sha512-djqpAjm/i8erWYF0K6UY4kRO3X5+T4TypIqw60Q8MTqSBaQNpNXDhxdjpZ3ikgb+wn99svA7jxcXpiyg9MUsdw==", "cpu": [ "ia32" ], @@ -4177,9 +4128,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.4.1.tgz", - "integrity": "sha512-eAhItDX9yQtZVM3yvXS/VR3qPqcnXvnLyx1pLXl4JzyNMBNO3KC986t/iAg2zcMzpAp9JSvxB5VZGnBiNoA98w==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.2.tgz", + "integrity": "sha512-teAqzLT0yTYZa8ZP7zhFKEx4cotS8Tkk5XiqNMJhD4CpaWB1BHARE4Qy+RzwnXvSAYv+Q3jAqCVBS+PS+Yee8Q==", "cpu": [ "x64" ], @@ -4190,13 +4141,13 @@ ] }, "node_modules/@schematics/angular": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.1.tgz", - "integrity": "sha512-BacI1fQsEXNYkfJzDJn3CsUSc9A4M7nhXtvt3XjceUhOqUp2AR4uIeUwDOrpLnkRwv5+rZLafUnRN3k01WUJOw==", + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.0.6.tgz", + "integrity": "sha512-AyC7Bk3Omy6PfADThhq5ci+zzdTTi2N1oZI35gw4tMK5ZxVwIACx2Zyhaz399m5c2RCDi9Hz4A2BOFq9f0j/dg==", "dev": true, "dependencies": { - "@angular-devkit/core": "17.0.1", - "@angular-devkit/schematics": "17.0.1", + "@angular-devkit/core": "17.0.6", + "@angular-devkit/schematics": "17.0.6", "jsonc-parser": "3.2.0" }, "engines": { @@ -4260,34 +4211,44 @@ } }, "node_modules/@sigstore/bundle": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.0.tgz", - "integrity": "sha512-89uOo6yh/oxaU8AeOUnVrTdVMcGk9Q1hJa7Hkvalc6G3Z3CupWk4Xe9djSgJm9fMkH69s0P0cVHUoKSOemLdng==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.1.tgz", + "integrity": "sha512-eqV17lO3EIFqCWK3969Rz+J8MYrRZKw9IBHpSo6DEcEX2c+uzDFOgHE9f2MnyDpfs48LFO4hXmk9KhQ74JzU1g==", "dev": true, "dependencies": { - "@sigstore/protobuf-specs": "^0.2.1" + "@sigstore/protobuf-specs": "^0.3.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/@sigstore/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", + "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/@sigstore/protobuf-specs": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz", - "integrity": "sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.1.tgz", + "integrity": "sha512-aIL8Z9NsMr3C64jyQzE0XlkEyBLpgEJJFDHLVVStkFV5Q3Il/r/YtY6NJWKQ4cy4AE7spP1IX5Jq7VCAxHHMfQ==", "dev": true, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/@sigstore/sign": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.2.0.tgz", - "integrity": "sha512-AAbmnEHDQv6CSfrWA5wXslGtzLPtAtHZleKOgxdQYvx/s76Fk6T6ZVt7w2IGV9j1UrFeBocTTQxaXG2oRrDhYA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.0.tgz", + "integrity": "sha512-tsAyV6FC3R3pHmKS880IXcDJuiFJiKITO1jxR1qbplcsBkZLBmjrEw5GbC7ikD6f5RU1hr7WnmxB/2kKc1qUWQ==", "dev": true, "dependencies": { - "@sigstore/bundle": "^2.1.0", - "@sigstore/protobuf-specs": "^0.2.1", + "@sigstore/bundle": "^2.3.0", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.1", "make-fetch-happen": "^13.0.0" }, "engines": { @@ -4295,13 +4256,27 @@ } }, "node_modules/@sigstore/tuf": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.2.0.tgz", - "integrity": "sha512-KKATZ5orWfqd9ZG6MN8PtCIx4eevWSuGRKQvofnWXRpyMyUEpmrzg5M5BrCpjM+NfZ0RbNGOh5tCz/P2uoRqOA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.2.tgz", + "integrity": "sha512-mwbY1VrEGU4CO55t+Kl6I7WZzIl+ysSzEYdA1Nv/FTrl2bkeaPXo5PnWZAVfcY2zSdhOpsUTJW67/M2zHXGn5w==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.0", + "tuf-js": "^2.2.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.0.tgz", + "integrity": "sha512-hQF60nc9yab+Csi4AyoAmilGNfpXT+EXdBgFkP9OgPwIBPwyqVf7JAWPtmqrrrneTmAT6ojv7OlH1f6Ix5BG4Q==", "dev": true, "dependencies": { - "@sigstore/protobuf-specs": "^0.2.1", - "tuf-js": "^2.1.0" + "@sigstore/bundle": "^2.3.1", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -4314,9 +4289,9 @@ "dev": true }, "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.1.tgz", + "integrity": "sha512-dzJtaDAAoXx4GCOJpbB2eG/Qj8VDpdwkLsWGzGm+0L7E8/434RyMbAHmk9ubXWVAb9nXmc44jUf8GKqVDiKezg==", "dev": true }, "node_modules/@tootallnate/once": { @@ -4353,9 +4328,9 @@ } }, "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "dev": true }, "node_modules/@tsconfig/node12": { @@ -4408,9 +4383,9 @@ } }, "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -4467,18 +4442,18 @@ "dev": true }, "node_modules/@types/cors": { - "version": "2.8.16", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.16.tgz", - "integrity": "sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/eslint": { - "version": "8.44.7", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz", - "integrity": "sha512-f5ORu2hcBbKei97U73mf+l9t4zTGl74IqZ0GQk4oVea/VS8tQZYkUveSYojk+frraAVYId0V2WC9O4PTNru2FQ==", + "version": "8.56.9", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.9.tgz", + "integrity": "sha512-W4W3KcqzjJ0sHg2vAq9vfml6OhsJ53TcUjUqfzzZf/EChUtwspszj/S0pzMxnfRcO55/iGq47dscXw71Fxc4Zg==", "dev": true, "dependencies": { "@types/estree": "*", @@ -4514,9 +4489,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.42", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.42.tgz", - "integrity": "sha512-ckM3jm2bf/MfB3+spLPWYPUH573plBFwpOhqQ2WottxYV85j1HQFlxmnTq57X1yHY9awZPig06hL/cLMgNWHIQ==", + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", + "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", "dev": true, "dependencies": { "@types/node": "*", @@ -4565,9 +4540,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.1.tgz", - "integrity": "sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==", + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -4589,9 +4564,9 @@ "dev": true }, "node_modules/@types/qs": { - "version": "6.9.11", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", - "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", + "integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==", "dev": true }, "node_modules/@types/range-parser": { @@ -4613,9 +4588,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", - "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/send": { @@ -4638,14 +4613,14 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", - "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dev": true, "dependencies": { "@types/http-errors": "*", - "@types/mime": "*", - "@types/node": "*" + "@types/node": "*", + "@types/send": "*" } }, "node_modules/@types/sockjs": { @@ -4667,9 +4642,9 @@ } }, "node_modules/@types/yargs": { - "version": "17.0.31", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.31.tgz", - "integrity": "sha512-bocYSx4DI8TmdlvxqGpVNXOgCNR1Jj0gNPhhAY+iz1rgKDAaYrAYdFYnhDV1IFuiuVc9HkOwyDcFxaTElF3/wg==", + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, "dependencies": { "@types/yargs-parser": "*" @@ -5635,21 +5610,21 @@ } }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.2.tgz", - "integrity": "sha512-DKHKVtpI+eA5fvObVgQ3QtTGU70CcCnedalzqmGSR050AzKZMdUzgC8KmlOneHWH8dF2hJ3wkC9+8FDVAaDRCw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz", + "integrity": "sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==", "dev": true, "engines": { "node": ">=14.6.0" }, "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + "vite": "^3.0.0 || ^4.0.0" } }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", @@ -5669,9 +5644,9 @@ "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { @@ -5692,15 +5667,15 @@ "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" + "@webassemblyjs/wasm-gen": "1.12.1" } }, "node_modules/@webassemblyjs/ieee754": { @@ -5728,28 +5703,28 @@ "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", "@webassemblyjs/leb128": "1.11.6", @@ -5757,24 +5732,24 @@ } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", "@webassemblyjs/ieee754": "1.11.6", @@ -5783,12 +5758,12 @@ } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, @@ -5841,6 +5816,13 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -5864,9 +5846,9 @@ } }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -5894,9 +5876,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", - "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", "dev": true, "engines": { "node": ">=0.4.0" @@ -5936,9 +5918,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "dependencies": { "debug": "^4.3.4" @@ -6143,13 +6125,16 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6177,17 +6162,18 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -6212,6 +6198,15 @@ "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", "dev": true }, + "node_modules/async-each-series": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", + "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/async-listen": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-1.2.0.tgz", @@ -6268,10 +6263,13 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -6279,6 +6277,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/axobject-query": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", @@ -6322,13 +6329,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", - "integrity": "sha512-OtIuQfafSzpo/LhnJaykc0R/MMnuLSSVjVYy9mHArIZ9qTCSZ6TpWCuEKZYVoN//t8HqBNScHrOtCrIK5IaGLg==", + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz", + "integrity": "sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.5.0", + "@babel/helper-define-polyfill-provider": "^0.6.1", "semver": "^6.3.1" }, "peerDependencies": { @@ -6385,6 +6392,22 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-polyfill-regenerator/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -6459,12 +6482,15 @@ } }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bindings": { @@ -6576,10 +6602,222 @@ "node": ">=8" } }, + "node_modules/browser-sync": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-2.29.3.tgz", + "integrity": "sha512-NiM38O6XU84+MN+gzspVmXV2fTOoe+jBqIBx3IBdhZrdeURr6ZgznJr/p+hQ+KzkKEiGH/GcC4SQFSL0jV49bg==", + "dev": true, + "dependencies": { + "browser-sync-client": "^2.29.3", + "browser-sync-ui": "^2.29.3", + "bs-recipes": "1.3.4", + "chalk": "4.1.2", + "chokidar": "^3.5.1", + "connect": "3.6.6", + "connect-history-api-fallback": "^1", + "dev-ip": "^1.0.1", + "easy-extender": "^2.3.4", + "eazy-logger": "^4.0.1", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "fs-extra": "3.0.1", + "http-proxy": "^1.18.1", + "immutable": "^3", + "localtunnel": "^2.0.1", + "micromatch": "^4.0.2", + "opn": "5.3.0", + "portscanner": "2.2.0", + "raw-body": "^2.3.2", + "resp-modifier": "6.0.2", + "rx": "4.1.0", + "send": "0.16.2", + "serve-index": "1.9.1", + "serve-static": "1.13.2", + "server-destroy": "1.0.1", + "socket.io": "^4.4.1", + "ua-parser-js": "^1.0.33", + "yargs": "^17.3.1" + }, + "bin": { + "browser-sync": "dist/bin.js" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/browser-sync-client": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.29.3.tgz", + "integrity": "sha512-4tK5JKCl7v/3aLbmCBMzpufiYLsB1+UI+7tUXCCp5qF0AllHy/jAqYu6k7hUF3hYtlClKpxExWaR+rH+ny07wQ==", + "dev": true, + "dependencies": { + "etag": "1.8.1", + "fresh": "0.5.2", + "mitt": "^1.1.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/browser-sync-ui": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.29.3.tgz", + "integrity": "sha512-kBYOIQjU/D/3kYtUIJtj82e797Egk1FB2broqItkr3i4eF1qiHbFCG6srksu9gWhfmuM/TNG76jMfzAdxEPakg==", + "dev": true, + "dependencies": { + "async-each-series": "0.1.1", + "chalk": "4.1.2", + "connect-history-api-fallback": "^1", + "immutable": "^3", + "server-destroy": "1.0.1", + "socket.io-client": "^4.4.1", + "stream-throttle": "^0.1.3" + } + }, + "node_modules/browser-sync-ui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/browser-sync-ui/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/browser-sync-ui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/browser-sync-ui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/browser-sync-ui/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync-ui/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/browser-sync/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/browser-sync/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/browser-sync/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/browser-sync/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "funding": [ { @@ -6596,8 +6834,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, @@ -6608,6 +6846,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-recipes": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", + "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==", + "dev": true + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -6660,9 +6904,9 @@ } }, "node_modules/builtins": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", - "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", "dev": true, "dependencies": { "semver": "^7.0.0" @@ -6684,9 +6928,9 @@ "dev": true }, "node_modules/cacache": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.0.tgz", - "integrity": "sha512-I7mVOPl3PUCeRub1U8YoGz2Lqv9WOBpobZ8RyWFXmReuILz+3OAyTa5oH3QPdtKZD7N0Yk00aLfzn0qvp8dZ1w==", + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz", + "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==", "dev": true, "dependencies": { "@npmcli/fs": "^3.1.0", @@ -6694,7 +6938,7 @@ "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", - "minipass-collect": "^1.0.2", + "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^4.0.0", @@ -6716,16 +6960,16 @@ } }, "node_modules/cacache/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", + "jackspeak": "^2.3.6", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -6738,21 +6982,18 @@ } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", - "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, "engines": { "node": "14 || >=16.14" } }, "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -6765,14 +7006,19 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6814,9 +7060,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001580", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001580.tgz", - "integrity": "sha512-mtj5ur2FFPZcCEpXFy8ADXbDACuNFXg6mxVDqp7tqooX6l3zwm+d8EPoeOSIFRDvHs8qu7/SLFOGniULkcH2iA==", + "version": "1.0.30001609", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001609.tgz", + "integrity": "sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA==", "dev": true, "funding": [ { @@ -7181,15 +7427,45 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "dev": true, "engines": { "node": ">=0.8" } }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -7560,12 +7836,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", - "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==", + "version": "3.36.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz", + "integrity": "sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==", "dev": true, "dependencies": { - "browserslist": "^4.22.2" + "browserslist": "^4.23.0" }, "funding": { "type": "opencollective", @@ -7854,26 +8130,77 @@ "node": ">=8" } }, - "node_modules/date-format": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", - "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, "engines": { - "node": ">=4.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, "engines": { - "node": "*" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/debug": { - "version": "4.3.4", + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, @@ -8019,17 +8346,20 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -8111,9 +8441,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "dev": true, "engines": { "node": ">=8" @@ -8125,6 +8455,18 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/dev-ip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", + "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", + "dev": true, + "bin": { + "dev-ip": "lib/dev-ip.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", @@ -8265,9 +8607,9 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.2.tgz", + "integrity": "sha512-HTlk5nmhkm8F6JcdXvHIzaorzCoziNQT9mGxLPVXW8wJF1TiGSL60ZGB4gHWabHOaMmWmhvk2/lPHfnBiT78AQ==", "dev": true, "engines": { "node": ">=12" @@ -8297,6 +8639,100 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/easy-extender": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", + "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", + "dev": true, + "dependencies": { + "lodash": "^4.17.10" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/eazy-logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", + "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", + "dev": true, + "dependencies": { + "chalk": "4.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eazy-logger/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eazy-logger/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eazy-logger/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eazy-logger/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eazy-logger/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eazy-logger/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/edge-runtime": { "version": "2.5.7", "resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.7.tgz", @@ -8363,9 +8799,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.647", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.647.tgz", - "integrity": "sha512-Z/fTNGwc45WrYQhPaEcz5tAJuZZ8G7S/DBnhS6Kgp4BxnS40Z/HqlJ0hHg3Z79IGVzuVartIlTcjw/cQbPLgOw==", + "version": "1.4.735", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.735.tgz", + "integrity": "sha512-pkYpvwg8VyOTQAeBqZ7jsmpCjko1Qc6We1ZtZCjRyYbT5v4AIUKDy5cQTRotQlSSZmMr8jqpEt6JtOj5k7lR7A==", "dev": true }, "node_modules/emoji-regex": { @@ -8445,19 +8881,32 @@ "node": ">=10.2.0" } }, + "node_modules/engine.io-client": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, "node_modules/engine.io-parser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", - "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", "dev": true, "engines": { "node": ">=10.0.0" } }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -8535,50 +8984,57 @@ } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -8587,21 +9043,54 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", + "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==", "dev": true }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -8625,11 +9114,12 @@ } }, "node_modules/esbuild": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", - "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.5.tgz", + "integrity": "sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==", "dev": true, "hasInstallScript": true, + "optional": true, "bin": { "esbuild": "bin/esbuild" }, @@ -8637,29 +9127,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.11", - "@esbuild/android-arm": "0.19.11", - "@esbuild/android-arm64": "0.19.11", - "@esbuild/android-x64": "0.19.11", - "@esbuild/darwin-arm64": "0.19.11", - "@esbuild/darwin-x64": "0.19.11", - "@esbuild/freebsd-arm64": "0.19.11", - "@esbuild/freebsd-x64": "0.19.11", - "@esbuild/linux-arm": "0.19.11", - "@esbuild/linux-arm64": "0.19.11", - "@esbuild/linux-ia32": "0.19.11", - "@esbuild/linux-loong64": "0.19.11", - "@esbuild/linux-mips64el": "0.19.11", - "@esbuild/linux-ppc64": "0.19.11", - "@esbuild/linux-riscv64": "0.19.11", - "@esbuild/linux-s390x": "0.19.11", - "@esbuild/linux-x64": "0.19.11", - "@esbuild/netbsd-x64": "0.19.11", - "@esbuild/openbsd-x64": "0.19.11", - "@esbuild/sunos-x64": "0.19.11", - "@esbuild/win32-arm64": "0.19.11", - "@esbuild/win32-ia32": "0.19.11", - "@esbuild/win32-x64": "0.19.11" + "@esbuild/android-arm": "0.19.5", + "@esbuild/android-arm64": "0.19.5", + "@esbuild/android-x64": "0.19.5", + "@esbuild/darwin-arm64": "0.19.5", + "@esbuild/darwin-x64": "0.19.5", + "@esbuild/freebsd-arm64": "0.19.5", + "@esbuild/freebsd-x64": "0.19.5", + "@esbuild/linux-arm": "0.19.5", + "@esbuild/linux-arm64": "0.19.5", + "@esbuild/linux-ia32": "0.19.5", + "@esbuild/linux-loong64": "0.19.5", + "@esbuild/linux-mips64el": "0.19.5", + "@esbuild/linux-ppc64": "0.19.5", + "@esbuild/linux-riscv64": "0.19.5", + "@esbuild/linux-s390x": "0.19.5", + "@esbuild/linux-x64": "0.19.5", + "@esbuild/netbsd-x64": "0.19.5", + "@esbuild/openbsd-x64": "0.19.5", + "@esbuild/sunos-x64": "0.19.5", + "@esbuild/win32-arm64": "0.19.5", + "@esbuild/win32-ia32": "0.19.5", + "@esbuild/win32-x64": "0.19.5" } }, "node_modules/esbuild-android-64": { @@ -8935,9 +9424,9 @@ } }, "node_modules/esbuild-wasm": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.19.11.tgz", - "integrity": "sha512-MIhnpc1TxERUHomteO/ZZHp+kUawGEc03D/8vMHGzffLvbFLeDe6mwxqEZwlqBNY7SLWbyp6bBQAcCen8+wpjQ==", + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.19.5.tgz", + "integrity": "sha512-7zmLLn2QCj93XfMmHtzrDJ1UBuOHB2CZz1ghoCEZiRajxjUvHsF40PnbzFIY/pmesqPRaEtEWii0uzsTbnAgrA==", "dev": true, "bin": { "esbuild": "bin/esbuild" @@ -8995,9 +9484,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -9264,9 +9753,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -9461,6 +9950,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -9522,17 +10017,17 @@ "dev": true }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -9563,34 +10058,10 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dev": true, - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/express/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true, "engines": { "node": ">= 0.6" @@ -9605,6 +10076,24 @@ "ms": "2.0.0" } }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -9629,21 +10118,6 @@ "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", "dev": true }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dev": true, - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -9664,23 +10138,68 @@ } ] }, - "node_modules/express/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "node_modules/express/node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, "engines": { - "node": ">= 0.8" + "node": ">= 0.8.0" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "node_modules/express/node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/external-editor": { - "version": "3.1.0", + "node_modules/express/node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "dev": true, @@ -9712,9 +10231,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -9740,9 +10259,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -9858,17 +10377,17 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", "dev": true, "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~1.0.1", "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", "unpipe": "~1.0.0" }, "engines": { @@ -9890,27 +10409,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/finalhandler/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/find-cache-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", @@ -9964,15 +10462,15 @@ } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -10077,6 +10575,17 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true }, + "node_modules/fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", @@ -10199,16 +10708,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -10335,13 +10848,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -10581,21 +11095,21 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "dev": true, "engines": { "node": ">= 0.4" @@ -10617,12 +11131,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -10638,9 +11152,9 @@ "dev": true }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "dependencies": { "function-bind": "^1.1.2" @@ -10760,9 +11274,9 @@ } }, "node_modules/html-entities": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", - "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", "dev": true, "funding": [ { @@ -10858,9 +11372,9 @@ } }, "node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "dependencies": { "agent-base": "^7.1.0", @@ -11067,9 +11581,9 @@ } }, "node_modules/ignore-walk": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz", - "integrity": "sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.4.tgz", + "integrity": "sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==", "dev": true, "dependencies": { "minimatch": "^9.0.0" @@ -11088,9 +11602,9 @@ } }, "node_modules/ignore-walk/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -11115,6 +11629,15 @@ "node": ">=0.10.0" } }, + "node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -11231,12 +11754,12 @@ } }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -11244,10 +11767,23 @@ "node": ">= 0.4" } }, - "node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, "node_modules/ipaddr.js": { @@ -11260,14 +11796,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11358,6 +11896,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -11440,9 +11993,9 @@ "dev": true }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "engines": { "node": ">= 0.4" @@ -11460,6 +12013,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-like": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", + "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "dev": true, + "dependencies": { + "lodash.isfinite": "^3.3.2" + } + }, "node_modules/is-number-object": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", @@ -11534,12 +12096,15 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11600,12 +12165,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -11782,9 +12347,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -12066,6 +12631,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -12091,10 +12662,13 @@ "dev": true }, "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/json-schema-to-ts": { "version": "1.6.4", @@ -12142,6 +12716,15 @@ "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", "dev": true }, + "node_modules/jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -12397,6 +12980,15 @@ "node": ">=0.10.0" } }, + "node_modules/karma/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/karma/node_modules/ua-parser-js": { "version": "0.7.37", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.37.tgz", @@ -12621,6 +13213,12 @@ } } }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", + "dev": true + }, "node_modules/lines-and-columns": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", @@ -12685,59 +13283,25 @@ "node": ">= 12.13.0" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/localtunnel": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.2.tgz", + "integrity": "sha512-n418Cn5ynvJd7m/N1d9WVJISLJF/ellZnfsLnx8WBWGzxv/ntNcFkJ1o6se5quUhCplfLGBNL5tYHiq5WF3Nug==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "axios": "0.21.4", + "debug": "4.3.2", + "openurl": "1.1.1", + "yargs": "17.1.1" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true - }, - "node_modules/lodash.ismatch": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", - "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "bin": { + "lt": "bin/lt.js" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8.3.0" } }, - "node_modules/log-symbols/node_modules/ansi-styles": { + "node_modules/localtunnel/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", @@ -12752,23 +13316,18 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/localtunnel/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "node_modules/log-symbols/node_modules/color-convert": { + "node_modules/localtunnel/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", @@ -12780,23 +13339,182 @@ "node": ">=7.0.0" } }, - "node_modules/log-symbols/node_modules/color-name": { + "node_modules/localtunnel/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/log-symbols/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/localtunnel/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, + "dependencies": { + "ms": "2.1.2" + }, "engines": { - "node": ">=8" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "0.1.0", + "node_modules/localtunnel/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/localtunnel/node_modules/yargs": { + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.1.1.tgz", + "integrity": "sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.isfinite": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", + "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", + "dev": true + }, + "node_modules/lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, @@ -13196,6 +13914,15 @@ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", "dev": true }, + "node_modules/micro/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micro/node_modules/toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", @@ -13360,35 +14087,17 @@ } }, "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-collect/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.0.3" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/minipass-collect/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/minipass-fetch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", @@ -13555,6 +14264,12 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/mitt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", + "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", + "dev": true + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -13586,9 +14301,9 @@ } }, "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", "dev": true, "engines": { "node": ">=10" @@ -13647,13 +14362,12 @@ "dev": true }, "node_modules/needle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", - "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, "optional": true, "dependencies": { - "debug": "^3.2.6", "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, @@ -13664,16 +14378,6 @@ "node": ">= 4.4.x" } }, - "node_modules/needle/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "optional": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/needle/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -13871,9 +14575,9 @@ } }, "node_modules/node-gyp": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.0.1.tgz", - "integrity": "sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.1.0.tgz", + "integrity": "sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==", "dev": true, "dependencies": { "env-paths": "^2.2.0", @@ -13895,9 +14599,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.0.tgz", - "integrity": "sha512-PbZERfeFdrHQOOXiAKOY0VPbykZy90ndPKk0d+CFDegTKmWp1VgOTz2xACVbr1BjCWxrQp68CXtvNsveFhqDJg==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", + "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", "dev": true, "bin": { "node-gyp-build": "bin.js", @@ -13915,16 +14619,16 @@ } }, "node_modules/node-gyp/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", + "jackspeak": "^2.3.6", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -13946,9 +14650,9 @@ } }, "node_modules/node-gyp/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -14096,24 +14800,21 @@ } }, "node_modules/npm-package-arg/node_modules/lru-cache": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", - "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, "engines": { "node": "14 || >=16.14" } }, "node_modules/npm-packlist": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.0.tgz", - "integrity": "sha512-ErAGFB5kJUciPy1mmx/C2YFbvxoJ0QJ9uwkCZOeR6CqLLISPZBOiFModAbSXnjjlwW5lOhuhXva+fURsSGJqyw==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", "dev": true, "dependencies": { - "ignore-walk": "^6.0.0" + "ignore-walk": "^6.0.4" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -14135,11 +14836,12 @@ } }, "node_modules/npm-registry-fetch": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.1.0.tgz", - "integrity": "sha512-PQCELXKt8Azvxnt5Y85GseQDJJlglTFM9L9U9gkv2y4e9s0k3GVDdOx3YoB6gm2Do0hlkzC39iCGXby+Wve1Bw==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.2.0.tgz", + "integrity": "sha512-zVH+G0q1O2hqgQBUvQ2LWp6ujr6VJAeDnmWxqiMlCguvLexEzBnuQIwC70r04vcvCMAcYEIpA/rO9YyVi+fmJQ==", "dev": true, "dependencies": { + "@npmcli/redact": "^1.1.0", "make-fetch-happen": "^13.0.0", "minipass": "^7.0.2", "minipass-fetch": "^3.0.0", @@ -14374,12 +15076,12 @@ "dev": true }, "node_modules/nx/node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", "dev": true, "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -14434,9 +15136,9 @@ } }, "node_modules/nx/node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "dependencies": { "graceful-fs": "^4.2.0", @@ -14600,13 +15302,13 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -14694,6 +15396,33 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/openurl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", + "integrity": "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==", + "dev": true + }, + "node_modules/opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/opn/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -14984,6 +15713,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/parse-json/node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -15139,12 +15874,12 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", "dev": true, "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { @@ -15155,13 +15890,10 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", - "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, "engines": { "node": "14 || >=16.14" } @@ -15227,11 +15959,12 @@ } }, "node_modules/piscina": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.2.1.tgz", - "integrity": "sha512-LShp0+lrO+WIzB9LXO+ZmO4zGHxtTJNZhEO56H9SSu+JPaUQb6oLcTCzWi5IL2DS8/vIkCE88ElahuSSw4TAkA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.1.0.tgz", + "integrity": "sha512-sjbLMi3sokkie+qmtZpkfMCUJTpbxJm/wvaPzU28vmYSsTSW8xk9JcFUsbqGJdtPpIQ9tuj+iDcTtgZjwnOSig==", "dev": true, "dependencies": { + "eventemitter-asyncresource": "^1.0.0", "hdr-histogram-js": "^2.0.1", "hdr-histogram-percentiles-obj": "^3.0.0" }, @@ -15412,10 +16145,42 @@ "ms": "^2.1.1" } }, + "node_modules/portscanner": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", + "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", + "dev": true, + "dependencies": { + "async": "^2.6.0", + "is-number-like": "^1.0.3" + }, + "engines": { + "node": ">=0.4", + "npm": ">=1.0.0" + } + }, + "node_modules/portscanner/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { - "version": "8.4.33", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", - "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -15432,7 +16197,7 @@ } ], "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -15441,14 +16206,14 @@ } }, "node_modules/postcss-loader": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", - "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.3.tgz", + "integrity": "sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==", "dev": true, "dependencies": { - "cosmiconfig": "^8.3.5", - "jiti": "^1.20.0", - "semver": "^7.5.4" + "cosmiconfig": "^8.2.0", + "jiti": "^1.18.2", + "semver": "^7.3.8" }, "engines": { "node": ">= 14.15.0" @@ -15463,9 +16228,9 @@ } }, "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "dev": true, "engines": { "node": "^10 || ^12 || >= 14" @@ -15475,9 +16240,9 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", - "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", "dev": true, "dependencies": { "icss-utils": "^5.0.0", @@ -15492,9 +16257,9 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", "dev": true, "dependencies": { "postcss-selector-parser": "^6.0.4" @@ -15522,9 +16287,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", "dev": true, "dependencies": { "cssesc": "^3.0.0", @@ -15905,15 +16670,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/read-package-json/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -15924,16 +16680,16 @@ } }, "node_modules/read-package-json/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", + "jackspeak": "^2.3.6", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -15957,31 +16713,19 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/read-package-json/node_modules/lru-cache": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.2.tgz", - "integrity": "sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, "engines": { "node": "14 || >=16.14" } }, "node_modules/read-package-json/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -16202,9 +16946,9 @@ } }, "node_modules/reflect-metadata": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", - "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", "dev": true }, "node_modules/regenerate": { @@ -16241,20 +16985,21 @@ } }, "node_modules/regex-parser": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", - "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", "dev": true }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -16399,6 +17144,34 @@ "node": ">=0.10.0" } }, + "node_modules/resp-modifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", + "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", + "dev": true, + "dependencies": { + "debug": "^2.2.0", + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/resp-modifier/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/resp-modifier/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -16432,9 +17205,9 @@ } }, "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", "dev": true }, "node_modules/rimraf": { @@ -16453,10 +17226,13 @@ } }, "node_modules/rollup": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.4.1.tgz", - "integrity": "sha512-idZzrUpWSblPJX66i+GzrpjKE3vbYrlWirUHteoAbjKReZwa0cohAErOYA5efoMmNCdvG9yrJS+w9Kl6csaH4w==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.2.tgz", + "integrity": "sha512-WkeoTWvuBoFjFAhsEOHKRoZ3r9GfTyhh7Vff1zwebEFLEFjT1lG3784xEgKiTa7E+e70vsC81roVL2MP4tgEEQ==", "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, "bin": { "rollup": "dist/bin/rollup" }, @@ -16465,18 +17241,21 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.4.1", - "@rollup/rollup-android-arm64": "4.4.1", - "@rollup/rollup-darwin-arm64": "4.4.1", - "@rollup/rollup-darwin-x64": "4.4.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.4.1", - "@rollup/rollup-linux-arm64-gnu": "4.4.1", - "@rollup/rollup-linux-arm64-musl": "4.4.1", - "@rollup/rollup-linux-x64-gnu": "4.4.1", - "@rollup/rollup-linux-x64-musl": "4.4.1", - "@rollup/rollup-win32-arm64-msvc": "4.4.1", - "@rollup/rollup-win32-ia32-msvc": "4.4.1", - "@rollup/rollup-win32-x64-msvc": "4.4.1", + "@rollup/rollup-android-arm-eabi": "4.14.2", + "@rollup/rollup-android-arm64": "4.14.2", + "@rollup/rollup-darwin-arm64": "4.14.2", + "@rollup/rollup-darwin-x64": "4.14.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.14.2", + "@rollup/rollup-linux-arm64-gnu": "4.14.2", + "@rollup/rollup-linux-arm64-musl": "4.14.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.14.2", + "@rollup/rollup-linux-riscv64-gnu": "4.14.2", + "@rollup/rollup-linux-s390x-gnu": "4.14.2", + "@rollup/rollup-linux-x64-gnu": "4.14.2", + "@rollup/rollup-linux-x64-musl": "4.14.2", + "@rollup/rollup-win32-arm64-msvc": "4.14.2", + "@rollup/rollup-win32-ia32-msvc": "4.14.2", + "@rollup/rollup-win32-x64-msvc": "4.14.2", "fsevents": "~2.3.2" } }, @@ -16512,6 +17291,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", + "dev": true + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -16609,13 +17394,13 @@ } }, "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -16633,15 +17418,18 @@ "dev": true }, "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -16653,9 +17441,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.69.7", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.7.tgz", - "integrity": "sha512-rzj2soDeZ8wtE2egyLXgOOHQvaC2iosZrkF6v3EUG+tBwEvhqUCzm0VP3k9gHF9LXbSrRhT5SksoI56Iw8NPnQ==", + "version": "1.69.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", + "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -16670,9 +17458,9 @@ } }, "node_modules/sass-loader": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.3.tgz", - "integrity": "sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.2.tgz", + "integrity": "sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg==", "dev": true, "dependencies": { "neo-async": "^2.6.2" @@ -16707,9 +17495,9 @@ } }, "node_modules/sass/node_modules/immutable": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", - "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", "dev": true }, "node_modules/sax": { @@ -16797,24 +17585,24 @@ "dev": true }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", "dev": true, "dependencies": { "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", + "depd": "~1.1.2", + "destroy": "~1.0.4", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" }, "engines": { "node": ">= 0.8.0" @@ -16829,43 +17617,76 @@ "ms": "2.0.0" } }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "node_modules/send/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/send/node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", "dev": true }, - "node_modules/send/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/send/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "dev": true, "dependencies": { - "ee-first": "1.1.1" + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/send/node_modules/mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "dev": true, + "bin": { + "mime": "cli.js" } }, + "node_modules/send/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, "node_modules/send/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", "dev": true, "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -16940,21 +17761,36 @@ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", "dev": true }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", "dev": true, "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" + "parseurl": "~1.3.2", + "send": "0.16.2" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "dev": true + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -16962,29 +17798,32 @@ "dev": true }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -17039,9 +17878,9 @@ } }, "node_modules/shiki": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz", - "integrity": "sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==", + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", + "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", "dev": true, "dependencies": { "ansi-sequence-parser": "^1.1.0", @@ -17051,14 +17890,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -17071,15 +17914,17 @@ "dev": true }, "node_modules/sigstore": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.1.0.tgz", - "integrity": "sha512-kPIj+ZLkyI3QaM0qX8V/nSsweYND3W448pwkDgS6CQ74MfhEkIR8ToK5Iyx46KJYRjseVcD3Rp9zAmUAj6ZjPw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.0.tgz", + "integrity": "sha512-q+o8L2ebiWD1AxD17eglf1pFrl9jtW7FHa0ygqY6EKvibK8JHyq9Z26v9MZXeDiw+RbfOJ9j2v70M10Hd6E06A==", "dev": true, "dependencies": { - "@sigstore/bundle": "^2.1.0", - "@sigstore/protobuf-specs": "^0.2.1", - "@sigstore/sign": "^2.1.0", - "@sigstore/tuf": "^2.1.0" + "@sigstore/bundle": "^2.3.1", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.1", + "@sigstore/sign": "^2.3.0", + "@sigstore/tuf": "^2.3.1", + "@sigstore/verify": "^1.2.0" }, "engines": { "node": "^16.14.0 || >=18.0.0" @@ -17111,9 +17956,9 @@ } }, "node_modules/socket.io": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", - "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", "dev": true, "dependencies": { "accepts": "~1.3.4", @@ -17129,14 +17974,30 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", "dev": true, "dependencies": { + "debug": "~4.3.4", "ws": "~8.11.0" } }, + "node_modules/socket.io-client": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -17171,26 +18032,26 @@ } }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "dev": true, "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks-proxy-agent": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", - "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz", + "integrity": "sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A==", "dev": true, "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.1", "debug": "^4.3.4", "socks": "^2.7.1" }, @@ -17208,25 +18069,26 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", - "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", + "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", "dev": true, "dependencies": { + "abab": "^2.0.6", "iconv-lite": "^0.6.3", "source-map-js": "^1.0.2" }, "engines": { - "node": ">= 18.12.0" + "node": ">= 14.15.0" }, "funding": { "type": "opencollective", @@ -17278,9 +18140,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -17294,9 +18156,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", - "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", "dev": true }, "node_modules/spdy": { @@ -17375,14 +18237,36 @@ "dev": true }, "node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", "dev": true, "engines": { "node": ">= 0.6" } }, + "node_modules/stream-throttle": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", + "integrity": "sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==", + "dev": true, + "dependencies": { + "commander": "^2.2.0", + "limiter": "^1.0.5" + }, + "bin": { + "throttleproxy": "bin/throttleproxy.js" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/stream-throttle/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/stream-to-array": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/stream-to-array/-/stream-to-array-2.3.0.tgz", @@ -17517,14 +18401,15 @@ } }, "node_modules/string.prototype.padend": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz", - "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", + "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -17534,14 +18419,15 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -17551,28 +18437,31 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -17705,9 +18594,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dev": true, "dependencies": { "chownr": "^2.0.0", @@ -17811,9 +18700,9 @@ } }, "node_modules/terser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", - "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -17829,16 +18718,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", - "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", - "terser": "^5.16.8" + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -17887,6 +18776,12 @@ "ajv": "^6.9.1" } }, + "node_modules/terser-webpack-plugin/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -17911,6 +18806,24 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.30.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz", + "integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -18080,12 +18993,12 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { - "node": ">=16.13.0" + "node": ">=16" }, "peerDependencies": { "typescript": ">=4.2.0" @@ -18209,9 +19122,9 @@ "dev": true }, "node_modules/tuf-js": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.1.0.tgz", - "integrity": "sha512-eD7YPPjVlMzdggrOeE8zwoegUaG/rt6Bt3jwoQPunRiNVzgcCE009UDFJKJjG+Gk9wFu6W/Vi+P5d/5QpdD9jA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.0.tgz", + "integrity": "sha512-ZSDngmP1z6zw+FIkIBjvOp/II/mIub/O7Pp12j1WNsiCpg5R5wAc//i555bBQsE44O94btLt0xM/Zr2LQjwdCg==", "dev": true, "dependencies": { "@tufjs/models": "2.0.0", @@ -18260,29 +19173,30 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -18292,16 +19206,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -18311,14 +19226,20 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -18370,9 +19291,9 @@ } }, "node_modules/typedoc/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -18397,6 +19318,29 @@ "node": ">=14.17" } }, + "node_modules/ua-parser-js": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz", + "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -18432,15 +19376,15 @@ } }, "node_modules/undici": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.2.1.tgz", - "integrity": "sha512-7Wa9thEM6/LMnnKtxJHlc8SrTlDmxqJecgz1iy8KlsN0/iskQXOQCuPkrZLXbElPaSw5slFFyKIKXyJ3UtbApw==", + "version": "5.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", + "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", "dev": true, "dependencies": { "@fastify/busboy": "^2.0.0" }, "engines": { - "node": ">=18.0" + "node": ">=14.0" } }, "node_modules/undici-types": { @@ -18744,29 +19688,29 @@ } }, "node_modules/vite": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", - "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", + "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", "dev": true, "dependencies": { - "esbuild": "^0.19.3", - "postcss": "^8.4.32", - "rollup": "^4.2.0" + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "fsevents": "~2.3.3" + "fsevents": "~2.3.2" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": ">= 14", "less": "*", "lightningcss": "^1.21.0", "sass": "*", @@ -18798,6 +19742,411 @@ } } }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", @@ -18820,9 +20169,9 @@ "dev": true }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -18996,10 +20345,19 @@ } } }, + "node_modules/webpack-dev-server/node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", - "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", "dev": true, "dependencies": { "colorette": "^2.0.10", @@ -19131,6 +20489,12 @@ "node": ">=4.0" } }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/webpack/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -19244,16 +20608,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -19432,6 +20796,15 @@ "node": ">= 6.0" } }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 1f24629b6..0b3258a91 100644 --- a/package.json +++ b/package.json @@ -67,15 +67,15 @@ }, "private": true, "dependencies": { - "@angular/animations": "17.0.3", - "@angular/cdk": "17.0.1", - "@angular/common": "17.0.3", - "@angular/compiler": "17.0.3", - "@angular/core": "17.0.3", - "@angular/forms": "17.0.3", - "@angular/platform-browser": "17.0.3", - "@angular/platform-browser-dynamic": "17.0.3", - "@angular/router": "17.0.3", + "@angular/animations": "17.0.6", + "@angular/cdk": "17.0.6", + "@angular/common": "17.0.6", + "@angular/compiler": "17.0.6", + "@angular/core": "17.0.6", + "@angular/forms": "17.0.6", + "@angular/platform-browser": "17.0.6", + "@angular/platform-browser-dynamic": "17.0.6", + "@angular/router": "17.0.6", "@scion/components": "17.0.0", "@scion/components.internal": "17.0.0", "@scion/microfrontend-platform": "1.2.2", @@ -85,14 +85,14 @@ "zone.js": "0.14.2" }, "devDependencies": { - "@angular-devkit/build-angular": "17.1.1", + "@angular-devkit/build-angular": "17.0.6", "@angular-eslint/builder": "17.1.0", "@angular-eslint/eslint-plugin": "17.1.0", "@angular-eslint/eslint-plugin-template": "17.1.0", "@angular-eslint/schematics": "17.1.0", "@angular-eslint/template-parser": "17.1.0", - "@angular/cli": "17.0.1", - "@angular/compiler-cli": "17.0.3", + "@angular/cli": "17.0.6", + "@angular/compiler-cli": "17.0.6", "@playwright/test": "1.40.0", "@types/jasmine": "5.1.2", "@typescript-eslint/eslint-plugin": "6.11.0", diff --git a/projects/scion/e2e-testing/src/app-header.po.ts b/projects/scion/e2e-testing/src/app-header.po.ts index 5b49aec07..87232af06 100644 --- a/projects/scion/e2e-testing/src/app-header.po.ts +++ b/projects/scion/e2e-testing/src/app-header.po.ts @@ -24,7 +24,7 @@ export class AppHeaderPO { * Handle to the specified perspective toggle button. * * @param locateBy - Specifies how to locate the perspective toggle button. - * @property perspectiveId - Identifies the toggle button by the perspective id + * @param locateBy.perspectiveId - Identifies the toggle button by the perspective id */ public perspectiveToggleButton(locateBy: {perspectiveId: string}): PerspectiveTogglePO { return new PerspectiveTogglePO(this._locator.locator('div.e2e-perspective-toggles').locator(`button.e2e-perspective[data-perspectiveid="${locateBy.perspectiveId}"]`)); diff --git a/projects/scion/e2e-testing/src/app.po.ts b/projects/scion/e2e-testing/src/app.po.ts index 99532a202..17a304100 100644 --- a/projects/scion/e2e-testing/src/app.po.ts +++ b/projects/scion/e2e-testing/src/app.po.ts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {coerceArray, DomRect, fromRect, waitUntilStable} from './helper/testing.util'; +import {coerceArray, DomRect, fromRect, waitForCondition, waitUntilStable} from './helper/testing.util'; import {StartPagePO} from './start-page.po'; import {Locator, Page} from '@playwright/test'; import {PartPO} from './part.po'; @@ -19,6 +19,7 @@ import {MessageBoxPO} from './message-box.po'; import {NotificationPO} from './notification.po'; import {AppHeaderPO} from './app-header.po'; import {DialogPO} from './dialog.po'; +import {ViewId} from '@scion/workbench'; export class AppPO { @@ -52,6 +53,15 @@ export class AppPO { * By passing a features object, you can control how to start the workbench and which app features to enable. */ public async navigateTo(options?: Options): Promise { + // Prepare local storage. + if (options?.localStorage) { + await this.navigateTo({microfrontendSupport: false}); + await this.page.evaluate(data => { + Object.entries(data).forEach(([key, value]) => window.localStorage.setItem(key, value)); + }, options.localStorage); + await this.page.goto('about:blank'); + } + this._workbenchStartupQueryParams = new URLSearchParams(); this._workbenchStartupQueryParams.append(WorkenchStartupQueryParams.LAUNCHER, options?.launcher ?? 'LAZY'); this._workbenchStartupQueryParams.append(WorkenchStartupQueryParams.STANDALONE, `${(options?.microfrontendSupport ?? true) === false}`); @@ -98,6 +108,13 @@ export class AppPO { await waitUntilStable(() => this.getCurrentNavigationId()); } + /** + * Opens a new browser window. + */ + public async openNewWindow(): Promise { + return new AppPO(await this.page.context().newPage()); + } + /** * Instructs the browser to move back one page in the session history. */ @@ -130,7 +147,7 @@ export class AppPO { * Handle to the specified part in the workbench layout. * * @param locateBy - Specifies how to locate the part. - * @property partId - Identifies the part by its id + * @param locateBy.partId - Identifies the part by its id */ public part(locateBy: {partId: string}): PartPO { return new PartPO(this.page.locator(`wb-part[data-partid="${locateBy.partId}"]`)); @@ -159,10 +176,10 @@ export class AppPO { * Handle to the specified view in the workbench layout. * * @param locateBy - Specifies how to locate the view. Either `viewId` or `cssClass`, or both must be set. - * @property viewId? - Identifies the view by its id - * @property cssClass? - Identifies the view by its CSS class + * @param locateBy.viewId - Identifies the view by its id + * @param locateBy.cssClass - Identifies the view by its CSS class */ - public view(locateBy: {viewId?: string; cssClass?: string}): ViewPO { + public view(locateBy: {viewId?: ViewId; cssClass?: string}): ViewPO { if (locateBy.viewId !== undefined && locateBy.cssClass !== undefined) { const viewLocator = this.page.locator(`wb-view[data-viewid="${locateBy.viewId}"].${locateBy.cssClass}`); const viewTabLocator = this.page.locator(`wb-view-tab[data-viewid="${locateBy.viewId}"].${locateBy.cssClass}`); @@ -235,17 +252,21 @@ export class AppPO { * Opens a new view tab. */ public async openNewViewTab(): Promise { + const navigationId = await this.getCurrentNavigationId(); await this.header.clickMenuItem({cssClass: 'e2e-open-start-page'}); // Wait until opened the start page to get its view id. - await waitUntilStable(() => this.getCurrentNavigationId()); - return new StartPagePO(this, {viewId: await this.activePart({inMainArea: true}).activeView.getViewId()}); + await waitForCondition(async () => (await this.getCurrentNavigationId()) !== navigationId); + const inMainArea = await this.hasMainArea(); + return new StartPagePO(this, {viewId: await this.activePart({inMainArea}).activeView.getViewId()}); } /** * Switches to the specified perspective. */ public async switchPerspective(perspectiveId: string): Promise { + const navigationId = await this.getCurrentNavigationId(); await this.header.perspectiveToggleButton({perspectiveId}).click(); + await waitForCondition(async () => (await this.getCurrentNavigationId()) !== navigationId); } /** @@ -278,7 +299,7 @@ export class AppPO { * * @see WORKBENCH_ID */ - public getWorkbenchIdId(): Promise { + public getWorkbenchId(): Promise { return this.page.locator('app-root').getAttribute('data-workbench-id').then(value => value ?? undefined); } @@ -307,6 +328,27 @@ export class AppPO { const pageFunction = (workbenchElement: HTMLElement, token: {name: string; value: string}): void => workbenchElement.style.setProperty(token.name, token.value); await this.workbench.evaluate(pageFunction, {name, value}); } + + /** + * Obtains the name of the current browser window. + */ + public getWindowName(): Promise { + return this.page.evaluate(() => window.name); + } + + /** + * Obtains the value associated with the specified key from local storage. + */ + public getLocalStorageItem(key: string): Promise { + return this.page.evaluate(key => localStorage.getItem(key), key); + } + + /** + * Tests if the layout has a main area. + */ + public hasMainArea(): Promise { + return this.workbench.locator('wb-main-area-layout').isVisible(); + } } /** @@ -346,6 +388,10 @@ export interface Options { * Controls the scope of application-modal workbench dialogs. By default, if not specified, workbench scope will be used. */ dialogModalityScope?: 'workbench' | 'viewport'; + /** + * Specifies data to be in local storage. + */ + localStorage?: {[key: string]: string}; } /** diff --git a/projects/scion/e2e-testing/src/helper/testing.util.ts b/projects/scion/e2e-testing/src/helper/testing.util.ts index 2050c03f4..abb151eec 100644 --- a/projects/scion/e2e-testing/src/helper/testing.util.ts +++ b/projects/scion/e2e-testing/src/helper/testing.util.ts @@ -10,6 +10,7 @@ import {Locator, Page} from '@playwright/test'; import {exhaustMap, filter, firstValueFrom, map, pairwise, timer} from 'rxjs'; +import {Commands} from '@scion/workbench'; /** * Returns if given CSS class is present on given element. @@ -55,6 +56,18 @@ export async function waitUntilStable(value: () => Promise | A, options?: return firstValueFrom(value$); } +/** + * Waits for a condition to be fulfilled. + */ +export async function waitForCondition(predicate: () => Promise): Promise { + const value$ = timer(0, 100) + .pipe( + exhaustMap(async () => await predicate()), + filter(Boolean), + ); + await firstValueFrom(value$); +} + /** * Waits until given locators are attached to the DOM. */ @@ -131,6 +144,13 @@ export function coerceMap(value: Record | Map): Map | null | undefined): string { + return Object.entries(object ?? {}).map(([key, value]) => `${key}=${value}`).join(';'); +} + /** * Returns a new {@link Record} with `undefined` and `` values removed. */ @@ -170,3 +190,22 @@ export interface DomRect { hcenter: number; vcenter: number; } + +/** + * Converts given segments to a path. + */ +export function commandsToPath(commands: Commands): string { + return commands + .reduce((path, command) => { + if (typeof command === 'string') { + return path.concat(command); + } + else if (!path.length) { + return path.concat(`.;${toMatrixNotation(command)}`); // Note that matrix parameters in the first segment are only supported in combination with a `relativeTo`. + } + else { + return path.concat(`${path.pop()};${toMatrixNotation(command)}`); + } + }, []) + .join('/'); +} diff --git a/projects/scion/e2e-testing/src/matcher/to-equal-workbench-layout.matcher.ts b/projects/scion/e2e-testing/src/matcher/to-equal-workbench-layout.matcher.ts index 6790336ed..750b77546 100644 --- a/projects/scion/e2e-testing/src/matcher/to-equal-workbench-layout.matcher.ts +++ b/projects/scion/e2e-testing/src/matcher/to-equal-workbench-layout.matcher.ts @@ -12,6 +12,7 @@ import {Locator} from '@playwright/test'; import {ExpectationResult} from './custom-matchers.definition'; import {MAIN_AREA} from '../workbench.model'; import {retryOnError} from '../helper/testing.util'; +import {ViewId} from '@scion/workbench'; /** * Provides the implementation of {@link CustomMatchers#toEqualWorkbenchLayout}. @@ -357,7 +358,7 @@ export class MPart { public readonly type = 'MPart'; public readonly id?: string; public views?: MView[]; - public activeViewId?: string; + public activeViewId?: ViewId; constructor(part: Omit) { Object.assign(this, part); @@ -368,5 +369,5 @@ export class MPart { * Modified version of {@link MView} to expect the workbench layout. */ export interface MView { - readonly id: string; + readonly id: ViewId; } diff --git a/projects/scion/e2e-testing/src/start-page.po.ts b/projects/scion/e2e-testing/src/start-page.po.ts index f32e4ff27..33059d12a 100644 --- a/projects/scion/e2e-testing/src/start-page.po.ts +++ b/projects/scion/e2e-testing/src/start-page.po.ts @@ -14,6 +14,8 @@ import {Locator} from '@playwright/test'; import {SciTabbarPO} from './@scion/components.internal/tabbar.po'; import {SciRouterOutletPO} from './workbench-client/page-object/sci-router-outlet.po'; import {WorkbenchViewPagePO} from './workbench/page-object/workbench-view-page.po'; +import {ViewId} from '@scion/workbench'; +import {waitForCondition} from './helper/testing.util'; /** * Page object to interact with {@link StartPageComponent}. @@ -26,7 +28,7 @@ export class StartPagePO implements WorkbenchViewPagePO { public readonly locator: Locator; - constructor(private _appPO: AppPO, locateBy?: {viewId?: string; cssClass?: string}) { + constructor(private _appPO: AppPO, locateBy?: {viewId?: ViewId; cssClass?: string}) { if (locateBy?.viewId || locateBy?.cssClass) { this._view = this._appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); this.locator = this._view.locator.locator('app-start-page'); @@ -47,14 +49,24 @@ export class StartPagePO implements WorkbenchViewPagePO { } } + /** + * Returns the part in which this page is displayed. + */ + public getPartId(): Promise { + return this.locator.getAttribute('data-partid'); + } + /** * Clicks the workbench view tile with specified CSS class set. */ public async openWorkbenchView(cssClass: string): Promise { const viewId = await this.view.getViewId(); + const navigationId = await this._appPO.getCurrentNavigationId(); await this._tabbar.selectTab('e2e-workbench-views'); await this._tabbarLocator.locator(`.e2e-workbench-view-tiles a.${cssClass}`).click(); await this._appPO.view({viewId, cssClass}).waitUntilAttached(); + // Wait until completed navigation. + await waitForCondition(async () => (await this._appPO.getCurrentNavigationId()) !== navigationId); } /** @@ -62,12 +74,15 @@ export class StartPagePO implements WorkbenchViewPagePO { */ public async openMicrofrontendView(cssClass: string, app: string): Promise { const viewId = await this.view.getViewId(); + const navigationId = await this._appPO.getCurrentNavigationId(); await this._tabbar.selectTab('e2e-microfrontend-views'); await this._tabbarLocator.locator(`.e2e-microfrontend-view-tiles a.${cssClass}.workbench-client-testing-${app}`).click(); await this._appPO.view({viewId, cssClass}).waitUntilAttached(); // Wait for microfrontend to be loaded. const frameLocator = new SciRouterOutletPO(this._appPO, {name: viewId}).frameLocator; await frameLocator.locator('app-root').waitFor({state: 'visible'}); + // Wait until completed navigation. + await waitForCondition(async () => (await this._appPO.getCurrentNavigationId()) !== navigationId); } /** diff --git a/projects/scion/e2e-testing/src/view-tab.po.ts b/projects/scion/e2e-testing/src/view-tab.po.ts index 7a6b957a7..fdeeab33b 100644 --- a/projects/scion/e2e-testing/src/view-tab.po.ts +++ b/projects/scion/e2e-testing/src/view-tab.po.ts @@ -14,6 +14,7 @@ import {PartPO} from './part.po'; import {ViewTabContextMenuPO} from './view-tab-context-menu.po'; import {ViewMoveDialogTestPagePO} from './workbench/page-object/test-pages/view-move-dialog-test-page.po'; import {AppPO} from './app.po'; +import {ViewId} from '@scion/workbench'; /** * Handle for interacting with a workbench view tab. @@ -44,8 +45,8 @@ export class ViewTabPO { this.part = part; } - public async getViewId(): Promise { - return (await this.locator.getAttribute('data-viewid'))!; + public async getViewId(): Promise { + return (await this.locator.getAttribute('data-viewid'))! as ViewId; } public async click(): Promise { @@ -158,12 +159,12 @@ export class ViewTabPO { * Drags this view tab to the specified region of specified part or grid. * * @param target - Specifies the part or grid where to drop this view tab. - * @property partId - Specifies the part where to drag this tab. - * @property grid - Specifies the grid where to drag this tab. - * @property region - Specifies the region where to drop this tab in the specified target. + * @param target.partId - Specifies the part where to drag this tab. + * @param target.grid - Specifies the grid where to drag this tab. + * @param target.region - Specifies the region where to drop this tab in the specified target. * @param options - Controls the drag operation. - * @property steps - Sets the number of intermediate events to be emitted while dragging; defaults to `2`. - * @property performDrop - Controls whether to perform the drop; defaults to `true`. + * @param options.steps - Sets the number of intermediate events to be emitted while dragging; defaults to `2`. + * @param options.performDrop - Controls whether to perform the drop; defaults to `true`. */ public async dragTo(target: {partId: string; region: 'north' | 'east' | 'south' | 'west' | 'center'}, options?: {steps?: number; performDrop?: boolean}): Promise; public async dragTo(target: {grid: 'workbench' | 'mainArea'; region: 'north' | 'east' | 'south' | 'west' | 'center'}, options?: {steps?: number; performDrop?: boolean}): Promise; diff --git a/projects/scion/e2e-testing/src/view.po.ts b/projects/scion/e2e-testing/src/view.po.ts index 16bd272bb..0b8e4fabb 100644 --- a/projects/scion/e2e-testing/src/view.po.ts +++ b/projects/scion/e2e-testing/src/view.po.ts @@ -12,6 +12,7 @@ import {DomRect, fromRect, getCssClasses} from './helper/testing.util'; import {Locator} from '@playwright/test'; import {PartPO} from './part.po'; import {ViewTabPO} from './view-tab.po'; +import {ViewId} from '@scion/workbench'; import {ViewInfo, ViewInfoDialogPO} from './workbench/page-object/view-info-dialog.po'; import {AppPO} from './app.po'; @@ -29,7 +30,7 @@ export class ViewPO { this.tab = tab; } - public async getViewId(): Promise { + public async getViewId(): Promise { return this.tab.getViewId(); } diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/dialog-opener-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/dialog-opener-page.po.ts index 73656e489..858303011 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/dialog-opener-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/dialog-opener-page.po.ts @@ -17,7 +17,7 @@ import {SciCheckboxPO} from '../../@scion/components.internal/checkbox.po'; import {Locator} from '@playwright/test'; import {SciRouterOutletPO} from './sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; -import {WorkbenchDialogOptions} from '@scion/workbench-client'; +import {ViewId, WorkbenchDialogOptions} from '@scion/workbench-client'; /** * Page object to interact with {@link DialogOpenerPageComponent}. @@ -30,7 +30,7 @@ export class DialogOpenerPagePO implements MicrofrontendViewPagePO { public readonly returnValue: Locator; public readonly error: Locator; - constructor(private _appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(private _appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = this._appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.outlet = new SciRouterOutletPO(this._appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.outlet.frameLocator.locator('app-dialog-opener-page'); diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/message-box-opener-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/message-box-opener-page.po.ts index 901f52860..ed7052d63 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/message-box-opener-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/message-box-opener-page.po.ts @@ -17,6 +17,7 @@ import {Locator} from '@playwright/test'; import {SciRouterOutletPO} from './sci-router-outlet.po'; import {ViewPO} from '../../view.po'; import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; +import {ViewId} from '@scion/workbench-client'; /** * Page object to interact with {@link MessageBoxOpenerPageComponent}. @@ -28,7 +29,7 @@ export class MessageBoxOpenerPagePO implements MicrofrontendViewPagePO { public readonly outlet: SciRouterOutletPO; public readonly closeAction: Locator; - constructor(private _appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(private _appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = this._appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.outlet = new SciRouterOutletPO(this._appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.outlet.frameLocator.locator('app-message-box-opener-page'); diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/messaging-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/messaging-page.po.ts index 577e5eff6..e967d99a6 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/messaging-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/messaging-page.po.ts @@ -15,6 +15,7 @@ import {SciTabbarPO} from '../../@scion/components.internal/tabbar.po'; import {SciRouterOutletPO} from './sci-router-outlet.po'; import {rejectWhenAttached, waitUntilAttached} from '../../helper/testing.util'; import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; +import {ViewId} from '@scion/workbench-client'; /** * Page object to interact with {@link MessagingPageComponent}. @@ -27,7 +28,7 @@ export class MessagingPagePO implements MicrofrontendViewPagePO { public readonly view: ViewPO; public readonly outlet: SciRouterOutletPO; - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.outlet = new SciRouterOutletPO(appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.outlet.frameLocator.locator('app-messaging-page'); diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/notification-opener-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/notification-opener-page.po.ts index 32c547476..1d9e1af2e 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/notification-opener-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/notification-opener-page.po.ts @@ -16,6 +16,7 @@ import {Locator} from '@playwright/test'; import {SciRouterOutletPO} from './sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; import {ViewPO} from '../../view.po'; +import {ViewId} from '@scion/workbench-client'; /** * Page object to interact with {@link NotificationOpenerPageComponent}. @@ -27,7 +28,7 @@ export class NotificationOpenerPagePO implements MicrofrontendViewPagePO { public readonly view: ViewPO; public readonly error: Locator; - constructor(private _appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(private _appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = this._appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.outlet = new SciRouterOutletPO(this._appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.outlet.frameLocator.locator('app-notification-opener-page'); diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/popup-opener-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/popup-opener-page.po.ts index 8374d1fab..9abff506c 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/popup-opener-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/popup-opener-page.po.ts @@ -19,6 +19,7 @@ import {SciCheckboxPO} from '../../@scion/components.internal/checkbox.po'; import {Locator} from '@playwright/test'; import {SciRouterOutletPO} from './sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; +import {ViewId} from '@scion/workbench-client'; /** * Page object to interact with {@link PopupOpenerPageComponent}. @@ -31,7 +32,7 @@ export class PopupOpenerPagePO implements MicrofrontendViewPagePO { public readonly returnValue: Locator; public readonly error: Locator; - constructor(private _appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(private _appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = this._appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.outlet = new SciRouterOutletPO(this._appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.outlet.frameLocator.locator('app-popup-opener-page'); @@ -99,7 +100,7 @@ export class PopupOpenerPagePO implements MicrofrontendViewPagePO { } } - public async enterContextualViewId(viewId: string | '' | ''): Promise { + public async enterContextualViewId(viewId: ViewId | '' | ''): Promise { await this.locator.locator('input.e2e-contextual-view-id').fill(viewId); } diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-capability-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-capability-page.po.ts index 7d787b71d..a9d1f6131 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-capability-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-capability-page.po.ts @@ -13,7 +13,7 @@ import {AppPO} from '../../app.po'; import {SciKeyValueFieldPO} from '../../@scion/components.internal/key-value-field.po'; import {SciCheckboxPO} from '../../@scion/components.internal/checkbox.po'; import {Locator} from '@playwright/test'; -import {WorkbenchDialogCapability as _WorkbenchDialogCapability, WorkbenchPopupCapability as _WorkbenchPopupCapability, WorkbenchViewCapability as _WorkbenchViewCapability} from '@scion/workbench-client'; +import {ViewId, WorkbenchDialogCapability as _WorkbenchDialogCapability, WorkbenchPopupCapability as _WorkbenchPopupCapability, WorkbenchViewCapability as _WorkbenchViewCapability} from '@scion/workbench-client'; import {Capability} from '@scion/microfrontend-platform'; import {SciRouterOutletPO} from './sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; @@ -39,7 +39,7 @@ export class RegisterWorkbenchCapabilityPagePO implements MicrofrontendViewPageP public readonly outlet: SciRouterOutletPO; public readonly view: ViewPO; - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.outlet = new SciRouterOutletPO(appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.outlet.frameLocator.locator('app-register-workbench-capability-page'); diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-intention-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-intention-page.po.ts index 6a9d231b7..a738fc7e8 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-intention-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/register-workbench-intention-page.po.ts @@ -16,6 +16,7 @@ import {rejectWhenAttached} from '../../helper/testing.util'; import {SciRouterOutletPO} from './sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; import {ViewPO} from '../../view.po'; +import {ViewId} from '@scion/workbench-client'; /** * Page object to interact with {@link RegisterWorkbenchIntentionPageComponent}. @@ -26,7 +27,7 @@ export class RegisterWorkbenchIntentionPagePO implements MicrofrontendViewPagePO public readonly outlet: SciRouterOutletPO; public readonly view: ViewPO; - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.outlet = new SciRouterOutletPO(appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.outlet.frameLocator.locator('app-register-workbench-intention-page'); diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/router-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/router-page.po.ts index 8c149679e..1e0a23900 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/router-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/router-page.po.ts @@ -17,6 +17,7 @@ import {Locator} from '@playwright/test'; import {coerceArray, rejectWhenAttached, waitUntilStable} from '../../helper/testing.util'; import {SciRouterOutletPO} from './sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; +import {ViewId} from '@scion/workbench-client'; /** * Page object to interact with {@link RouterPageComponent} of workbench-client testing app. @@ -27,7 +28,7 @@ export class RouterPagePO implements MicrofrontendViewPagePO { public readonly view: ViewPO; public readonly outlet: SciRouterOutletPO; - constructor(private _appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(private _appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = this._appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.outlet = new SciRouterOutletPO(this._appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.outlet.frameLocator.locator('app-router-page'); @@ -45,12 +46,12 @@ export class RouterPagePO implements MicrofrontendViewPagePO { await keyValueField.addEntries(params); } - public async enterTarget(target?: string | 'blank' | 'auto'): Promise { + public async enterTarget(target: string | 'blank' | 'auto' | undefined): Promise { await this.locator.locator('input.e2e-target').fill(target ?? ''); } - public async enterInsertionIndex(insertionIndex: number | 'start' | 'end' | undefined): Promise { - await this.locator.locator('input.e2e-insertion-index').fill(`${insertionIndex}`); + public async enterPosition(position: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'): Promise { + await this.locator.locator('input.e2e-position').fill(`${position}`); } public async checkActivate(check: boolean): Promise { @@ -62,7 +63,7 @@ export class RouterPagePO implements MicrofrontendViewPagePO { } public async enterCssClass(cssClass: string | string[]): Promise { - await this.locator.locator('input.e2e-css-class').fill(coerceArray(cssClass).join(' ')); + await this.locator.locator('input.e2e-class').fill(coerceArray(cssClass).join(' ')); } /** diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/angular-zone-test-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/angular-zone-test-page.po.ts index 93e3ccc16..1f5bcfc33 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/angular-zone-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/angular-zone-test-page.po.ts @@ -16,6 +16,7 @@ import {MicrofrontendNavigator} from '../../microfrontend-navigator'; import {SciRouterOutletPO} from '../sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../../workbench/page-object/workbench-view-page.po'; import {ViewPO} from '../../../view.po'; +import {ViewId} from '@scion/workbench-client'; export class AngularZoneTestPagePO implements MicrofrontendViewPagePO { @@ -29,7 +30,7 @@ export class AngularZoneTestPagePO implements MicrofrontendViewPagePO { activePanel: PanelPO; }; - constructor(appPO: AppPO, viewId: string) { + constructor(appPO: AppPO, viewId: ViewId) { this.view = appPO.view({viewId}); this.outlet = new SciRouterOutletPO(appPO, {name: viewId}); this.locator = this.outlet.frameLocator.locator('app-angular-zone-test-page'); diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/bulk-navigation-test-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/bulk-navigation-test-page.po.ts index 25fa84dba..baa076746 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/bulk-navigation-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/bulk-navigation-test-page.po.ts @@ -15,6 +15,7 @@ import {MicrofrontendNavigator} from '../../microfrontend-navigator'; import {SciRouterOutletPO} from '../sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../../workbench/page-object/workbench-view-page.po'; import {ViewPO} from '../../../view.po'; +import {ViewId} from '@scion/workbench-client'; export class BulkNavigationTestPagePO implements MicrofrontendViewPagePO { @@ -22,7 +23,7 @@ export class BulkNavigationTestPagePO implements MicrofrontendViewPagePO { public readonly view: ViewPO; public readonly outlet: SciRouterOutletPO; - constructor(private _appPO: AppPO, viewId: string) { + constructor(private _appPO: AppPO, viewId: ViewId) { this.view = this._appPO.view({viewId}); this.outlet = new SciRouterOutletPO(this._appPO, {name: viewId}); this.locator = this.outlet.frameLocator.locator('app-bulk-navigation-test-page'); @@ -33,7 +34,7 @@ export class BulkNavigationTestPagePO implements MicrofrontendViewPagePO { } public async enterCssClass(cssClass: string): Promise { - await this.locator.locator('input.e2e-css-class').fill(cssClass); + await this.locator.locator('input.e2e-class').fill(cssClass); } /** diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/microfrontend-view-test-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/microfrontend-view-test-page.po.ts index d15f4d5ff..875450f30 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/microfrontend-view-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/microfrontend-view-test-page.po.ts @@ -13,6 +13,7 @@ import {Locator} from '@playwright/test'; import {ViewPO} from '../../../view.po'; import {SciRouterOutletPO} from '../sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../../workbench/page-object/workbench-view-page.po'; +import {ViewId} from '@scion/workbench-client'; export class MicrofrontendViewTestPagePO implements MicrofrontendViewPagePO { @@ -20,7 +21,7 @@ export class MicrofrontendViewTestPagePO implements MicrofrontendViewPagePO { public readonly view: ViewPO; public readonly outlet: SciRouterOutletPO; - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); this.outlet = new SciRouterOutletPO(appPO, {name: locateBy?.viewId, cssClass: locateBy?.cssClass}); this.locator = this.outlet.frameLocator.locator('app-microfrontend-test-page'); diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/view-properties-test-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/view-properties-test-page.po.ts index 7a1be173c..bc4b7f2c7 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/view-properties-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/view-properties-test-page.po.ts @@ -13,6 +13,7 @@ import {Locator} from '@playwright/test'; import {SciRouterOutletPO} from '../sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../../workbench/page-object/workbench-view-page.po'; import {ViewPO} from '../../../view.po'; +import {ViewId} from '@scion/workbench-client'; export class ViewPropertiesTestPagePO implements MicrofrontendViewPagePO { @@ -20,7 +21,7 @@ export class ViewPropertiesTestPagePO implements MicrofrontendViewPagePO { public readonly view: ViewPO; public readonly outlet: SciRouterOutletPO; - constructor(appPO: AppPO, viewId: string) { + constructor(appPO: AppPO, viewId: ViewId) { this.view = appPO.view({viewId}); this.outlet = new SciRouterOutletPO(appPO, {name: viewId}); this.locator = this.outlet.frameLocator.locator('app-view-properties-test-page'); diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/workbench-theme-test-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/workbench-theme-test-page.po.ts index 391f0b224..c14b5c492 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/workbench-theme-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/test-pages/workbench-theme-test-page.po.ts @@ -14,6 +14,7 @@ import {MicrofrontendNavigator} from '../../microfrontend-navigator'; import {SciRouterOutletPO} from '../sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../../workbench/page-object/workbench-view-page.po'; import {ViewPO} from '../../../view.po'; +import {ViewId} from '@scion/workbench-client'; export class WorkbenchThemeTestPagePO implements MicrofrontendViewPagePO { @@ -24,7 +25,7 @@ export class WorkbenchThemeTestPagePO implements MicrofrontendViewPagePO { public readonly theme: Locator; public readonly colorScheme: Locator; - constructor(appPO: AppPO, viewId: string) { + constructor(appPO: AppPO, viewId: ViewId) { this.view = appPO.view({viewId}); this.outlet = new SciRouterOutletPO(appPO, {name: viewId}); this.locator = this.outlet.frameLocator.locator('app-workbench-theme-test-page'); diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/unregister-workbench-capability-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/unregister-workbench-capability-page.po.ts index 6409fc76b..8caa7f6c7 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/unregister-workbench-capability-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/unregister-workbench-capability-page.po.ts @@ -14,6 +14,7 @@ import {rejectWhenAttached, waitUntilAttached} from '../../helper/testing.util'; import {SciRouterOutletPO} from './sci-router-outlet.po'; import {MicrofrontendViewPagePO} from '../../workbench/page-object/workbench-view-page.po'; import {ViewPO} from '../../view.po'; +import {ViewId} from '@scion/workbench-client'; /** * Page object to interact with {@link UnregisterWorkbenchCapabilityPageComponent}. @@ -24,7 +25,7 @@ export class UnregisterWorkbenchCapabilityPagePO implements MicrofrontendViewPag public readonly view: ViewPO; public readonly outlet: SciRouterOutletPO; - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.outlet = new SciRouterOutletPO(appPO, {name: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.outlet.frameLocator.locator('app-unregister-workbench-capability-page'); diff --git a/projects/scion/e2e-testing/src/workbench-client/page-object/view-page.po.ts b/projects/scion/e2e-testing/src/workbench-client/page-object/view-page.po.ts index 13271f064..03ea6a798 100644 --- a/projects/scion/e2e-testing/src/workbench-client/page-object/view-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench-client/page-object/view-page.po.ts @@ -12,7 +12,7 @@ import {DomRect, fromRect} from '../../helper/testing.util'; import {AppPO} from '../../app.po'; import {ViewPO} from '../../view.po'; import {Params} from '@angular/router'; -import {WorkbenchViewCapability} from '@scion/workbench-client'; +import {ViewId, WorkbenchViewCapability} from '@scion/workbench-client'; import {SciAccordionPO} from '../../@scion/components.internal/accordion.po'; import {SciKeyValuePO} from '../../@scion/components.internal/key-value.po'; import {SciCheckboxPO} from '../../@scion/components.internal/checkbox.po'; @@ -32,7 +32,7 @@ export class ViewPagePO implements MicrofrontendViewPagePO { public readonly outlet: SciRouterOutletPO; public readonly path: Locator; - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); this.outlet = new SciRouterOutletPO(appPO, {name: locateBy?.viewId, cssClass: locateBy?.cssClass}); this.locator = this.outlet.frameLocator.locator('app-view-page'); diff --git a/projects/scion/e2e-testing/src/workbench-client/router-params.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/router-params.e2e-spec.ts index f07dea315..dbdd6c400 100644 --- a/projects/scion/e2e-testing/src/workbench-client/router-params.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/router-params.e2e-spec.ts @@ -580,9 +580,10 @@ test.describe('Workbench Router', () => { await routerPage.enterQualifier({component: 'view', app: 'app1'}); await routerPage.enterParams({initialTitle: 'TITLE', transientParam: 'TRANSIENT PARAM'}); await routerPage.enterTarget('blank'); + await routerPage.enterCssClass('testee'); await routerPage.clickNavigate(); - const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'e2e-test-view'}); + const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); // expect transient param to be contained in view params await expect.poll(() => testeeViewPage.getViewParams()).toMatchObject({ diff --git a/projects/scion/e2e-testing/src/workbench-client/router.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/router.e2e-spec.ts index 436d391ea..5fa66df0a 100644 --- a/projects/scion/e2e-testing/src/workbench-client/router.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/router.e2e-spec.ts @@ -852,7 +852,6 @@ test.describe('Workbench Router', () => { properties: { path: 'test-view', title: 'testee', - cssClass: 'testee', }, }); @@ -860,6 +859,7 @@ test.describe('Workbench Router', () => { const routerPage = await microfrontendNavigator.openInNewTab(RouterPagePO, 'app1'); await routerPage.enterQualifier({component: 'testee'}); await routerPage.enterTarget('blank'); + await routerPage.enterCssClass('testee'); await routerPage.clickNavigate(); // expect the view to be present @@ -1279,7 +1279,7 @@ test.describe('Workbench Router', () => { await routerPage.checkClose(true); // expect closing to be rejected - await expect(routerPage.clickNavigate()).rejects.toThrow(/\[WorkbenchRouterError]\[IllegalArgumentError]/); + await expect(routerPage.clickNavigate()).rejects.toThrow(/\[NavigateError]/); await expect(appPO.views()).toHaveCount(2); }); diff --git a/projects/scion/e2e-testing/src/workbench-client/view-css-class.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/view-css-class.e2e-spec.ts index 6a1f90fef..6fed2b08c 100644 --- a/projects/scion/e2e-testing/src/workbench-client/view-css-class.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/view-css-class.e2e-spec.ts @@ -12,7 +12,6 @@ import {test} from '../fixtures'; import {ViewPagePO} from './page-object/view-page.po'; import {expect} from '@playwright/test'; import {RouterPagePO} from './page-object/router-page.po'; -import {LayoutPagePO} from '../workbench/page-object/layout-page.po'; test.describe('Workbench View CSS Class', () => { @@ -43,9 +42,10 @@ test.describe('Workbench View CSS Class', () => { }); await microfrontendNavigator.registerIntention('app1', {type: 'view', qualifier: {component: 'testee-2'}}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('right', {align: 'right'}); - await layoutPage.addView('view.100', {partId: 'right', activateView: true}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {align: 'right'}) + .addView('view.100', {partId: 'right', activateView: true, cssClass: 'testee-layout'}), + ); const viewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); @@ -59,6 +59,10 @@ test.describe('Workbench View CSS Class', () => { await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-navigation-1'); await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-navigation-1'); await expect.poll(() => viewPage.outlet.getCssClasses()).toContain('testee-navigation-1'); + // Expect CSS classes of the view to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.outlet.getCssClasses()).toContain('testee-layout'); // Expect CSS classes of the capability to be set. await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-capability-1'); await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-capability-1'); @@ -81,6 +85,10 @@ test.describe('Workbench View CSS Class', () => { await expect.poll(() => viewPage.view.getCssClasses()).not.toContain('testee-capability-1'); await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-capability-1'); await expect.poll(() => viewPage.outlet.getCssClasses()).not.toContain('testee-capability-1'); + // Expect CSS classes of the view to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.outlet.getCssClasses()).toContain('testee-layout'); // Expect CSS classes of the capability to be set. await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-capability-2'); await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-capability-2'); @@ -101,6 +109,10 @@ test.describe('Workbench View CSS Class', () => { await expect.poll(() => viewPage.view.getCssClasses()).not.toContain('testee-capability-2'); await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-capability-2'); await expect.poll(() => viewPage.outlet.getCssClasses()).not.toContain('testee-capability-2'); + // Expect CSS classes of the view to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.outlet.getCssClasses()).toContain('testee-layout'); // Expect CSS classes of the capability to be set. await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-capability-1'); await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-capability-1'); diff --git a/projects/scion/e2e-testing/src/workbench-client/view.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench-client/view.e2e-spec.ts index 53384e848..e9dbf43c8 100644 --- a/projects/scion/e2e-testing/src/workbench-client/view.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench-client/view.e2e-spec.ts @@ -191,7 +191,7 @@ test.describe('Workbench View', () => { await expect(viewPage.view.tab.closeButton).not.toBeVisible(); }); - test('should allow closing the view', async ({appPO, microfrontendNavigator}) => { + test('should close a view', async ({appPO, microfrontendNavigator}) => { await appPO.navigateTo({microfrontendSupport: true}); const viewPage = await microfrontendNavigator.openInNewTab(ViewPagePO, 'app1'); @@ -205,7 +205,7 @@ test.describe('Workbench View', () => { await expectView(viewPage).not.toBeAttached(); }); - test('should allow preventing the view from closing', async ({appPO, microfrontendNavigator}) => { + test('should prevent closing a view', async ({appPO, microfrontendNavigator}) => { await appPO.navigateTo({microfrontendSupport: true}); await microfrontendNavigator.registerIntention('app1', {type: 'messagebox'}); @@ -215,7 +215,7 @@ test.describe('Workbench View', () => { // prevent the view from closing await testeeViewPage.checkConfirmClosing(true); - // try closing the view + // try closing the view via view tab. await testeeViewPage.view.tab.close(); const messageBox = appPO.messagebox({cssClass: ['e2e-close-view', await testeeViewPage.view.getViewId()]}); await messageBox.clickActionButton('no'); @@ -223,14 +223,14 @@ test.describe('Workbench View', () => { // expect the view not to be closed await expectView(testeeViewPage).toBeActive(); - // try closing the view + // try closing the view via handle. await testeeViewPage.clickClose(); await messageBox.clickActionButton('no'); // expect the view not to be closed await expectView(testeeViewPage).toBeActive(); - // try closing the view + // close the view await testeeViewPage.view.tab.close(); await messageBox.clickActionButton('yes'); @@ -238,7 +238,7 @@ test.describe('Workbench View', () => { await expectView(testeeViewPage).not.toBeAttached(); }); - test('should only close confirmed views, leaving other views open', async ({appPO, microfrontendNavigator}) => { + test('should close confirmed views, leaving other views open', async ({appPO, microfrontendNavigator}) => { await appPO.navigateTo({microfrontendSupport: true}); await microfrontendNavigator.registerIntention('app1', {type: 'messagebox'}); @@ -253,7 +253,6 @@ test.describe('Workbench View', () => { // open test view 3 const testee3ViewPage = await microfrontendNavigator.openInNewTab(ViewPagePO, 'app1'); - await testee3ViewPage.checkConfirmClosing(true); // prevent the view from closing // open context menu of viewtab 3 const contextMenu = await testee3ViewPage.view.tab.openContextMenu(); @@ -261,36 +260,62 @@ test.describe('Workbench View', () => { // click to close all tabs await contextMenu.menuItems.closeAll.click(); - // expect all views being still open + // expect all views to be opened await expect(appPO.views()).toHaveCount(3); // confirm closing view 1 const messageBox1 = appPO.messagebox({cssClass: ['e2e-close-view', await testee1ViewPage.view.getViewId()]}); await messageBox1.clickActionButton('yes'); - // expect view 1 being closed - await expect(appPO.views()).toHaveCount(2); - // prevent closing view 2 const messageBox2 = appPO.messagebox({cssClass: ['e2e-close-view', await testee2ViewPage.view.getViewId()]}); await messageBox2.clickActionButton('no'); - // expect view 2 being still open - await expect(appPO.views()).toHaveCount(2); + // expect view 1 and view 3 to be closed. + await expectView(testee1ViewPage).not.toBeAttached(); + await expectView(testee2ViewPage).toBeActive(); + await expectView(testee3ViewPage).not.toBeAttached(); + }); - // confirm closing view 3 - const messageBox3 = appPO.messagebox({cssClass: ['e2e-close-view', await testee3ViewPage.view.getViewId()]}); - await messageBox3.clickActionButton('yes'); + test('should close view and log error if `CanClose` guard throws an error', async ({appPO, microfrontendNavigator, consoleLogs}) => { + await appPO.navigateTo({microfrontendSupport: true}); - // expect view 3 to be closed - await expect(appPO.views()).toHaveCount(1); - await expectView(testee3ViewPage).not.toBeAttached(); + await microfrontendNavigator.registerIntention('app1', {type: 'messagebox'}); - // expect view 2 not to be closed and active - await expectView(testee2ViewPage).toBeActive(); + // open test view 1 + const testee1ViewPage = await microfrontendNavigator.openInNewTab(ViewPagePO, 'app1'); + await testee1ViewPage.checkConfirmClosing(true); // prevent the view from closing - // expect view 1 to be closed + // open test view 2 + const testee2ViewPage = await microfrontendNavigator.openInNewTab(ViewPagePO, 'app1'); + await testee2ViewPage.checkConfirmClosing(true); // prevent the view from closing + + // open test view 3 + const testee3ViewPage = await microfrontendNavigator.openInNewTab(ViewPagePO, 'app1'); + + // open context menu of viewtab 3 + const contextMenu = await testee3ViewPage.view.tab.openContextMenu(); + + // click to close all tabs + await contextMenu.menuItems.closeAll.click(); + + // expect all views to be opened + await expect(appPO.views()).toHaveCount(3); + + // simulate view 1 to throw error + const messageBox1 = appPO.messagebox({cssClass: ['e2e-close-view', await testee1ViewPage.view.getViewId()]}); + await messageBox1.clickActionButton('error'); + + // prevent closing view 2 + const messageBox2 = appPO.messagebox({cssClass: ['e2e-close-view', await testee2ViewPage.view.getViewId()]}); + await messageBox2.clickActionButton('no'); + + // expect view 1 and view 3 to be closed. await expectView(testee1ViewPage).not.toBeAttached(); + await expectView(testee2ViewPage).toBeActive(); + await expectView(testee3ViewPage).not.toBeAttached(); + + await expect.poll(() => consoleLogs.contains({severity: 'error', message: /\[CanCloseSpecError] Error in CanLoad of view 'view\.1'\./})).toBe(true); }); test('should activate viewtab when switching between tabs', async ({appPO, microfrontendNavigator}) => { @@ -329,7 +354,7 @@ test.describe('Workbench View', () => { expect(activeViewSize).toEqual(inactiveViewSize); }); - test('should not confirm closing when switching between viewtabs', async ({appPO, microfrontendNavigator}) => { + test('should not invoke `CanClose` guard when switching between viewtabs', async ({appPO, microfrontendNavigator}) => { await appPO.navigateTo({microfrontendSupport: true}); await microfrontendNavigator.registerIntention('app1', {type: 'messagebox'}); diff --git a/projects/scion/e2e-testing/src/workbench.model.ts b/projects/scion/e2e-testing/src/workbench.model.ts index e248703f5..a5d762ddd 100644 --- a/projects/scion/e2e-testing/src/workbench.model.ts +++ b/projects/scion/e2e-testing/src/workbench.model.ts @@ -9,7 +9,7 @@ */ /** - * This files contains model classes of @scion/workbench. + * This file contains model classes of @scion/workbench. * * In Playwright tests, we cannot reference types from other modules, only interfaces, because they are erased when transpiled to JavaScript. */ diff --git a/projects/scion/e2e-testing/src/workbench/browser-history.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/browser-history.e2e-spec.ts index a4ce5338e..1095140d9 100644 --- a/projects/scion/e2e-testing/src/workbench/browser-history.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/browser-history.e2e-spec.ts @@ -10,7 +10,6 @@ import {test} from '../fixtures'; import {RouterPagePO} from './page-object/router-page.po'; -import {LayoutPagePO} from './page-object/layout-page.po'; import {StandaloneViewTestPagePO} from './page-object/test-pages/standalone-view-test-page.po'; import {NonStandaloneViewTestPagePO} from './page-object/test-pages/non-standalone-view-test-page.po'; import {MAIN_AREA} from '../workbench.model'; @@ -23,25 +22,26 @@ test.describe('Browser History', () => { await appPO.navigateTo({microfrontendSupport: false}); // Add part to the workbench grid - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}); + await workbenchNavigator.modifyLayout(layout => layout.addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .25})); // Add view-1 to the left part const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.101'); - await routerPage.enterBlankPartId('left'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.101', + partId: 'left', + cssClass: 'testee' + }); // Expect view-1 to be active const testee1ViewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); await expectView(testee1ViewPage).toBeActive(); // Add view-2 to the left part - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.102'); - await routerPage.enterBlankPartId('left'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.102', + partId: 'left', + cssClass: 'testee' + }); // Expect view-2 to be active const testee2ViewPage = new ViewPagePO(appPO, {viewId: 'view.102'}); @@ -112,18 +112,20 @@ test.describe('Browser History', () => { // Add view-1 const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.101'); - await routerPage.checkActivate(false); - await routerPage.enterInsertionIndex('end'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.101', + activate: false, + position: 'end', + cssClass: 'testee' + }); // Add view-2 - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.102'); - await routerPage.checkActivate(false); - await routerPage.enterInsertionIndex('end'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.102', + activate: false, + position: 'end', + cssClass: 'testee' + }); const testee1ViewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); const testee2ViewPage = new ViewPagePO(appPO, {viewId: 'view.102'}); @@ -191,10 +193,10 @@ test.describe('Browser History', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/standalone-view-test-page/component'); - await routerPage.enterTarget(await routerPage.view.getViewId()); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/standalone-view-test-page/component'], { + target: await routerPage.view.getViewId(), + cssClass: 'testee' + }); const standaloneViewTestPage = new StandaloneViewTestPagePO(appPO, {cssClass: 'testee'}); await expectView(routerPage).not.toBeAttached(); @@ -213,10 +215,10 @@ test.describe('Browser History', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/standalone-view-test-page/load-component'); - await routerPage.enterTarget(await routerPage.view.getViewId()); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/standalone-view-test-page/load-component'], { + target: await routerPage.view.getViewId(), + cssClass: 'testee' + }); const standaloneViewTestPage = new StandaloneViewTestPagePO(appPO, {cssClass: 'testee'}); await expectView(routerPage).not.toBeAttached(); @@ -235,10 +237,10 @@ test.describe('Browser History', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/standalone-view-test-page/load-children/module'); - await routerPage.enterTarget(await routerPage.view.getViewId()); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/standalone-view-test-page/load-children/module'], { + target: await routerPage.view.getViewId(), + cssClass: 'testee' + }); const standaloneViewTestPage = new StandaloneViewTestPagePO(appPO, {cssClass: 'testee'}); await expectView(routerPage).not.toBeAttached(); @@ -257,10 +259,10 @@ test.describe('Browser History', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/standalone-view-test-page/load-children/routes'); - await routerPage.enterTarget(await routerPage.view.getViewId()); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/standalone-view-test-page/load-children/routes'], { + target: await routerPage.view.getViewId(), + cssClass: 'testee' + }); const standaloneViewTestPage = new StandaloneViewTestPagePO(appPO, {cssClass: 'testee'}); await expectView(routerPage).not.toBeAttached(); @@ -279,10 +281,10 @@ test.describe('Browser History', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/standalone-view-test-page/children'); - await routerPage.enterTarget(await routerPage.view.getViewId()); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/standalone-view-test-page/children'], { + target: await routerPage.view.getViewId(), + cssClass: 'testee' + }); const standaloneViewTestPage = new StandaloneViewTestPagePO(appPO, {cssClass: 'testee'}); await expectView(routerPage).not.toBeAttached(); @@ -304,10 +306,10 @@ test.describe('Browser History', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/non-standalone-view-test-page/component'); - await routerPage.enterTarget(await routerPage.view.getViewId()); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/non-standalone-view-test-page/component'], { + target: await routerPage.view.getViewId(), + cssClass: 'testee' + }); const nonStandaloneViewTestPage = new NonStandaloneViewTestPagePO(appPO, {cssClass: 'testee'}); await expectView(routerPage).not.toBeAttached(); @@ -326,10 +328,10 @@ test.describe('Browser History', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/non-standalone-view-test-page/load-children/module'); - await routerPage.enterTarget(await routerPage.view.getViewId()); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/non-standalone-view-test-page/load-children/module'], { + target: await routerPage.view.getViewId(), + cssClass: 'testee', + }); const nonStandaloneViewTestPage = new NonStandaloneViewTestPagePO(appPO, {cssClass: 'testee'}); await expectView(routerPage).not.toBeAttached(); @@ -348,10 +350,10 @@ test.describe('Browser History', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/non-standalone-view-test-page/children'); - await routerPage.enterTarget(await routerPage.view.getViewId()); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/non-standalone-view-test-page/children'], { + target: await routerPage.view.getViewId(), + cssClass: 'testee' + }); const nonStandaloneViewTestPage = new NonStandaloneViewTestPagePO(appPO, {cssClass: 'testee'}); await expectView(routerPage).not.toBeAttached(); diff --git a/projects/scion/e2e-testing/src/workbench/browser-reload.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/browser-reload.e2e-spec.ts index 4dd5d2250..01a8d7f76 100644 --- a/projects/scion/e2e-testing/src/workbench/browser-reload.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/browser-reload.e2e-spec.ts @@ -22,9 +22,9 @@ test.describe('Browser Reload', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/standalone-view-test-page/component'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/standalone-view-test-page/component'], { + target: 'view.101', + }); const standaloneViewTestPage = new StandaloneViewTestPagePO(appPO, {viewId: 'view.101'}); await expectView(standaloneViewTestPage).toBeActive(); @@ -37,9 +37,9 @@ test.describe('Browser Reload', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/standalone-view-test-page/load-component'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/standalone-view-test-page/load-component'], { + target: 'view.101', + }); const standaloneViewTestPage = new StandaloneViewTestPagePO(appPO, {viewId: 'view.101'}); await expectView(standaloneViewTestPage).toBeActive(); @@ -52,9 +52,9 @@ test.describe('Browser Reload', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/standalone-view-test-page/load-children/module'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/standalone-view-test-page/load-children/module'], { + target: 'view.101', + }); const standaloneViewTestPage = new StandaloneViewTestPagePO(appPO, {viewId: 'view.101'}); await expectView(standaloneViewTestPage).toBeActive(); @@ -67,9 +67,9 @@ test.describe('Browser Reload', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/standalone-view-test-page/load-children/routes'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/standalone-view-test-page/load-children/routes'], { + target: 'view.101', + }); const standaloneViewTestPage = new StandaloneViewTestPagePO(appPO, {viewId: 'view.101'}); await expectView(standaloneViewTestPage).toBeActive(); @@ -82,9 +82,9 @@ test.describe('Browser Reload', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/standalone-view-test-page/children'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/standalone-view-test-page/children'], { + target: 'view.101', + }); const standaloneViewTestPage = new StandaloneViewTestPagePO(appPO, {viewId: 'view.101'}); await expectView(standaloneViewTestPage).toBeActive(); @@ -100,9 +100,9 @@ test.describe('Browser Reload', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/non-standalone-view-test-page/component'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/non-standalone-view-test-page/component'], { + target: 'view.101', + }); const nonStandaloneViewTestPage = new NonStandaloneViewTestPagePO(appPO, {viewId: 'view.101'}); await expectView(nonStandaloneViewTestPage).toBeActive(); @@ -115,9 +115,9 @@ test.describe('Browser Reload', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/non-standalone-view-test-page/load-children/module'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/non-standalone-view-test-page/load-children/module'], { + target: 'view.101', + }); const nonStandaloneViewTestPage = new NonStandaloneViewTestPagePO(appPO, {viewId: 'view.101'}); await expectView(nonStandaloneViewTestPage).toBeActive(); @@ -130,9 +130,9 @@ test.describe('Browser Reload', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/non-standalone-view-test-page/children'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/non-standalone-view-test-page/children'], { + target: 'view.101', + }); const nonStandaloneViewTestPage = new NonStandaloneViewTestPagePO(appPO, {viewId: 'view.101'}); await expectView(nonStandaloneViewTestPage).toBeActive(); diff --git a/projects/scion/e2e-testing/src/workbench/dialog.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/dialog.e2e-spec.ts index c19fb973c..27231826f 100644 --- a/projects/scion/e2e-testing/src/workbench/dialog.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/dialog.e2e-spec.ts @@ -44,7 +44,7 @@ test.describe('Workbench Dialog', () => { const dialogOpenerPage = await workbenchNavigator.openInNewTab(DialogOpenerPagePO); // Expect to error when opening the dialog. - await expect(dialogOpenerPage.open('dialog-page', {modality: 'view', context: {viewId: 'non-existent'}})).rejects.toThrow('[NullViewError] View \'non-existent\' not found.'); + await expect(dialogOpenerPage.open('dialog-page', {modality: 'view', context: {viewId: 'view.100'}})).rejects.toThrow('[NullViewError] View \'view.100\' not found.'); // Expect no error to be logged to the console. await expect.poll(() => consoleLogs.get({severity: 'error'})).toEqual([]); @@ -2108,10 +2108,10 @@ test.describe('Workbench Dialog', () => { await dialogOpenerPage.view.tab.dragTo({partId: await dialogOpenerPage.view.part.getPartId(), region: 'east'}); // Navigate to FocusTestPageComponent - await routerPage.enterPath('/test-pages/focus-test-page'); - await routerPage.enterTarget(await routerPage.view.getViewId()); - await routerPage.enterCssClass('focus-page'); - await routerPage.clickNavigate(); + await routerPage.navigate(['/test-pages/focus-test-page'], { + target: await routerPage.view.getViewId(), + cssClass: 'focus-page' + }); // Open application-modal dialog. await dialogOpenerPage.open('dialog-page', {modality: 'application', cssClass: 'testee'}); @@ -2139,10 +2139,10 @@ test.describe('Workbench Dialog', () => { await dialogOpenerPage.view.tab.dragTo({partId: await dialogOpenerPage.view.part.getPartId(), region: 'east'}); // Navigate to FocusTestPageComponent - await routerPage.enterPath('/test-pages/focus-test-page'); - await routerPage.enterTarget(await routerPage.view.getViewId()); - await routerPage.enterCssClass('focus-page'); - await routerPage.clickNavigate(); + await routerPage.navigate(['/test-pages/focus-test-page'], { + target: await routerPage.view.getViewId(), + cssClass: 'focus-page' + }); // Open application-modal dialog. await dialogOpenerPage.open('dialog-page', {modality: 'application', cssClass: 'testee'}); diff --git a/projects/scion/e2e-testing/src/workbench/maximize-main-area.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/maximize-main-area.e2e-spec.ts index f2d52bcb4..dd12cfc58 100644 --- a/projects/scion/e2e-testing/src/workbench/maximize-main-area.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/maximize-main-area.e2e-spec.ts @@ -9,8 +9,6 @@ */ import {test} from '../fixtures'; -import {PerspectivePagePO} from './page-object/perspective-page.po'; -import {LayoutPagePO} from './page-object/layout-page.po'; import {MAIN_AREA} from '../workbench.model'; import {RouterPagePO} from './page-object/router-page.po'; import {expect} from '@playwright/test'; @@ -21,42 +19,27 @@ test.describe('Workbench', () => { test('should allow maximizing the main area', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Register Angular routes. - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.registerRoute({outlet: 'view', path: '', component: 'view-page'}); - await layoutPage.view.tab.close(); - - // Register perspective. - const perspectivePage = await workbenchNavigator.openInNewTab(PerspectivePagePO); - await perspectivePage.registerPerspective({ - id: 'perspective', - parts: [ - {id: MAIN_AREA}, - {id: 'left', relativeTo: MAIN_AREA, align: 'left', ratio: .2}, - ], - views: [ - {id: 'view', partId: 'left', activateView: true}, - ], - }); - await perspectivePage.view.tab.close(); - - // Activate the perspective. - await appPO.switchPerspective('perspective'); + await workbenchNavigator.createPerspective(factory => factory + .addPart(MAIN_AREA) + .addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .2}) + .addView('view.100', {partId: 'left', activateView: true}) + .navigateView('view.100', ['test-view']), + ); // Open view 1 in main area. const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('view-1'); - await routerPage.checkActivate(false); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + activate: false, + cssClass: 'view-1' + }); // Open view 2 in main area. - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('view-2'); - await routerPage.checkActivate(false); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + activate: false, + cssClass: 'view-2' + }); await routerPage.view.tab.close(); // Move view 2 to the right of view 1. @@ -75,8 +58,8 @@ test.describe('Workbench', () => { ratio: .2, child1: new MPart({ id: 'left', - views: [{id: 'view'}], - activeViewId: 'view', + views: [{id: 'view.100'}], + activeViewId: 'view.100', }), child2: new MPart({id: MAIN_AREA}), }), @@ -125,8 +108,8 @@ test.describe('Workbench', () => { ratio: .2, child1: new MPart({ id: 'left', - views: [{id: 'view'}], - activeViewId: 'view', + views: [{id: 'view.100'}], + activeViewId: 'view.100', }), child2: new MPart({id: MAIN_AREA}), }), diff --git a/projects/scion/e2e-testing/src/workbench/move-view-to-new-window.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/move-view-to-new-window.e2e-spec.ts index 89ba7ebf5..48608623a 100644 --- a/projects/scion/e2e-testing/src/workbench/move-view-to-new-window.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/move-view-to-new-window.e2e-spec.ts @@ -14,37 +14,31 @@ import {RouterPagePO} from './page-object/router-page.po'; import {ViewPagePO} from './page-object/view-page.po'; import {MPart, MTreeNode} from '../matcher/to-equal-workbench-layout.matcher'; import {MAIN_AREA} from '../workbench.model'; -import {PerspectivePagePO} from './page-object/perspective-page.po'; import {WorkbenchNavigator} from './workbench-navigator'; -import {LayoutPagePO} from './page-object/layout-page.po'; import {getPerspectiveId} from '../helper/testing.util'; import {expectView} from '../matcher/view-matcher'; test.describe('Workbench View', () => { - test('should allow moving a named view in the workbench grid to a new window', async ({appPO, workbenchNavigator}) => { + test('should move a path-based view in the workbench grid to a new window', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Define perspective with a view in the workbench grid. - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {align: 'left', ratio: .25}); - await layoutPage.addView('other', {partId: 'left', activateView: true}); - await layoutPage.addView('testee', {partId: 'left', activateView: true}); - await layoutPage.registerRoute({path: '', component: 'view-page', outlet: 'other'}); - await layoutPage.registerRoute({path: '', component: 'view-page', outlet: 'testee'}); + // Add two views to the peripheral area. + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left', ratio: .25}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: 'left', activateView: true}) + .navigateView('view.101', ['test-view']) + .navigateView('view.102', ['test-view']), + ); // Move test view to new window. - const newAppPO = await appPO.view({viewId: 'testee'}).tab.moveToNewWindow(); + const newAppPO = await appPO.view({viewId: 'view.101'}).tab.moveToNewWindow(); const newWindow = { appPO: newAppPO, workbenchNavigator: new WorkbenchNavigator(newAppPO), }; - // Register route for named view. - const newWindowLayoutPage = await newWindow.workbenchNavigator.openInNewTab(LayoutPagePO); - await newWindowLayoutPage.registerRoute({path: '', component: 'view-page', outlet: 'testee'}); - await newWindowLayoutPage.view.tab.close(); - // Expect test view to be moved to the new window. await expect(newAppPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { @@ -52,14 +46,14 @@ test.describe('Workbench View', () => { }, mainAreaGrid: { root: new MPart({ - views: [{id: 'testee'}], - activeViewId: 'testee', + views: [{id: 'view.1'}], + activeViewId: 'view.1', }), }, }); // Expect test view to display. - const viewPage = new ViewPagePO(newWindow.appPO, {viewId: 'testee'}); + const viewPage = new ViewPagePO(newWindow.appPO, {viewId: 'view.1'}); await expectView(viewPage).toBeActive(); // Expect test view to be removed from the origin window. @@ -68,41 +62,31 @@ test.describe('Workbench View', () => { root: new MTreeNode({ direction: 'row', ratio: .25, - child1: new MPart({id: 'left', views: [{id: 'other'}], activeViewId: 'other'}), + child1: new MPart({id: 'left', views: [{id: 'view.102'}], activeViewId: 'view.102'}), child2: new MPart({id: MAIN_AREA}), }), }, - mainAreaGrid: { - root: new MPart({ - views: [{id: await layoutPage.view.getViewId()}], - activeViewId: await layoutPage.view.getViewId(), - }), - }, }); }); - test('should allow moving an unnamed view in the workbench grid to a new window', async ({appPO, workbenchNavigator}) => { + test('should move an empty-path view in the workbench grid to a new window', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); // Define perspective with a view in the workbench grid. - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {align: 'left', ratio: .25}); - await layoutPage.addView('other', {partId: 'left', activateView: true}); - await layoutPage.registerRoute({path: '', component: 'view-page', outlet: 'other'}); - - // Open test view in workbench grid. - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterBlankPartId('left'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); - await routerPage.view.tab.close(); - - const testViewPage = appPO.view({cssClass: 'testee'}); - const testViewId = await testViewPage.getViewId(); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left', ratio: .25}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: 'left', activateView: true}) + .navigateView('view.101', [], {hint: 'test-view'}) + .navigateView('view.102', [], {hint: 'test-view'}), + ); // Move test view to new window. - const newAppPO = await appPO.view({viewId: testViewId}).tab.moveToNewWindow(); + const newAppPO = await appPO.view({viewId: 'view.101'}).tab.moveToNewWindow(); + const newWindow = { + appPO: newAppPO, + workbenchNavigator: new WorkbenchNavigator(newAppPO), + }; // Expect test view to be moved to the new window. await expect(newAppPO.workbench).toEqualWorkbenchLayout({ @@ -111,14 +95,14 @@ test.describe('Workbench View', () => { }, mainAreaGrid: { root: new MPart({ - views: [{id: testViewId}], - activeViewId: testViewId, + views: [{id: 'view.1'}], + activeViewId: 'view.1', }), }, }); // Expect test view to display. - const viewPage = new ViewPagePO(newAppPO, {viewId: testViewId}); + const viewPage = new ViewPagePO(newWindow.appPO, {viewId: 'view.1'}); await expectView(viewPage).toBeActive(); // Expect test view to be removed from the origin window. @@ -127,44 +111,29 @@ test.describe('Workbench View', () => { root: new MTreeNode({ direction: 'row', ratio: .25, - child1: new MPart({id: 'left', views: [{id: 'other'}], activeViewId: 'other'}), + child1: new MPart({id: 'left', views: [{id: 'view.102'}], activeViewId: 'view.102'}), child2: new MPart({id: MAIN_AREA}), }), }, - mainAreaGrid: { - root: new MPart({ - views: [{id: await layoutPage.view.getViewId()}], - activeViewId: await layoutPage.view.getViewId(), - }), - }, }); }); - test('should allow moving a named view in the main area to a new window', async ({appPO, workbenchNavigator}) => { + test('should move a path-based view in the main area to a new window', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Register route of named view - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.registerRoute({path: '', component: 'view-page', outlet: 'testee'}); - // Open test view in main area. const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath(''); - await routerPage.enterTarget('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.100', + }); // Move test view to new window. - const newAppPO = await appPO.view({viewId: 'testee'}).tab.moveToNewWindow(); + const newAppPO = await appPO.view({viewId: 'view.100'}).tab.moveToNewWindow(); const newWindow = { appPO: newAppPO, workbenchNavigator: new WorkbenchNavigator(newAppPO), }; - // Register route for named view. - const newWindowLayoutPage = await newWindow.workbenchNavigator.openInNewTab(LayoutPagePO); - await newWindowLayoutPage.registerRoute({path: '', component: 'view-page', outlet: 'testee'}); - await newWindowLayoutPage.view.tab.close(); - // Expect test view to be moved to the new window. await expect(newAppPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { @@ -172,14 +141,14 @@ test.describe('Workbench View', () => { }, mainAreaGrid: { root: new MPart({ - views: [{id: 'testee'}], - activeViewId: 'testee', + views: [{id: 'view.1'}], + activeViewId: 'view.1', }), }, }); // Expect test view to display. - const viewPage = new ViewPagePO(newWindow.appPO, {viewId: 'testee'}); + const viewPage = new ViewPagePO(newWindow.appPO, {viewId: 'view.1'}); await expectView(viewPage).toBeActive(); // Expect test view to be removed from the origin window. @@ -190,7 +159,6 @@ test.describe('Workbench View', () => { mainAreaGrid: { root: new MPart({ views: [ - {id: await layoutPage.view.getViewId()}, {id: await routerPage.view.getViewId()}, ], activeViewId: await routerPage.view.getViewId(), @@ -199,17 +167,16 @@ test.describe('Workbench View', () => { }); }); - test('should allow moving an unnamed view in the main area to a new window', async ({appPO, workbenchNavigator}) => { + test('should move an empty-path view in the main area to a new window', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); // Open test view in main area. const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + cssClass: 'testee' + }); const testViewPage = appPO.view({cssClass: 'testee'}); - const testViewId = await testViewPage.getViewId(); // Move test view to new window. const newAppPO = await testViewPage.tab.moveToNewWindow(); @@ -221,14 +188,14 @@ test.describe('Workbench View', () => { }, mainAreaGrid: { root: new MPart({ - views: [{id: testViewId}], - activeViewId: testViewId, + views: [{id: 'view.1'}], + activeViewId: 'view.1', }), }, }); // Expect test view to display. - const viewPage = new ViewPagePO(newAppPO, {viewId: testViewId}); + const viewPage = new ViewPagePO(newAppPO, {viewId: 'view.1'}); await expectView(viewPage).toBeActive(); // Expect test view to be removed from the origin window. @@ -245,23 +212,21 @@ test.describe('Workbench View', () => { }); }); - test('should allow moving a view to another window', async ({appPO, workbenchNavigator}) => { + test('should move a view to another window', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Open view 1 (path-based view) - const view1 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - // Open view 2 (path-based view) - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - // Open view 3 (empty-path view) - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath(''); - await routerPage.enterTarget('outlet'); // TODO [WB-LAYOUT] Create test views via layout - await routerPage.clickNavigate(); - await routerPage.view.tab.close(); - const view3 = appPO.view({viewId: 'outlet'}); + await workbenchNavigator.createPerspective(factory => factory + .addPart('part') + .addView('view.101', {partId: 'part'}) + .addView('view.102', {partId: 'part'}) + .addView('view.103', {partId: 'part', activateView: true}) + .navigateView('view.101', ['test-view']) // path-based view + .navigateView('view.102', ['test-view']) // path-based view + .navigateView('view.103', [], {hint: 'test-view'}), // empty-path view + ); // Move view 1 to a new window - const newAppPO = await view1.tab.moveToNewWindow(); + const newAppPO = await appPO.view({viewId: 'view.101'}).tab.moveToNewWindow(); // Expect view 1 to be moved to the new window. await expect(newAppPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { @@ -279,22 +244,19 @@ test.describe('Workbench View', () => { // Expect view 1 to be removed from the original window. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { - root: new MPart({id: MAIN_AREA}), - }, - mainAreaGrid: { root: new MPart({ views: [ - {id: 'view.2'}, - {id: 'outlet'}, + {id: 'view.102'}, + {id: 'view.103'}, ], - activeViewId: 'outlet', + activeViewId: 'view.103', }), }, }); // Move view 2 to the new window - await view2.tab.moveTo(await newAppPO.activePart({inMainArea: true}).getPartId(), { - workbenchId: await newAppPO.getWorkbenchIdId(), + await appPO.view({viewId: 'view.102'}).tab.moveTo(await newAppPO.activePart({inMainArea: true}).getPartId(), { + workbenchId: await newAppPO.getWorkbenchId(), }); // Expect view 2 to be moved to the new window. await expect(newAppPO.workbench).toEqualWorkbenchLayout({ @@ -314,21 +276,18 @@ test.describe('Workbench View', () => { // Expect view 2 to be removed from the original window. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { - root: new MPart({id: MAIN_AREA}), - }, - mainAreaGrid: { root: new MPart({ views: [ - {id: 'outlet'}, + {id: 'view.103'}, ], - activeViewId: 'outlet', + activeViewId: 'view.103', }), }, }); // Move view 3 (empty-path view) to the new window - await view3.tab.moveTo(await newAppPO.activePart({inMainArea: true}).getPartId(), { - workbenchId: await newAppPO.getWorkbenchIdId(), + await appPO.view({viewId: 'view.103'}).tab.moveTo(await newAppPO.activePart({inMainArea: true}).getPartId(), { + workbenchId: await newAppPO.getWorkbenchId(), }); // Expect view 3 to be moved to the new window. await expect(newAppPO.workbench).toEqualWorkbenchLayout({ @@ -340,9 +299,9 @@ test.describe('Workbench View', () => { views: [ {id: 'view.1'}, {id: 'view.2'}, - {id: 'outlet'}, + {id: 'view.3'}, ], - activeViewId: 'outlet', + activeViewId: 'view.3', }), }, }); @@ -355,12 +314,11 @@ test.describe('Workbench View', () => { // Open test view in main area. const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('test-view'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + cssClass: 'test-view', + }); const testViewPage = appPO.view({cssClass: 'test-view'}); - const testViewId = await testViewPage.getViewId(); // Move test view to new window. const newAppPO = await testViewPage.tab.moveToNewWindow(); @@ -372,10 +330,10 @@ test.describe('Workbench View', () => { // Open peripheral view in the new browser window. const newAppRouterPage = await newWindow.workbenchNavigator.openInNewTab(RouterPagePO); - await newAppRouterPage.enterPath('test-view'); - await newAppRouterPage.enterCssClass('peripheral-view'); - await newAppRouterPage.enterTarget('blank'); - await newAppRouterPage.clickNavigate(); + await newAppRouterPage.navigate(['test-view'], { + target: 'blank', + cssClass: 'peripheral-view' + }); await newAppRouterPage.view.tab.close(); // Move peripheral view to workbench grid. @@ -398,8 +356,8 @@ test.describe('Workbench View', () => { }, mainAreaGrid: { root: new MPart({ - views: [{id: testViewId}], - activeViewId: testViewId, + views: [{id: 'view.1'}], + activeViewId: 'view.1', }), }, }); @@ -407,19 +365,8 @@ test.describe('Workbench View', () => { // Capture name of anonymous perspective. const anonymousPerspectiveName = await getPerspectiveId(newWindow.page); - // Register new perspective. - const perspectivePage = await newWindow.workbenchNavigator.openInNewTab(PerspectivePagePO); - await perspectivePage.registerPerspective({ - id: 'test-blank', - data: { - label: 'blank', - }, - parts: [{id: MAIN_AREA}], - }); - await perspectivePage.view.tab.close(); - // Switch to the new perspective. - await newWindow.appPO.switchPerspective('test-blank'); + await newWindow.workbenchNavigator.createPerspective(factory => factory.addPart(MAIN_AREA)); // Expect the layout to be blank. await expect(newWindow.appPO.workbench).toEqualWorkbenchLayout({ @@ -428,8 +375,8 @@ test.describe('Workbench View', () => { }, mainAreaGrid: { root: new MPart({ - views: [{id: testViewId}], - activeViewId: testViewId, + views: [{id: 'view.1'}], + activeViewId: 'view.1', }), }, }); @@ -452,8 +399,8 @@ test.describe('Workbench View', () => { }, mainAreaGrid: { root: new MPart({ - views: [{id: testViewId}], - activeViewId: testViewId, + views: [{id: 'view.1'}], + activeViewId: 'view.1', }), }, }); @@ -464,12 +411,11 @@ test.describe('Workbench View', () => { // Open test view in main area. const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('test-view'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + cssClass: 'test-view' + }); const testViewPage = appPO.view({cssClass: 'test-view'}); - const testViewId = await testViewPage.getViewId(); // Move test view to new window. const newAppPO = await testViewPage.tab.moveToNewWindow(); @@ -480,10 +426,10 @@ test.describe('Workbench View', () => { // Open peripheral view. const newAppRouterPage = await newWindow.workbenchNavigator.openInNewTab(RouterPagePO); - await newAppRouterPage.enterPath('test-view'); - await newAppRouterPage.enterCssClass('peripheral-view'); - await newAppRouterPage.enterTarget('blank'); - await newAppRouterPage.clickNavigate(); + await newAppRouterPage.navigate(['test-view'], { + target: 'blank', + cssClass: 'peripheral-view' + }); await newAppRouterPage.view.tab.close(); // Move peripheral view to workbench grid. @@ -506,8 +452,8 @@ test.describe('Workbench View', () => { }, mainAreaGrid: { root: new MPart({ - views: [{id: testViewId}], - activeViewId: testViewId, + views: [{id: 'view.1'}], + activeViewId: 'view.1', }), }, }); @@ -522,8 +468,8 @@ test.describe('Workbench View', () => { }, mainAreaGrid: { root: new MPart({ - views: [{id: testViewId}], - activeViewId: testViewId, + views: [{id: 'view.1'}], + activeViewId: 'view.1', }), }, }); diff --git a/projects/scion/e2e-testing/src/workbench/navigational-state.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/navigational-state.e2e-spec.ts index ea72d278c..d93a3379f 100644 --- a/projects/scion/e2e-testing/src/workbench/navigational-state.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/navigational-state.e2e-spec.ts @@ -20,24 +20,37 @@ test.describe('Navigational State', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + cssClass: 'testee', + }); - const viewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); + const viewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect.poll(() => viewPage.getState()).toEqual({}); }); - test('should have state passed', async ({appPO, workbenchNavigator}) => { + test('should pass state (WorkbenchRouter.navigate)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterState({some: 'state'}); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + state: {some: 'state'}, + cssClass: 'testee', + }); + + const viewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); + await expect.poll(() => viewPage.getState()).toEqual({some: 'state'}); + }); + + test('should pass state (WorkbenchLayout.navigateView)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); - const viewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {align: 'right'}) + .addView('testee', {partId: 'right', activateView: true, cssClass: 'testee'}) + .navigateView('testee', ['test-view'], {state: {some: 'state'}}), + ); + + const viewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect.poll(() => viewPage.getState()).toEqual({some: 'state'}); }); @@ -45,20 +58,20 @@ test.describe('Navigational State', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterState({ - state1: 'value', - state2: '0', - state3: '2', - state4: 'true', - state5: 'false', - state6: '', - state7: '', + await routerPage.navigate(['test-view'], { + state: { + state1: 'value', + state2: '0', + state3: '2', + state4: 'true', + state5: 'false', + state6: '', + state7: '', + }, + cssClass: 'testee', }); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); - const viewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); + const viewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect.poll(() => viewPage.getState()).toEqual({ state1: 'value', state2: '0 [number]', @@ -75,41 +88,39 @@ test.describe('Navigational State', () => { // Navigate view const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterState({state1: 'state 1'}); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + state: {state1: 'state 1'}, + cssClass: 'testee' + }); - const viewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); + const viewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect.poll(() => viewPage.getState()).toEqual({state1: 'state 1'}); // Navigate view again with a different state await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterMatrixParams({matrix: 'param'}); - await routerPage.enterState({state2: 'state 2'}); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {matrix: 'param'}], { + state: {state2: 'state 2'}, + cssClass: 'testee' + }); await expect.poll(() => viewPage.getState()).toEqual({state2: 'state 2'}); await expect.poll(() => viewPage.getParams()).toEqual({matrix: 'param'}); // Navigate view again without state await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterMatrixParams({}); - await routerPage.enterState({}); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + cssClass: 'testee' + }); await expect.poll(() => viewPage.getState()).toEqual({}); await expect.poll(() => viewPage.getParams()).toEqual({}); // Navigate view again with a different state await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterState({state3: 'state 3'}); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + state: {state3: 'state 3'}, + cssClass: 'testee' + }); await expect.poll(() => viewPage.getState()).toEqual({state3: 'state 3'}); }); @@ -118,12 +129,12 @@ test.describe('Navigational State', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterState({some: 'state'}); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + state: {some: 'state'}, + cssClass: 'testee', + }); - const viewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); + const viewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect.poll(() => viewPage.getState()).toEqual({some: 'state'}); await appPO.reload(); @@ -134,19 +145,18 @@ test.describe('Navigational State', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterState({some: 'state'}); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + state: {some: 'state'}, + cssClass: 'testee', + }); - const viewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); + const viewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect.poll(() => viewPage.getState()).toEqual({some: 'state'}); await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterState({}); - await routerPage.enterTarget('blank'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + }); // Expect view state to be preserved. await viewPage.view.tab.click(); @@ -156,30 +166,39 @@ test.describe('Navigational State', () => { test('should maintain state when navigating back and forth in browser history', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); - - const viewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); - await viewPage.view.tab.moveTo(await viewPage.view.part.getPartId(), {region: 'east'}); - - await routerPage.enterPath('test-view'); - await routerPage.enterState({'state': 'a'}); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await workbenchNavigator.createPerspective(factory => factory + .addPart('left') + .addPart('right', {align: 'right'}) + .addView('router', {partId: 'left', activateView: true, cssClass: 'router'}) + .addView('testee', {partId: 'right', activateView: true, cssClass: 'testee'}) + .navigateView('router', ['test-router']) + .navigateView('testee', ['test-view']), + ); + + const viewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); + + const routerPage = new RouterPagePO(appPO, {cssClass: 'router'}); + await routerPage.navigate(['test-view'], { + target: 'testee', + state: {'state': 'a'}, + }); await expect.poll(() => viewPage.getState()).toEqual({state: 'a'}); - await routerPage.enterPath('test-view'); - await routerPage.enterState({'state': 'b'}); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + // Move the view to the left and back again, simulating navigation without explicitly setting the state. + // When navigating back, expect the view state to be restored. + await viewPage.view.tab.moveTo('left'); + await viewPage.view.tab.moveTo('right'); + + await routerPage.navigate(['test-view'], { + target: 'testee', + state: {'state': 'b'}, + }); await expect.poll(() => viewPage.getState()).toEqual({state: 'b'}); - await routerPage.enterPath('test-view'); - await routerPage.enterState({'state': 'c'}); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'testee', + state: {'state': 'c'}, + }); await expect.poll(() => viewPage.getState()).toEqual({state: 'c'}); await appPO.navigateBack(); @@ -198,30 +217,32 @@ test.describe('Navigational State', () => { test('should maintain state when navigating through the Angular router', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Open Workbench Router - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - - // Open Angular router - await routerPage.enterPath('test-pages/angular-router-test-page'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); - const angularRouterPage = new AngularRouterTestPagePO(appPO, {viewId: 'view.101'}); + await workbenchNavigator.createPerspective(factory => factory + .addPart('left') + .addPart('right', {align: 'right'}) + .addView('workbench-router', {partId: 'left', activateView: true, cssClass: 'workbench-router'}) + .addView('angular-router', {partId: 'left', cssClass: 'angular-router'}) + .navigateView('workbench-router', ['test-router']) + .navigateView('angular-router', ['test-pages/angular-router-test-page']), + ); // Open test view - await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterState({some: 'state'}); - await routerPage.enterTarget('view.102'); - await routerPage.clickNavigate(); - const viewPage = new ViewPagePO(appPO, {viewId: 'view.102'}); - await viewPage.view.tab.moveTo(await viewPage.view.part.getPartId(), {region: 'east'}); + const routerPage = new RouterPagePO(appPO, {cssClass: 'workbench-router'}); + await routerPage.navigate(['test-view'], { + target: 'blank', + partId: 'right', + state: {some: 'state'}, + cssClass: 'testee' + }); // Expect view state to be passed to the view. + const viewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect.poll(() => viewPage.getState()).toEqual({some: 'state'}); // Navigate through the Angular router + const angularRouterPage = new AngularRouterTestPagePO(appPO, {cssClass: 'angular-router'}); await angularRouterPage.view.tab.click(); - await angularRouterPage.navigate('test-view', {outlet: await angularRouterPage.view.getViewId()}); + await angularRouterPage.navigate(['test-view'], {outlet: await angularRouterPage.view.getViewId()}); // Expect view state to be preserved. await expect.poll(() => viewPage.getState()).toEqual({some: 'state'}); diff --git a/projects/scion/e2e-testing/src/workbench/page-object/blank-view-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/blank-view-page.po.ts new file mode 100644 index 000000000..801e50877 --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/blank-view-page.po.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {AppPO} from '../../app.po'; +import {ViewPO} from '../../view.po'; +import {ViewId} from '@scion/workbench'; +import {WorkbenchViewPagePO} from './workbench-view-page.po'; +import {Locator} from '@playwright/test'; + +/** + * Page object to interact with a workbench view, which has not been navigated. + */ +export class BlankViewPagePO implements WorkbenchViewPagePO { + + public readonly view: ViewPO; + public readonly locator: Locator; + + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { + this.view = appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); + this.locator = this.view.locator.locator(':scope:has(router-outlet:first-child:last-child)'); + } +} diff --git a/projects/scion/e2e-testing/src/workbench/page-object/layout-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/layout-page.po.ts deleted file mode 100644 index 0c66c5341..000000000 --- a/projects/scion/e2e-testing/src/workbench/page-object/layout-page.po.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) 2018-2023 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms from the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {coerceArray, rejectWhenAttached, waitUntilAttached} from '../../helper/testing.util'; -import {AppPO} from '../../app.po'; -import {ViewPO} from '../../view.po'; -import {SciCheckboxPO} from '../../@scion/components.internal/checkbox.po'; -import {Locator} from '@playwright/test'; -import {ReferencePart} from '@scion/workbench'; -import {SciTabbarPO} from '../../@scion/components.internal/tabbar.po'; -import {WorkbenchViewPagePO} from './workbench-view-page.po'; - -/** - * Page object to interact with {@link LayoutPageComponent}. - */ -export class LayoutPagePO implements WorkbenchViewPagePO { - - public readonly locator: Locator; - public readonly view: ViewPO; - - private readonly _tabbar: SciTabbarPO; - - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { - this.view = appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); - this.locator = this.view.locator.locator('app-layout-page'); - this._tabbar = new SciTabbarPO(this.locator.locator('sci-tabbar')); - } - - public async addPart(partId: string, relativeTo: ReferencePart, options?: {activate?: boolean}): Promise { - const locator = this.locator.locator('app-add-part-page'); - - await this.view.tab.click(); - await this._tabbar.selectTab('e2e-add-part'); - await locator.locator('section.e2e-part').locator('input.e2e-part-id').fill(partId); - await new SciCheckboxPO(locator.locator('section.e2e-part').locator('sci-checkbox.e2e-activate')).toggle(options?.activate ?? false); - await locator.locator('section.e2e-reference-part').locator('input.e2e-part-id').fill(relativeTo.relativeTo ?? ''); - await locator.locator('section.e2e-reference-part').locator('select.e2e-align').selectOption(relativeTo.align); - await locator.locator('section.e2e-reference-part').locator('input.e2e-ratio').fill(relativeTo.ratio ? `${relativeTo.ratio}` : ''); - await locator.locator('button.e2e-navigate').click(); - - // Evaluate the response: resolve the promise on success, or reject it on error. - await Promise.race([ - waitUntilAttached(this.locator.locator('output.e2e-navigate-success')), - rejectWhenAttached(this.locator.locator('output.e2e-navigate-error')), - ]); - } - - public async addView(viewId: string, options: {partId: string; position?: number; activateView?: boolean; activatePart?: boolean}): Promise { - const locator = this.locator.locator('app-add-view-page'); - - await this.view.tab.click(); - await this._tabbar.selectTab('e2e-add-view'); - await locator.locator('section.e2e-view').locator('input.e2e-view-id').fill(viewId); - await locator.locator('section.e2e-view-options').locator('input.e2e-part-id').fill(options.partId); - options.position && await locator.locator('section.e2e-view-options').locator('input.e2e-position').fill(`${options.position}`); - await new SciCheckboxPO(locator.locator('section.e2e-view-options').locator('sci-checkbox.e2e-activate-view')).toggle(options.activateView ?? false); - await new SciCheckboxPO(locator.locator('section.e2e-view-options').locator('sci-checkbox.e2e-activate-part')).toggle(options.activatePart ?? false); - await locator.locator('button.e2e-navigate').click(); - - // Evaluate the response: resolve the promise on success, or reject it on error. - await Promise.race([ - waitUntilAttached(this.locator.locator('output.e2e-navigate-success')), - rejectWhenAttached(this.locator.locator('output.e2e-navigate-error')), - ]); - } - - public async activateView(viewId: string, options?: {activatePart?: boolean}): Promise { - const locator = this.locator.locator('app-activate-view-page'); - - await this.view.tab.click(); - await this._tabbar.selectTab('e2e-activate-view'); - await locator.locator('section.e2e-view').locator('input.e2e-view-id').fill(viewId); - await new SciCheckboxPO(locator.locator('section.e2e-view-options').locator('sci-checkbox.e2e-activate-part')).toggle(options?.activatePart ?? false); - await locator.locator('button.e2e-navigate').click(); - - // Evaluate the response: resolve the promise on success, or reject it on error. - await Promise.race([ - waitUntilAttached(this.locator.locator('output.e2e-navigate-success')), - rejectWhenAttached(this.locator.locator('output.e2e-navigate-error')), - ]); - } - - public async registerPartAction(content: string, options?: {align?: 'start' | 'end'; viewId?: string | string[]; partId?: string | string[]; grid?: 'workbench' | 'mainArea'; cssClass?: string | string[]}): Promise { - const locator = this.locator.locator('app-register-part-action-page'); - - await this.view.tab.click(); - await this._tabbar.selectTab('e2e-register-part-action'); - await locator.locator('section').locator('input.e2e-content').fill(content); - await locator.locator('section').locator('select.e2e-align').selectOption(options?.align ?? ''); - await locator.locator('section').locator('input.e2e-class').fill(coerceArray(options?.cssClass).join(' ')); - await locator.locator('section.e2e-can-match').locator('input.e2e-view-id').fill(coerceArray(options?.viewId).join(' ')); - await locator.locator('section.e2e-can-match').locator('input.e2e-part-id').fill(coerceArray(options?.partId).join(' ')); - await locator.locator('section.e2e-can-match').locator('input.e2e-grid').fill(options?.grid ?? ''); - await locator.locator('button.e2e-register').click(); - - // Evaluate the response: resolve the promise on success, or reject it on error. - await Promise.race([ - waitUntilAttached(this.locator.locator('output.e2e-register-success')), - rejectWhenAttached(this.locator.locator('output.e2e-register-error')), - ]); - } - - public async registerRoute(route: {path: string; component: 'view-page' | 'router-page'; outlet?: string}, routeData?: {title?: string; cssClass?: string | string[]}): Promise { - const locator = this.locator.locator('app-register-route-page'); - - await this._tabbar.selectTab('e2e-register-route'); - - await locator.locator('input.e2e-path').fill(route.path); - await locator.locator('input.e2e-component').fill(route.component); - await locator.locator('input.e2e-outlet').fill(route.outlet ?? ''); - await locator.locator('section.e2e-route-data').locator('input.e2e-title').fill(routeData?.title ?? ''); - await locator.locator('section.e2e-route-data').locator('input.e2e-css-class').fill(coerceArray(routeData?.cssClass).join(' ')); - await locator.locator('button.e2e-register').click(); - } -} diff --git a/projects/scion/e2e-testing/src/workbench/page-object/layout-page/create-perspective-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/layout-page/create-perspective-page.po.ts new file mode 100644 index 000000000..4120fde89 --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/layout-page/create-perspective-page.po.ts @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {rejectWhenAttached, waitUntilAttached} from '../../../helper/testing.util'; +import {Locator} from '@playwright/test'; +import {SciCheckboxPO} from '../../../@scion/components.internal/checkbox.po'; +import {SciKeyValueFieldPO} from '../../../@scion/components.internal/key-value-field.po'; +import {WorkbenchLayout, WorkbenchLayoutFactory} from '@scion/workbench'; +import {LayoutPages} from './layout-pages.po'; +import {ɵWorkbenchLayout, ɵWorkbenchLayoutFactory} from './layout.model'; + +/** + * Page object to interact with {@link CreatePerspectivePageComponent}. + */ +export class CreatePerspectivePagePO { + + constructor(public locator: Locator) { + } + + public async createPerspective(id: string, definition: PerspectiveDefinition): Promise { + // Enter perspective data. + await this.locator.locator('input.e2e-id').fill(id); + await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-transient')).toggle(definition.transient === true); + await this.enterData(definition.data); + + // Enter the layout. + const {parts, views, viewNavigations} = definition.layout(new ɵWorkbenchLayoutFactory()) as ɵWorkbenchLayout; + await LayoutPages.enterParts(this.locator.locator('app-add-parts'), parts); + await LayoutPages.enterViews(this.locator.locator('app-add-views'), views); + await LayoutPages.enterViewNavigations(this.locator.locator('app-navigate-views'), viewNavigations); + + // Register the perspective. + await this.locator.locator('button.e2e-register').click(); + + // Evaluate the response: resolve the promise on success, or reject it on error. + await Promise.race([ + waitUntilAttached(this.locator.locator('output.e2e-register-success')), + rejectWhenAttached(this.locator.locator('output.e2e-register-error')), + ]); + } + + private async enterData(data: {[key: string]: any} | undefined): Promise { + const keyValueField = new SciKeyValueFieldPO(this.locator.locator('sci-key-value-field.e2e-data')); + await keyValueField.clear(); + await keyValueField.addEntries(data ?? {}); + } +} + +export interface PerspectiveDefinition { + layout: (factory: WorkbenchLayoutFactory) => WorkbenchLayout; + data?: {[key: string]: any}; + transient?: true; +} diff --git a/projects/scion/e2e-testing/src/workbench/page-object/layout-page/layout-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/layout-page/layout-page.po.ts new file mode 100644 index 000000000..fbf7da5fe --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/layout-page/layout-page.po.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2018-2023 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {AppPO} from '../../../app.po'; +import {ViewPO} from '../../../view.po'; +import {Locator} from '@playwright/test'; +import {ViewId, WorkbenchLayout} from '@scion/workbench'; +import {SciTabbarPO} from '../../../@scion/components.internal/tabbar.po'; +import {WorkbenchViewPagePO} from '../workbench-view-page.po'; +import {RegisterPartActionPagePO} from './register-part-action-page.po'; +import {ModifyLayoutPagePO} from './modify-layout-page.po'; +import {CreatePerspectivePagePO, PerspectiveDefinition} from './create-perspective-page.po'; + +/** + * Page object to interact with {@link LayoutPageComponent}. + */ +export class LayoutPagePO implements WorkbenchViewPagePO { + + public readonly locator: Locator; + public readonly view: ViewPO; + + private readonly _tabbar: SciTabbarPO; + + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { + this.view = appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); + this.locator = this.view.locator.locator('app-layout-page'); + this._tabbar = new SciTabbarPO(this.locator.locator('sci-tabbar')); + } + + /** + * Creates a perspective based on the given definition. + * + * @see WorkbenchService.registerPerspective + */ + public async createPerspective(id: string, definition: PerspectiveDefinition): Promise { + await this.view.tab.click(); + await this._tabbar.selectTab('e2e-create-perspective'); + + const createPerspectivePage = new CreatePerspectivePagePO(this.locator.locator('app-create-perspective-page')); + return createPerspectivePage.createPerspective(id, definition); + } + + /** + * Modifies the current workbench layout. + * + * @see WorkbenchRouter.navigate + */ + public async modifyLayout(fn: (layout: WorkbenchLayout, activePartId: string) => WorkbenchLayout): Promise { + await this.view.tab.click(); + await this._tabbar.selectTab('e2e-modify-layout'); + + const modifyLayoutPage = new ModifyLayoutPagePO(this.view, this.locator.locator('app-modify-layout-page')); + return modifyLayoutPage.modify(fn); + } + + public async registerPartAction(content: string, options?: {align?: 'start' | 'end'; viewId?: ViewId | ViewId[]; partId?: string | string[]; grid?: 'workbench' | 'mainArea'; cssClass?: string | string[]}): Promise { + await this.view.tab.click(); + await this._tabbar.selectTab('e2e-register-part-action'); + + const registerPartActionPage = new RegisterPartActionPagePO(this.locator.locator('app-register-part-action-page')); + return registerPartActionPage.registerPartAction(content, options); + } +} diff --git a/projects/scion/e2e-testing/src/workbench/page-object/layout-page/layout-pages.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/layout-page/layout-pages.po.ts new file mode 100644 index 000000000..3f04f016c --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/layout-page/layout-pages.po.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Locator} from '@playwright/test'; +import {coerceArray, commandsToPath, toMatrixNotation} from '../../../helper/testing.util'; +import {PartDescriptor, ViewDescriptor, ViewNavigationDescriptor} from './layout.model'; +import {SciCheckboxPO} from '../../../@scion/components.internal/checkbox.po'; + +export const LayoutPages = { + + /** + * Enters parts into {@link AddPartsComponent}. + */ + enterParts: async (locator: Locator, parts: PartDescriptor[]): Promise => { + for (const [i, part] of parts.entries()) { + await locator.locator('button.e2e-add').click(); + await locator.locator('input.e2e-part-id').nth(i).fill(part.id); + await new SciCheckboxPO(locator.locator('sci-checkbox.e2e-activate-part').nth(i)).toggle(part.activate === true); + if (part.relativeTo !== undefined) { + await locator.locator('input.e2e-relative-to').nth(i).fill(part.relativeTo ?? ''); + } + if (part.align !== undefined) { + await locator.locator('select.e2e-align').nth(i).selectOption(part.align ?? null); + } + if (part.ratio !== undefined) { + await locator.locator('input.e2e-ratio').nth(i).fill(`${part.ratio ?? ''}`); + } + } + }, + + /** + * Enters views into {@link AddViewsComponent}. + */ + enterViews: async (locator: Locator, views: ViewDescriptor[] = []): Promise => { + for (const [i, view] of views.entries()) { + await locator.locator('button.e2e-add').click(); + await locator.locator('input.e2e-view-id').nth(i).fill(view.id); + await locator.locator('input.e2e-part-id').nth(i).fill(view.partId); + await locator.locator('input.e2e-position').nth(i).fill(view.position?.toString() ?? ''); + await locator.locator('input.e2e-class').nth(i).fill(coerceArray(view.cssClass).join(' ')); + await new SciCheckboxPO(locator.locator('sci-checkbox.e2e-activate-view').nth(i)).toggle(view.activateView === true); + await new SciCheckboxPO(locator.locator('sci-checkbox.e2e-activate-part').nth(i)).toggle(view.activatePart === true); + } + }, + + /** + * Enters view navigations into {@link NavigateViewsComponent}. + */ + enterViewNavigations: async (locator: Locator, viewNavigations: ViewNavigationDescriptor[] = []): Promise => { + for (const [i, viewNavigation] of viewNavigations.entries()) { + await locator.locator('button.e2e-add').click(); + await locator.locator('input.e2e-view-id').nth(i).fill(viewNavigation.id); + await locator.locator('input.e2e-commands').nth(i).fill(commandsToPath(viewNavigation.commands)); + await locator.locator('input.e2e-hint').nth(i).fill(viewNavigation.hint ?? ''); + await locator.locator('input.e2e-state').nth(i).fill(toMatrixNotation(viewNavigation.state)); + await locator.locator('input.e2e-class').nth(i).fill(coerceArray(viewNavigation.cssClass).join(' ')); + } + }, +} as const; diff --git a/projects/scion/e2e-testing/src/workbench/page-object/layout-page/layout.model.ts b/projects/scion/e2e-testing/src/workbench/page-object/layout-page/layout.model.ts new file mode 100644 index 000000000..7d390e65d --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/layout-page/layout.model.ts @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Commands, ReferencePart, ViewState, WorkbenchLayout, WorkbenchLayoutFactory} from '@scion/workbench'; +import {MAIN_AREA} from '../../../workbench.model'; +import {ActivatedRoute} from '@angular/router'; + +/** + * Implementation of {@link WorkbenchLayoutFactory} that can be used in page objects. + */ +export class ɵWorkbenchLayoutFactory implements WorkbenchLayoutFactory { + + public addPart(id: string | MAIN_AREA, options?: {activate?: boolean}): WorkbenchLayout { + return new ɵWorkbenchLayout().addInitialPart(id, options); + } +} + +/** + * Implementation of {@link WorkbenchLayout} that can be used in page objects. + */ +export class ɵWorkbenchLayout implements WorkbenchLayout { + + public parts = new Array(); + public views = new Array(); + public viewNavigations = new Array(); + + public addInitialPart(id: string | MAIN_AREA, options?: {activate?: boolean}): WorkbenchLayout { + this.parts.push({id, activate: options?.activate}); + return this; + } + + public addPart(id: string | MAIN_AREA, relativeTo: ReferencePart, options?: {activate?: boolean}): WorkbenchLayout { + this.parts.push({ + id, + relativeTo: relativeTo.relativeTo, + align: relativeTo.align, + ratio: relativeTo.ratio, + activate: options?.activate, + }); + return this; + } + + public addView(id: string, options: {partId: string; position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; activateView?: boolean; activatePart?: boolean; cssClass?: string | string[]}): WorkbenchLayout { + this.views.push({ + id, + partId: options.partId, + position: options.position, + cssClass: options.cssClass, + activatePart: options.activatePart, + activateView: options.activateView, + }); + return this; + } + + public navigateView(id: string, commands: Commands, extras?: {hint?: string; relativeTo?: ActivatedRoute; state?: ViewState; cssClass?: string | string[]}): WorkbenchLayout { + if (extras?.relativeTo) { + throw Error('[PageObjectError] Property `relativeTo` in `WorkbenchLayout.navigateView` is not supported.'); + } + + this.viewNavigations.push({ + id, + commands, + hint: extras?.hint, + state: extras?.state, + cssClass: extras?.cssClass, + }); + return this; + } + + public removeView(id: string): WorkbenchLayout { + throw Error('[PageObjectError] Operation `WorkbenchLayout.removeView` is not supported.'); + } + + public removePart(id: string): WorkbenchLayout { + throw Error('[PageObjectError] Operation `WorkbenchLayout.removePart` is not supported.'); + } + + public activateView(id: string, options?: {activatePart?: boolean}): WorkbenchLayout { + throw Error('[PageObjectError] Operation `WorkbenchLayout.activateView` is not supported.'); + } + + public activatePart(id: string): WorkbenchLayout { + throw Error('[PageObjectError] Operation `WorkbenchLayout.activatePart` is not supported.'); + } + + public moveView(id: string, targetPartId: string, options?: {position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; activateView?: boolean; activatePart?: boolean}): WorkbenchLayout { + throw Error('[PageObjectError] Operation `WorkbenchLayout.moveView` is not supported.'); + } +} + +/** + * Represents a part to add to the layout. + */ +export interface PartDescriptor { + id: string | MAIN_AREA; + relativeTo?: string; + align?: 'left' | 'right' | 'top' | 'bottom'; + ratio?: number; + activate?: boolean; +} + +/** + * Represents a view to add to the layout. + */ +export interface ViewDescriptor { + id: string; + partId: string; + position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; + activateView?: boolean; + activatePart?: boolean; + cssClass?: string | string[]; +} + +/** + * Represents a view navigation in the layout. + */ +export interface ViewNavigationDescriptor { + id: string; + commands: Commands; + hint?: string; + state?: ViewState; + cssClass?: string | string[]; +} diff --git a/projects/scion/e2e-testing/src/workbench/page-object/layout-page/modify-layout-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/layout-page/modify-layout-page.po.ts new file mode 100644 index 000000000..59efda12b --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/layout-page/modify-layout-page.po.ts @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {rejectWhenAttached, waitForCondition} from '../../../helper/testing.util'; +import {Locator} from '@playwright/test'; +import {WorkbenchLayout} from '@scion/workbench'; +import {LayoutPages} from './layout-pages.po'; +import {AppPO} from '../../../app.po'; +import {ViewPO} from '../../../view.po'; +import {ɵWorkbenchLayout} from './layout.model'; + +/** + * Page object to interact with {@link ModifyLayoutPageComponent}. + */ +export class ModifyLayoutPagePO { + + constructor(public view: ViewPO, public locator: Locator) { + } + + public async modify(fn: (layout: WorkbenchLayout, activePartId: string) => WorkbenchLayout): Promise { + const activePartId = await this.view.part.getPartId(); + const {parts, views, viewNavigations} = fn(new ɵWorkbenchLayout(), activePartId) as ɵWorkbenchLayout; + + // Enter the layout. + await LayoutPages.enterParts(this.locator.locator('app-add-parts'), parts); + await LayoutPages.enterViews(this.locator.locator('app-add-views'), views); + await LayoutPages.enterViewNavigations(this.locator.locator('app-navigate-views'), viewNavigations); + + // Apply the layout. + const appPO = new AppPO(this.locator.page()); + const navigationId = await appPO.getCurrentNavigationId(); + await this.locator.locator('button.e2e-modify').click(); + + // Evaluate the response: resolve the promise on success, or reject it on error. + await Promise.race([ + waitForCondition(async () => (await appPO.getCurrentNavigationId()) !== navigationId), + rejectWhenAttached(this.locator.locator('output.e2e-modify-error')), + ]); + } +} diff --git a/projects/scion/e2e-testing/src/workbench/page-object/layout-page/register-part-action-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/layout-page/register-part-action-page.po.ts new file mode 100644 index 000000000..5b7a0ef90 --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/layout-page/register-part-action-page.po.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {coerceArray, rejectWhenAttached, waitUntilAttached} from '../../../helper/testing.util'; +import {Locator} from '@playwright/test'; +import {ViewId} from '@scion/workbench'; + +/** + * Page object to interact with {@link RegisterPartActionPageComponent}. + */ +export class RegisterPartActionPagePO { + + constructor(public locator: Locator) { + } + + public async registerPartAction(content: string, options?: {align?: 'start' | 'end'; viewId?: ViewId | ViewId[]; partId?: string | string[]; grid?: 'workbench' | 'mainArea'; cssClass?: string | string[]}): Promise { + await this.locator.locator('input.e2e-content').fill(content); + await this.locator.locator('select.e2e-align').selectOption(options?.align ?? ''); + await this.locator.locator('input.e2e-class').fill(coerceArray(options?.cssClass).join(' ')); + await this.locator.locator('input.e2e-view-id').fill(coerceArray(options?.viewId).join(' ')); + await this.locator.locator('input.e2e-part-id').fill(coerceArray(options?.partId).join(' ')); + await this.locator.locator('input.e2e-grid').fill(options?.grid ?? ''); + await this.locator.locator('button.e2e-register').click(); + + // Evaluate the response: resolve the promise on success, or reject it on error. + await Promise.race([ + waitUntilAttached(this.locator.locator('output.e2e-register-success')), + rejectWhenAttached(this.locator.locator('output.e2e-register-error')), + ]); + } +} diff --git a/projects/scion/e2e-testing/src/workbench/page-object/message-box-opener-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/message-box-opener-page.po.ts index 5c9f7bdf9..160cbdc9d 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/message-box-opener-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/message-box-opener-page.po.ts @@ -14,7 +14,7 @@ import {SciCheckboxPO} from '../../@scion/components.internal/checkbox.po'; import {SciKeyValueFieldPO} from '../../@scion/components.internal/key-value-field.po'; import {Locator} from '@playwright/test'; import {ViewPO} from '../../view.po'; -import {WorkbenchMessageBoxOptions} from '@scion/workbench'; +import {ViewId, WorkbenchMessageBoxOptions} from '@scion/workbench'; import {WorkbenchViewPagePO} from './workbench-view-page.po'; /** @@ -28,7 +28,7 @@ export class MessageBoxOpenerPagePO implements WorkbenchViewPagePO { public readonly view: ViewPO; private readonly _openButton: Locator; - constructor(private _appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(private _appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = this._appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); this.locator = this.view.locator.locator('app-message-box-opener-page'); this.closeAction = this.locator.locator('output.e2e-close-action'); diff --git a/projects/scion/e2e-testing/src/workbench/page-object/notification-opener-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/notification-opener-page.po.ts index bac1fa8d9..1b1c8a87c 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/notification-opener-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/notification-opener-page.po.ts @@ -14,6 +14,7 @@ import {SciCheckboxPO} from '../../@scion/components.internal/checkbox.po'; import {Locator} from '@playwright/test'; import {WorkbenchViewPagePO} from './workbench-view-page.po'; import {ViewPO} from '../../view.po'; +import {ViewId} from '@scion/workbench'; /** * Page object to interact with {@link NotificationPageComponent}. @@ -24,7 +25,7 @@ export class NotificationOpenerPagePO implements WorkbenchViewPagePO { public readonly view: ViewPO; public readonly error: Locator; - constructor(private _appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(private _appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = this._appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); this.locator = this.view.locator.locator('app-notification-opener-page'); this.error = this.locator.locator('output.e2e-notification-open-error'); diff --git a/projects/scion/e2e-testing/src/workbench/page-object/page-not-found-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/page-not-found-page.po.ts new file mode 100644 index 000000000..7d82f1eb1 --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/page-object/page-not-found-page.po.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2018-2024 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms from the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {AppPO} from '../../app.po'; +import {ViewPO} from '../../view.po'; +import {Locator} from '@playwright/test'; +import {WorkbenchViewPagePO} from './workbench-view-page.po'; +import {ViewId} from '@scion/workbench'; + +/** + * Page object to interact with {@link PageNotFoundComponent}. + */ +export class PageNotFoundPagePO implements WorkbenchViewPagePO { + + public readonly locator: Locator; + public readonly view: ViewPO; + + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { + this.view = appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); + this.locator = this.view.locator.locator('wb-page-not-found'); + } +} diff --git a/projects/scion/e2e-testing/src/workbench/page-object/perspective-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/perspective-page.po.ts deleted file mode 100644 index 0898ffe8f..000000000 --- a/projects/scion/e2e-testing/src/workbench/page-object/perspective-page.po.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2018-2023 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms from the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {rejectWhenAttached, waitUntilAttached} from '../../helper/testing.util'; -import {AppPO} from '../../app.po'; -import {ViewPO} from '../../view.po'; -import {SciCheckboxPO} from '../../@scion/components.internal/checkbox.po'; -import {Locator} from '@playwright/test'; -import {SciKeyValueFieldPO} from '../../@scion/components.internal/key-value-field.po'; -import {MAIN_AREA} from '../../workbench.model'; -import {WorkbenchViewPagePO} from './workbench-view-page.po'; - -/** - * Page object to interact with {@link PerspectivePageComponent}. - */ -export class PerspectivePagePO implements WorkbenchViewPagePO { - - public readonly locator: Locator; - public readonly view: ViewPO; - - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { - this.view = appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); - this.locator = this.view.locator.locator('app-perspective-page'); - } - - public async registerPerspective(definition: PerspectiveDefinition): Promise { - // Enter perspective definition. - await this.locator.locator('input.e2e-id').fill(definition.id); - await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-transient')).toggle(definition.transient === true); - await this.enterData(definition.data); - - // Enter parts. - await this.enterParts(definition.parts); - - // Enter views. - await this.enterViews(definition.views); - - // Register perspective. - await this.locator.locator('button.e2e-register').click(); - - // Evaluate the response: resolve the promise on success, or reject it on error. - await Promise.race([ - waitUntilAttached(this.locator.locator('output.e2e-register-success')), - rejectWhenAttached(this.locator.locator('output.e2e-register-error')), - ]); - } - - private async enterData(data: {[key: string]: any} | undefined): Promise { - const keyValueField = new SciKeyValueFieldPO(this.locator.locator('sci-key-value-field.e2e-data')); - await keyValueField.clear(); - await keyValueField.addEntries(data ?? {}); - } - - private async enterParts(parts: PerspectivePartDescriptor[]): Promise { - const partsLocator = await this.locator.locator('app-perspective-page-parts'); - for (const [i, part] of parts.entries()) { - await partsLocator.locator('button.e2e-add').click(); - await partsLocator.locator('input.e2e-part-id').nth(i).fill(part.id); - await new SciCheckboxPO(partsLocator.locator('sci-checkbox.e2e-part-activate').nth(i)).toggle(part.activate === true); - if (i > 0) { - await partsLocator.locator('select.e2e-part-align').nth(i).selectOption(part.align!); - await partsLocator.locator('input.e2e-part-relative-to').nth(i).fill(part.relativeTo ?? ''); - await partsLocator.locator('input.e2e-part-ratio').nth(i).fill(part.ratio?.toString() ?? ''); - } - } - } - - private async enterViews(views: PerspectiveViewDescriptor[] = []): Promise { - const viewsLocator = await this.locator.locator('app-perspective-page-views'); - for (const [i, view] of views.entries()) { - await viewsLocator.locator('button.e2e-add').click(); - await viewsLocator.locator('input.e2e-view-id').nth(i).fill(view.id); - await viewsLocator.locator('input.e2e-view-part-id').nth(i).fill(view.partId); - await viewsLocator.locator('input.e2e-view-position').nth(i).fill(view.position?.toString() ?? ''); - await new SciCheckboxPO(viewsLocator.locator('sci-checkbox.e2e-view-activate-view').nth(i)).toggle(view.activateView === true); - await new SciCheckboxPO(viewsLocator.locator('sci-checkbox.e2e-view-activate-part').nth(i)).toggle(view.activatePart === true); - } - } -} - -export interface PerspectiveDefinition { - id: string; - transient?: true; - parts: PerspectivePartDescriptor[]; - views?: PerspectiveViewDescriptor[]; - data?: {[key: string]: any}; -} - -export interface PerspectivePartDescriptor { - id: string | MAIN_AREA; - relativeTo?: string; - align?: 'left' | 'right' | 'top' | 'bottom'; - ratio?: number; - activate?: boolean; -} - -export interface PerspectiveViewDescriptor { - id: string; - partId: string; - position?: number; - activateView?: boolean; - activatePart?: boolean; -} diff --git a/projects/scion/e2e-testing/src/workbench/page-object/popup-opener-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/popup-opener-page.po.ts index 5b76fc9a4..cf9926bb9 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/popup-opener-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/popup-opener-page.po.ts @@ -10,7 +10,7 @@ import {coerceArray, DomRect, fromRect, rejectWhenAttached} from '../../helper/testing.util'; import {AppPO} from '../../app.po'; -import {BottomLeftPoint, BottomRightPoint, PopupOrigin, PopupSize, TopLeftPoint, TopRightPoint} from '@scion/workbench'; +import {BottomLeftPoint, BottomRightPoint, PopupOrigin, PopupSize, TopLeftPoint, TopRightPoint, ViewId} from '@scion/workbench'; import {SciAccordionPO} from '../../@scion/components.internal/accordion.po'; import {SciCheckboxPO} from '../../@scion/components.internal/checkbox.po'; import {Locator} from '@playwright/test'; @@ -176,7 +176,7 @@ export class PopupOpenerPagePO implements WorkbenchViewPagePO { } } - public async enterContextualViewId(viewId: string | '' | ''): Promise { + public async enterContextualViewId(viewId: ViewId | '' | ''): Promise { await this.locator.locator('input.e2e-contextual-view-id').fill(viewId); } diff --git a/projects/scion/e2e-testing/src/workbench/page-object/router-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/router-page.po.ts index c75d54dea..3c097d2af 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/router-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/router-page.po.ts @@ -8,15 +8,14 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {coerceArray, rejectWhenAttached, waitUntilStable} from '../../helper/testing.util'; +import {coerceArray, commandsToPath, rejectWhenAttached, waitForCondition} from '../../helper/testing.util'; import {AppPO} from '../../app.po'; import {ViewPO} from '../../view.po'; import {SciKeyValueFieldPO} from '../../@scion/components.internal/key-value-field.po'; import {SciCheckboxPO} from '../../@scion/components.internal/checkbox.po'; import {Locator} from '@playwright/test'; -import {Params} from '@angular/router'; import {WorkbenchViewPagePO} from './workbench-view-page.po'; -import {ViewState} from '@scion/workbench'; +import {Commands, ViewId, ViewState, WorkbenchNavigationExtras} from '@scion/workbench'; /** * Page object to interact with {@link RouterPageComponent}. @@ -26,80 +25,112 @@ export class RouterPagePO implements WorkbenchViewPagePO { public readonly locator: Locator; public readonly view: ViewPO; - constructor(private _appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(private _appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = this._appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); this.locator = this.view.locator.locator('app-router-page'); } - public async enterPath(path: string): Promise { - await this.locator.locator('input.e2e-path').fill(path); + /** + * Navigates via {@link WorkbenchRouter}. + */ + public async navigate(commands: Commands, extras?: WorkbenchNavigationExtras & RouterPageOptions): Promise { + await this.enterCommands(commands); + await this.enterExtras(extras); + + const navigationId = await this._appPO.getCurrentNavigationId(); + await this.locator.locator('button.e2e-router-navigate').click(); + + if (!(extras?.waitForNavigation ?? true)) { + return; + } + + // Evaluate the response: resolve the promise on success, or reject it on error. + await Promise.race([ + waitForCondition(async () => (await this._appPO.getCurrentNavigationId()) !== navigationId), + rejectWhenAttached(this.locator.locator('output.e2e-navigate-error')), + ]); } - public async enterMatrixParams(params: Params): Promise { - const keyValueField = new SciKeyValueFieldPO(this.locator.locator('sci-key-value-field.e2e-matrix-params')); - await keyValueField.clear(); - await keyValueField.addEntries(params); + /** + * Navigates via workbench router link. + */ + public async navigateViaRouterLink(commands: Commands, extras?: Omit & RouterPageOptions & RouterLinkPageOptions): Promise { + await this.enterCommands(commands); + await this.enterExtras(extras); + + const navigationId = await this._appPO.getCurrentNavigationId(); + await this.locator.locator('a.e2e-router-link-navigate').click({modifiers: extras?.modifiers}); + + // Wait until navigation completed. + await waitForCondition(async () => (await this._appPO.getCurrentNavigationId()) !== navigationId); } - public async enterQueryParams(params: Params): Promise { - const keyValueField = new SciKeyValueFieldPO(this.locator.locator('sci-key-value-field.e2e-query-params')); - await keyValueField.clear(); - await keyValueField.addEntries(params); + private async enterCommands(commands: Commands): Promise { + await this.locator.locator('input.e2e-commands').fill(commandsToPath(commands)); } - public async enterState(state: ViewState): Promise { + private async enterExtras(extras: WorkbenchNavigationExtras & RouterPageOptions | undefined): Promise { + await this.enterTarget(extras?.target); + await this.enterHint(extras?.hint); + await this.enterState(extras?.state); + await this.checkActivate(extras?.activate); + await this.checkClose(extras?.close); + await this.enterPosition(extras?.position); + await this.enterPartId(extras?.partId); + await this.checkViewContext(extras?.viewContextActive); + await this.enterCssClass(extras?.cssClass); + } + + private async enterState(state?: ViewState): Promise { const keyValueField = new SciKeyValueFieldPO(this.locator.locator('sci-key-value-field.e2e-state')); await keyValueField.clear(); - await keyValueField.addEntries(state); + await keyValueField.addEntries(state ?? {}); } - public async checkActivate(check: boolean): Promise { - await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-activate')).toggle(check); + private async checkActivate(check?: boolean): Promise { + if (check !== undefined) { + await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-activate')).toggle(check); + } } - public async checkClose(check: boolean): Promise { - await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-close')).toggle(check); + private async checkClose(check?: boolean): Promise { + if (check !== undefined) { + await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-close')).toggle(check); + } } - public async enterTarget(target?: string | 'blank' | 'auto'): Promise { + private async enterTarget(target?: string | 'blank' | 'auto'): Promise { await this.locator.locator('input.e2e-target').fill(target ?? ''); } - public async enterInsertionIndex(insertionIndex: number | 'start' | 'end' | undefined): Promise { - await this.locator.locator('input.e2e-insertion-index').fill(`${insertionIndex}`); + private async enterHint(hint?: string): Promise { + await this.locator.locator('input.e2e-hint').fill(hint ?? ''); } - public async enterBlankPartId(blankPartId: string): Promise { - await this.locator.locator('input.e2e-blank-part-id').fill(blankPartId); + private async enterPosition(position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'): Promise { + await this.locator.locator('input.e2e-position').fill(`${position ?? ''}`); } - public async enterCssClass(cssClass: string | string[]): Promise { - await this.locator.locator('input.e2e-css-class').fill(coerceArray(cssClass).join(' ')); + private async enterPartId(partId?: string): Promise { + await this.locator.locator('input.e2e-part-id').fill(partId ?? ''); } - public async checkViewContext(check: boolean): Promise { - await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-view-context')).toggle(check); + private async enterCssClass(cssClass?: string | string[]): Promise { + await this.locator.locator('input.e2e-class').fill(coerceArray(cssClass).join(' ')); } - /** - * Clicks on a button to navigate via {@link WorkbenchRouter}. - */ - public async clickNavigate(): Promise { - await this.locator.locator('button.e2e-router-navigate').click(); - - // Evaluate the response: resolve the promise on success, or reject it on error. - await Promise.race([ - waitUntilStable(() => this._appPO.getCurrentNavigationId()), - rejectWhenAttached(this.locator.locator('output.e2e-navigate-error')), - ]); + private async checkViewContext(check?: boolean): Promise { + if (check !== undefined) { + await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-view-context')).toggle(check); + } } +} - /** - * Clicks on the router link, optionally pressing the specified modifier key. - */ - public async clickNavigateViaRouterLink(modifiers?: Array<'Alt' | 'Control' | 'Meta' | 'Shift'>): Promise { - await this.locator.locator('a.e2e-router-link-navigate').click({modifiers}); - // Wait until navigation completed. - await waitUntilStable(() => this._appPO.getCurrentNavigationId()); - } +export interface RouterPageOptions { + viewContextActive?: boolean; + waitForNavigation?: false; +} + +export interface RouterLinkPageOptions { + modifiers?: Array<'Alt' | 'Control' | 'Meta' | 'Shift'>; } diff --git a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/angular-router-test-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/angular-router-test-page.po.ts index 3745c2684..8d4ac95c9 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/angular-router-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/angular-router-test-page.po.ts @@ -12,26 +12,29 @@ import {AppPO} from '../../../app.po'; import {Locator} from '@playwright/test'; import {WorkbenchViewPagePO} from '../workbench-view-page.po'; import {ViewPO} from '../../../view.po'; -import {rejectWhenAttached, waitUntilStable} from '../../../helper/testing.util'; +import {Commands, ViewId} from '@scion/workbench'; +import {commandsToPath, rejectWhenAttached, waitForCondition} from '../../../helper/testing.util'; export class AngularRouterTestPagePO implements WorkbenchViewPagePO { public readonly locator: Locator; public readonly view: ViewPO; - constructor(private _appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(private _appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = this._appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); this.locator = this.view.locator.locator('app-angular-router-test-page'); } - public async navigate(path: string, extras: {outlet: string}): Promise { - await this.locator.locator('input.e2e-path').fill(path); + public async navigate(commands: Commands, extras: {outlet: string}): Promise { + await this.locator.locator('input.e2e-commands').fill(commandsToPath(commands)); await this.locator.locator('input.e2e-outlet').fill(extras.outlet); + + const navigationId = await this._appPO.getCurrentNavigationId(); await this.locator.locator('button.e2e-navigate').click(); // Evaluate the response: resolve the promise on success, or reject it on error. await Promise.race([ - waitUntilStable(() => this._appPO.getCurrentNavigationId()), + waitForCondition(async () => (await this._appPO.getCurrentNavigationId()) !== navigationId), rejectWhenAttached(this.locator.locator('output.e2e-navigate-error')), ]); } diff --git a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/bulk-navigation-test-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/bulk-navigation-test-page.po.ts index 38d722de3..e3f0cd554 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/bulk-navigation-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/bulk-navigation-test-page.po.ts @@ -15,13 +15,14 @@ import {RouterPagePO} from '../router-page.po'; import {WorkbenchNavigator} from '../../workbench-navigator'; import {WorkbenchViewPagePO} from '../workbench-view-page.po'; import {ViewPO} from '../../../view.po'; +import {ViewId} from '@scion/workbench'; export class BulkNavigationTestPagePO implements WorkbenchViewPagePO { public readonly locator: Locator; public readonly view: ViewPO; - constructor(private _appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(private _appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = this._appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.view.locator.locator('app-bulk-navigation-test-page'); } @@ -31,7 +32,7 @@ export class BulkNavigationTestPagePO implements WorkbenchViewPagePO { } public async enterCssClass(cssClass: string): Promise { - await this.locator.locator('input.e2e-css-class').fill(cssClass); + await this.locator.locator('input.e2e-class').fill(cssClass); } public async clickNavigateNoAwait(): Promise { @@ -50,9 +51,9 @@ export class BulkNavigationTestPagePO implements WorkbenchViewPagePO { const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); const viewId = await routerPage.view.getViewId(); - await routerPage.enterPath('test-pages/bulk-navigation-test-page'); - await routerPage.enterTarget(viewId); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/bulk-navigation-test-page'], { + target: viewId, + }); const view = appPO.view({cssClass: 'e2e-test-bulk-navigation', viewId}); await view.waitUntilAttached(); diff --git a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/focus-test-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/focus-test-page.po.ts index a2ea30465..e04aff04b 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/focus-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/focus-test-page.po.ts @@ -46,7 +46,7 @@ export class FocusTestPagePO implements WorkbenchViewPagePO, WorkbenchDialogPage return this.lastField.click({timeout: options?.timeout}); } default: { - throw Error(`[IllegalArgumentError] Specified field not found: ${field}`); + throw Error(`[PageObjectError] Specified field not found: ${field}`); } } } diff --git a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/input-field-test-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/input-field-test-page.po.ts index c2f3b0190..4256366fa 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/input-field-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/input-field-test-page.po.ts @@ -76,10 +76,10 @@ export class InputFieldTestPagePO implements WorkbenchViewPagePO, WorkbenchDialo const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); const viewId = await routerPage.view.getViewId(); - await routerPage.enterPath('test-pages/input-field-test-page'); - await routerPage.enterTarget(viewId); - await routerPage.enterCssClass('input-field-test-page'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/input-field-test-page'], { + target: viewId, + cssClass: 'input-field-test-page' + }); const view = appPO.view({cssClass: 'input-field-test-page', viewId}); await view.waitUntilAttached(); diff --git a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/navigation-test-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/navigation-test-page.po.ts index 70612ab8e..d2ef94dfe 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/navigation-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/navigation-test-page.po.ts @@ -12,13 +12,14 @@ import {AppPO} from '../../../app.po'; import {Locator} from '@playwright/test'; import {WorkbenchViewPagePO} from '../workbench-view-page.po'; import {ViewPO} from '../../../view.po'; +import {ViewId} from '@scion/workbench'; export class NavigationTestPagePO implements WorkbenchViewPagePO { public readonly locator: Locator; public readonly view: ViewPO; - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); this.locator = this.view.locator.locator('app-navigation-test-page'); } diff --git a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/non-standalone-view-test-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/non-standalone-view-test-page.po.ts index 678650fcd..ff4bf71f7 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/non-standalone-view-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/non-standalone-view-test-page.po.ts @@ -12,13 +12,14 @@ import {AppPO} from '../../../app.po'; import {Locator} from '@playwright/test'; import {ViewPO} from '../../../view.po'; import {WorkbenchViewPagePO} from '../workbench-view-page.po'; +import {ViewId} from '@scion/workbench'; export class NonStandaloneViewTestPagePO implements WorkbenchViewPagePO { public readonly locator: Locator; public readonly view: ViewPO; - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.view.locator.locator('app-non-standalone-view-test-page'); } diff --git a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/standalone-view-test-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/standalone-view-test-page.po.ts index e9e249e3f..2bb01eafe 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/standalone-view-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/standalone-view-test-page.po.ts @@ -12,13 +12,14 @@ import {AppPO} from '../../../app.po'; import {Locator} from '@playwright/test'; import {ViewPO} from '../../../view.po'; import {WorkbenchViewPagePO} from '../workbench-view-page.po'; +import {ViewId} from '@scion/workbench'; export class StandaloneViewTestPagePO implements WorkbenchViewPagePO { public readonly locator: Locator; public readonly view: ViewPO; - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.view.locator.locator('app-standalone-view-test-page'); } diff --git a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/workbench-theme-test-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/workbench-theme-test-page.po.ts index 71f7752bb..3ec005e98 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/test-pages/workbench-theme-test-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/test-pages/workbench-theme-test-page.po.ts @@ -14,6 +14,7 @@ import {WorkbenchNavigator} from '../../workbench-navigator'; import {RouterPagePO} from '../router-page.po'; import {WorkbenchViewPagePO} from '../workbench-view-page.po'; import {ViewPO} from '../../../view.po'; +import {ViewId} from '@scion/workbench'; export class WorkbenchThemeTestPagePO implements WorkbenchViewPagePO { @@ -22,7 +23,7 @@ export class WorkbenchThemeTestPagePO implements WorkbenchViewPagePO { public readonly theme: Locator; public readonly colorScheme: Locator; - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = appPO.view({viewId: locateBy.viewId, cssClass: locateBy.cssClass}); this.locator = this.view.locator.locator('app-workbench-theme-test-page'); @@ -34,9 +35,9 @@ export class WorkbenchThemeTestPagePO implements WorkbenchViewPagePO { const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); const viewId = await routerPage.view.getViewId(); - await routerPage.enterPath('test-pages/workbench-theme-test-page'); - await routerPage.enterTarget(viewId); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/workbench-theme-test-page'], { + target: viewId, + }); const view = appPO.view({cssClass: 'e2e-test-workbench-theme', viewId}); await view.waitUntilAttached(); diff --git a/projects/scion/e2e-testing/src/workbench/page-object/view-info-dialog.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/view-info-dialog.po.ts index 3dda1df1d..370f4008a 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/view-info-dialog.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/view-info-dialog.po.ts @@ -12,7 +12,7 @@ import {Locator} from '@playwright/test'; import {SciKeyValuePO} from '../../@scion/components.internal/key-value.po'; import {DialogPO} from '../../dialog.po'; import {WorkbenchDialogPagePO} from './workbench-dialog-page.po'; -import {ViewState} from '@scion/workbench'; +import {ViewId, ViewState} from '@scion/workbench'; import {Data, Params} from '@angular/router'; /** @@ -32,11 +32,13 @@ export class ViewInfoDialogPO implements WorkbenchDialogPagePO { const state = this.locator.locator('sci-key-value.e2e-state'); return { - viewId: await this.locator.locator('span.e2e-view-id').innerText(), + viewId: await this.locator.locator('span.e2e-view-id').innerText() as ViewId, + alternativeId: await this.locator.locator('span.e2e-alternative-view-id').innerText(), partId: await this.locator.locator('span.e2e-part-id').innerText(), title: await this.locator.locator('span.e2e-title').innerText(), heading: await this.locator.locator('span.e2e-heading').innerText(), urlSegments: await this.locator.locator('span.e2e-url-segments').innerText(), + navigationHint: await this.locator.locator('span.e2e-navigation-hint').innerText(), routeParams: await routeParams.isVisible() ? await new SciKeyValuePO(routeParams).readEntries() : {}, routeData: await routeData.isVisible() ? await new SciKeyValuePO(routeData).readEntries() : {}, state: await state.isVisible() ? await new SciKeyValuePO(state).readEntries() : {}, @@ -45,11 +47,13 @@ export class ViewInfoDialogPO implements WorkbenchDialogPagePO { } export interface ViewInfo { - viewId: string; + viewId: ViewId; + alternativeId: string; partId: string; title: string; heading: string; urlSegments: string; + navigationHint: string; routeParams: Params; routeData: Data; state: ViewState; diff --git a/projects/scion/e2e-testing/src/workbench/page-object/view-page.po.ts b/projects/scion/e2e-testing/src/workbench/page-object/view-page.po.ts index 091543902..81ebae5e2 100644 --- a/projects/scion/e2e-testing/src/workbench/page-object/view-page.po.ts +++ b/projects/scion/e2e-testing/src/workbench/page-object/view-page.po.ts @@ -17,7 +17,7 @@ import {SciAccordionPO} from '../../@scion/components.internal/accordion.po'; import {Params} from '@angular/router'; import {SciKeyValuePO} from '../../@scion/components.internal/key-value.po'; import {WorkbenchViewPagePO} from './workbench-view-page.po'; -import {ViewState} from '@scion/workbench'; +import {ViewId, ViewState} from '@scion/workbench'; /** * Page object to interact with {@link ViewPageComponent}. @@ -28,7 +28,7 @@ export class ViewPagePO implements WorkbenchViewPagePO { public readonly view: ViewPO; public readonly viewId: Locator; - constructor(appPO: AppPO, locateBy: {viewId?: string; cssClass?: string}) { + constructor(appPO: AppPO, locateBy: {viewId?: ViewId; cssClass?: string}) { this.view = appPO.view({viewId: locateBy?.viewId, cssClass: locateBy?.cssClass}); this.locator = this.view.locator.locator('app-view-page'); this.viewId = this.locator.locator('span.e2e-view-id'); @@ -79,13 +79,17 @@ export class ViewPagePO implements WorkbenchViewPagePO { } public async enterCssClass(cssClass: string | string[]): Promise { - await this.locator.locator('input.e2e-css-class').fill(coerceArray(cssClass).join(' ')); + await this.locator.locator('input.e2e-class').fill(coerceArray(cssClass).join(' ')); } public async checkClosable(check: boolean): Promise { await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-closable')).toggle(check); } + public async checkConfirmClosing(check: boolean): Promise { + await new SciCheckboxPO(this.locator.locator('sci-checkbox.e2e-confirm-closing')).toggle(check); + } + public async clickClose(): Promise { const accordion = new SciAccordionPO(this.locator.locator('sci-accordion.e2e-view-actions')); await accordion.expand(); diff --git a/projects/scion/e2e-testing/src/workbench/router-link.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/router-link.e2e-spec.ts index d5064380e..a3a463265 100644 --- a/projects/scion/e2e-testing/src/workbench/router-link.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/router-link.e2e-spec.ts @@ -11,13 +11,11 @@ import {expect} from '@playwright/test'; import {test} from '../fixtures'; import {RouterPagePO} from './page-object/router-page.po'; -import {LayoutPagePO} from './page-object/layout-page.po'; -import {PerspectivePagePO} from './page-object/perspective-page.po'; import {MPart, MTreeNode} from '../matcher/to-equal-workbench-layout.matcher'; -import {MAIN_AREA} from '../workbench.model'; import {expectView} from '../matcher/view-matcher'; import {ViewPagePO} from './page-object/view-page.po'; -import {NavigationTestPagePO} from './page-object/test-pages/navigation-test-page.po'; +import {ViewInfo} from './page-object/view-info-dialog.po'; +import {MAIN_AREA} from '../workbench.model'; test.describe('Workbench RouterLink', () => { @@ -25,9 +23,9 @@ test.describe('Workbench RouterLink', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigateViaRouterLink(); + await routerPage.navigateViaRouterLink(['/test-view'], { + cssClass: 'testee', + }); const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect(appPO.views()).toHaveCount(1); @@ -36,14 +34,55 @@ test.describe('Workbench RouterLink', () => { await expectView(testeeViewPage).toBeActive(); }); + test('should open view in current tab (view is in peripheral area)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.createPerspective(factory => factory + .addPart(MAIN_AREA) + .addPart('left', {align: 'left'}) + .addView('view.101', {partId: 'left', activateView: true}), + ); + + // Add state via separate navigation as not supported when adding views to the perspective. + await workbenchNavigator.modifyLayout(layout => layout + .navigateView('view.101', ['test-router'], {state: {navigated: 'false'}}), + ); + + // Open test view via router link. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.101'}); + await routerPage.navigateViaRouterLink(['/test-view'], { + state: {navigated: 'true'}, + }); + + // Expect router page to be replaced + await expect.poll(() => routerPage.view.getInfo()).toMatchObject( + { + viewId: 'view.101', + urlSegments: 'test-view', + state: {navigated: 'true'}, + } satisfies Partial, + ); + + await expect(appPO.workbench).toEqualWorkbenchLayout({ + workbenchGrid: { + root: new MTreeNode({ + direction: 'row', + ratio: .5, + child1: new MPart({id: 'left', views: [{id: 'view.101'}], activeViewId: 'view.101'}), + child2: new MPart({id: MAIN_AREA}), + }), + }, + }); + }); + test('should open the view in a new view tab (target="auto")', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.enterTarget('auto'); - await routerPage.clickNavigateViaRouterLink(); + await routerPage.navigateViaRouterLink(['/test-view'], { + target: 'auto', + cssClass: 'testee', + }); const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect(appPO.views()).toHaveCount(2); @@ -55,10 +94,10 @@ test.describe('Workbench RouterLink', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.enterTarget('blank'); - await routerPage.clickNavigateViaRouterLink(); + await routerPage.navigateViaRouterLink(['/test-view'], { + target: 'blank', + cssClass: 'testee', + }); const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect(appPO.views()).toHaveCount(2); @@ -70,9 +109,10 @@ test.describe('Workbench RouterLink', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigateViaRouterLink(['Control']); + await routerPage.navigateViaRouterLink(['/test-view'], { + cssClass: 'testee', + modifiers: ['Control'], + }); const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect(appPO.views()).toHaveCount(2); @@ -80,6 +120,36 @@ test.describe('Workbench RouterLink', () => { await expectView(testeeViewPage).toBeInactive(); }); + test('should open the view in a new view tab without activating it when pressing the CTRL modifier key (target="auto")', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + + // Navigate via router link while pressing CTRL Modifier key. + await routerPage.navigateViaRouterLink(['/test-view'], { + target: 'auto', + cssClass: 'testee-1', + modifiers: ['Control'], + }); + + const testeeViewPage1 = new ViewPagePO(appPO, {cssClass: 'testee-1'}); + await expect(appPO.views()).toHaveCount(2); + await expectView(routerPage).toBeActive(); + await expectView(testeeViewPage1).toBeInactive(); + + // Navigate via router link again while pressing CTRL Modifier key. + await routerPage.navigateViaRouterLink(['/test-view'], { + target: 'auto', + cssClass: 'testee-2', + modifiers: ['Control'], + }); + + const testeeViewPage2 = new ViewPagePO(appPO, {cssClass: 'testee-2'}); + await expect(appPO.views()).toHaveCount(3); + await expectView(routerPage).toBeActive(); + await expectView(testeeViewPage1).toBeInactive(); + await expectView(testeeViewPage2).toBeInactive(); + }); + /** * The Meta key is the Windows logo key, or the Command or ⌘ key on Mac keyboards. * @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values @@ -88,9 +158,10 @@ test.describe('Workbench RouterLink', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigateViaRouterLink(['Meta']); + await routerPage.navigateViaRouterLink(['/test-view'], { + cssClass: 'testee', + modifiers: ['Meta'], + }); const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect(appPO.views()).toHaveCount(2); @@ -102,10 +173,11 @@ test.describe('Workbench RouterLink', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.checkActivate(true); - await routerPage.clickNavigateViaRouterLink(['Control']); + await routerPage.navigateViaRouterLink(['/test-view'], { + activate: true, + cssClass: 'testee', + modifiers: ['Control'], + }); const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect(appPO.views()).toHaveCount(2); @@ -121,10 +193,11 @@ test.describe('Workbench RouterLink', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.checkActivate(true); - await routerPage.clickNavigateViaRouterLink(['Meta']); + await routerPage.navigateViaRouterLink(['/test-view'], { + activate: true, + cssClass: 'testee', + modifiers: ['Meta'], + }); const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); await expect(appPO.views()).toHaveCount(2); @@ -132,292 +205,379 @@ test.describe('Workbench RouterLink', () => { await expectView(testeeViewPage).toBeActive(); }); - test('should close view by path', async ({appPO, workbenchNavigator}) => { + test('should navigate present view(s) if navigating outside a view and not setting a target', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); + // WHEN const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigateViaRouterLink(['/test-view'], { + viewContextActive: false, // simulate navigating outside a view context + cssClass: 'testee', + }); - // GIVEN - // Open test view 1 (but do not activate it) - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.checkActivate(false); - await routerPage.clickNavigate(); + const testViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); - const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); - await expectView(routerPage).toBeActive(); - await expectView(testeeViewPage).toBeInactive(); + // THEN + await expectView(routerPage).toBeInactive(); + await expectView(testViewPage).toBeActive(); + await expect(appPO.views()).toHaveCount(2); + }); + + test('should replace the current view if navigating inside a view (and not activate a matching view)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['/test-view'], { + activate: false, + cssClass: 'testee-1', + }); // WHEN - await routerPage.enterPath('/test-view'); - await routerPage.checkClose(true); - await routerPage.clickNavigateViaRouterLink(); + await routerPage.navigateViaRouterLink(['/test-view'], { + cssClass: 'testee-2', + }); + + const testViewPage1 = new ViewPagePO(appPO, {cssClass: 'testee-1'}); + const testViewPage2 = new ViewPagePO(appPO, {cssClass: 'testee-2'}); // THEN - await expectView(routerPage).toBeActive(); - await expectView(testeeViewPage).not.toBeAttached(); - await expect(appPO.views()).toHaveCount(1); + await expectView(testViewPage1).toBeInactive(); + await expectView(testViewPage2).toBeActive(); + await expect(appPO.views()).toHaveCount(2); }); - test('should close view by id', async ({appPO, workbenchNavigator}) => { + test('should navigate current view when navigating from path-based route to path-based route', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - - // GIVEN - // Open test view 1 (but do not activate it) - await routerPage.enterPath('/test-view'); - await routerPage.enterTarget('view.101'); - await routerPage.checkActivate(false); - await routerPage.clickNavigate(); + // Open router page as path-based route. + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addView('view.100', {partId: activePartId}) + .navigateView('view.100', ['test-router']), + ); - const testeeViewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); - await expectView(routerPage).toBeActive(); - await expectView(testeeViewPage).toBeInactive(); + // Navigate to path-based route via router link. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.100'}); + await routerPage.navigateViaRouterLink(['/test-view']); - // WHEN - await routerPage.enterPath(''); - await routerPage.enterTarget('view.101'); - await routerPage.checkClose(true); - await routerPage.clickNavigateViaRouterLink(); + const testeeViewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); - // THEN - await expectView(routerPage).toBeActive(); - await expectView(testeeViewPage).not.toBeAttached(); + // Expect view to display path-based route. + await expectView(testeeViewPage).toBeActive(); await expect(appPO.views()).toHaveCount(1); + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: ''}, + } satisfies Partial, + ); }); - test('should close the current view without explicit target', async ({appPO, workbenchNavigator}) => { + test('should navigate current view when navigating from path-based route to empty-path route (1/2)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // GIVEN - const routerPage1 = await workbenchNavigator.openInNewTab(RouterPagePO); - const routerPage2 = await workbenchNavigator.openInNewTab(RouterPagePO); - const routerPage3 = await workbenchNavigator.openInNewTab(RouterPagePO); + // Open router page as path-based route. + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addView('view.100', {partId: activePartId}) + .navigateView('view.100', ['test-router']), + ); - // WHEN - await routerPage2.view.tab.click(); - await routerPage2.enterPath(''); - await routerPage2.checkClose(true); - await routerPage2.clickNavigateViaRouterLink(); + // Navigate to empty-path route via router link. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.100'}); + await routerPage.navigateViaRouterLink([], { + state: {navigated: 'true'}, + }); - // THEN - await expectView(routerPage1).toBeInactive(); - await expectView(routerPage2).not.toBeAttached(); - await expectView(routerPage3).toBeActive(); - await expect(appPO.views()).toHaveCount(2); + const testeeViewPage = new RouterPagePO(appPO, {viewId: 'view.100'}); + + // Expect view to display empty-path route. + await expectView(testeeViewPage).toBeActive(); + await expect(appPO.views()).toHaveCount(1); + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-router', navigationHint: ''}, + state: {navigated: 'true'}, + } satisfies Partial, + ); }); - test('should close matching views', async ({appPO, workbenchNavigator}) => { + test('should navigate current view when navigating from path-based route to empty-path route (2/2)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // GIVEN - // Open test view 1 (but do not activate it) - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('/test-pages/navigation-test-page/1'); - await routerPage.enterCssClass('testee-1'); - await routerPage.checkActivate(false); - await routerPage.clickNavigate(); - - // Open test view 2 (but do not activate it) - await routerPage.enterPath('/test-pages/navigation-test-page/2'); - await routerPage.enterCssClass('testee-2'); - await routerPage.checkActivate(false); - await routerPage.clickNavigate(); - - // Open test view 3 (but do not activate it) - await routerPage.enterPath('/test-pages/navigation-test-page/3'); - await routerPage.enterCssClass('testee-3'); - await routerPage.checkActivate(false); - await routerPage.clickNavigate(); - - const testViewPage1 = new NavigationTestPagePO(appPO, {cssClass: 'testee-1'}); - const testViewPage2 = new NavigationTestPagePO(appPO, {cssClass: 'testee-2'}); - const testViewPage3 = new NavigationTestPagePO(appPO, {cssClass: 'testee-3'}); + // Open router page as path-based route. + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addView('view.100', {partId: activePartId}) + .navigateView('view.100', ['test-router']), + ); - await expectView(routerPage).toBeActive(); - await expectView(testViewPage1).toBeInactive(); - await expectView(testViewPage2).toBeInactive(); - await expectView(testViewPage3).toBeInactive(); + // Navigate to empty-path route via router link. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.100'}); + await routerPage.navigateViaRouterLink(['/'], {hint: 'test-view'}); - // WHEN - await routerPage.enterPath('/test-pages/navigation-test-page/*'); - await routerPage.checkClose(true); - await routerPage.clickNavigateViaRouterLink(); + const testeeViewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); - // THEN - await expectView(routerPage).toBeActive(); - await expectView(testViewPage1).not.toBeAttached(); - await expectView(testViewPage2).not.toBeAttached(); - await expectView(testViewPage3).not.toBeAttached(); + // Expect view to display empty-path route. + await expectView(testeeViewPage).toBeActive(); + await expect(appPO.views()).toHaveCount(1); + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: '', navigationHint: 'test-view'}, + } satisfies Partial, + ); }); - test('should navigate present view(s) if navigating outside a view and not setting a target', async ({appPO, workbenchNavigator}) => { + test('should navigate current view when navigating from empty-path route to empty-path route (1/2)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // WHEN - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.checkViewContext(false); // simulate navigating outside a view context - await routerPage.clickNavigateViaRouterLink(); + // Open router page as empty-path route. + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addView('view.100', {partId: activePartId}) + .navigateView('view.100', [], {hint: 'test-router'}), + ); - const testViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); + // Navigate to empty-path route via router link. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.100'}); + await routerPage.navigateViaRouterLink([], {hint: 'test-view'}); - // THEN - await expectView(routerPage).toBeInactive(); - await expectView(testViewPage).toBeActive(); - await expect(appPO.views()).toHaveCount(2); + const testeeViewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); + + // Expect view to display empty-path route. + await expectView(testeeViewPage).toBeActive(); + await expect(appPO.views()).toHaveCount(1); + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: '', navigationHint: 'test-view'}, + } satisfies Partial, + ); }); - test('should replace the current view if navigating inside a view (and not activate a matching view)', async ({appPO, workbenchNavigator}) => { + test('should navigate current view when navigating from empty-path route to empty-path route (2/2)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee-1'); - await routerPage.checkActivate(false); - await routerPage.clickNavigate(); + // Open router page as empty-path route. + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addView('view.100', {partId: activePartId}) + .navigateView('view.100', [], {hint: 'test-router'}), + ); - // WHEN - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee-2'); - await routerPage.clickNavigateViaRouterLink(); + // Navigate to empty-path route via router link. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.100'}); + await routerPage.navigateViaRouterLink(['/'], {hint: 'test-view'}); - const testViewPage1 = new ViewPagePO(appPO, {cssClass: 'testee-1'}); - const testViewPage2 = new ViewPagePO(appPO, {cssClass: 'testee-2'}); + const testeeViewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); - // THEN - await expectView(testViewPage1).toBeInactive(); - await expectView(testViewPage2).toBeActive(); - await expect(appPO.views()).toHaveCount(2); + // Expect view to display empty-path route. + await expectView(testeeViewPage).toBeActive(); + await expect(appPO.views()).toHaveCount(1); + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: '', navigationHint: 'test-view'}, + } satisfies Partial, + ); }); - test('should not navigate current view if not the target of primary routes', async ({appPO, workbenchNavigator}) => { + test('should navigate current view when navigating from empty-path route to path-based route', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Add router page to the workbench grid as named view - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}); - await layoutPage.addView('router', {partId: 'left', activateView: true}); - await layoutPage.registerRoute({path: '', component: 'router-page', outlet: 'router'}, {title: 'Workbench Router'}); - await layoutPage.view.tab.close(); + // Open router page as empty-path route. + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addView('view.100', {partId: activePartId}) + .navigateView('view.100', [], {hint: 'test-router'}), + ); - // Navigate in the router page via router link - const routerPage = new RouterPagePO(appPO, {viewId: 'router'}); - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigateViaRouterLink(); + // Navigate to path-based route via router link. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.100'}); + await routerPage.navigateViaRouterLink(['test-view']); - const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); + const testeeViewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); - // Expect the test view to be opened in the main area + // Expect view to display path-based route. await expectView(testeeViewPage).toBeActive(); - await expect(appPO.views({inMainArea: true})).toHaveCount(1); - await expect.poll(() => testeeViewPage.view.part.isInMainArea()).toBe(true); - - // Expect the router page to be still opened in the workbench grid - await expectView(routerPage).toBeActive(); - await expect.poll(() => routerPage.view.part.getPartId()).toEqual('left'); - await expect.poll(() => routerPage.view.part.isInMainArea()).toBe(false); - await expect(appPO.views({inMainArea: false})).toHaveCount(1); + await expect(appPO.views()).toHaveCount(1); + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: ''}, + } satisfies Partial, + ); }); - test('should navigate current view if the target of primary routes', async ({appPO, workbenchNavigator}) => { + test('should update matrix parameters of current view', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Add part to workbench grid - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}); - - // Add router page to the part as unnamed view - { - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('/test-router'); - await routerPage.enterTarget('view.101'); - await routerPage.enterCssClass('router'); - await routerPage.enterBlankPartId('left'); - await routerPage.clickNavigate(); - await routerPage.view.tab.close(); - } - - // Navigate in the router page via router link - const routerPage = new RouterPagePO(appPO, {cssClass: 'router'}); - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigateViaRouterLink(); - - // Expect the test view to replace the router view - const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); - await expectView(testeeViewPage).toBeActive(); - await expectView(routerPage).not.toBeAttached(); - await expect.poll(() => testeeViewPage.view.part.getPartId()).toEqual('left'); - await expect.poll(() => testeeViewPage.view.part.isInMainArea()).toBe(false); - await expect.poll(() => testeeViewPage.view.getViewId()).toEqual('view.101'); + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigateViaRouterLink([{a: 'b', c: 'd'}]); + + await expectView(routerPage).toBeActive(); + await expect.poll(() => routerPage.view.getInfo()).toMatchObject( + { + routeParams: {a: 'b', c: 'd'}, + routeData: {path: 'test-router', navigationHint: ''}, + } satisfies Partial, + ); }); test('should open view in the current part (layout without main area)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Register Angular routes. - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.registerRoute({path: '', component: 'router-page', outlet: 'router'}); - await layoutPage.registerRoute({path: '', component: 'view-page', outlet: 'other'}); - await layoutPage.registerRoute({path: 'testee', component: 'view-page'}); - await layoutPage.view.tab.close(); - - // Register new perspective. - const perspectivePage = await workbenchNavigator.openInNewTab(PerspectivePagePO); - await perspectivePage.registerPerspective({ - id: 'test', - data: { - label: 'test', + await workbenchNavigator.createPerspective(factory => factory + .addPart('left') + .addPart('right', {align: 'right'}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: 'right', activateView: true}), + ); + await workbenchNavigator.modifyLayout(layout => layout + .navigateView('view.101', ['test-router'], {state: {navigated: 'false'}}) + .navigateView('view.102', ['test-view'], {state: {navigated: 'false'}}), + ); + + const view1 = appPO.view({viewId: 'view.101'}); + const view2 = appPO.view({viewId: 'view.102'}); + + // Open test view via router link. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.101'}); + await routerPage.navigateViaRouterLink(['/test-view'], { + state: {navigated: 'true'}, + }); + + // Expect test view to replace the router page + await expect.poll(() => view1.getInfo()).toMatchObject( + { + urlSegments: 'test-view', + state: {navigated: 'true'}, + } satisfies Partial, + ); + + // Expect test view not to replace test view on the right. + await expect.poll(() => view2.getInfo()).toMatchObject( + { + urlSegments: 'test-view', + state: {navigated: 'false'}, + } satisfies Partial, + ); + + await expect(appPO.workbench).toEqualWorkbenchLayout({ + workbenchGrid: { + root: new MTreeNode({ + direction: 'row', + ratio: .5, + child1: new MPart({id: 'left', views: [{id: 'view.101'}], activeViewId: 'view.101'}), + child2: new MPart({id: 'right', views: [{id: 'view.102'}], activeViewId: 'view.102'}), + }), }, - parts: [ - {id: 'left'}, - {id: 'right', align: 'right'}, - ], - views: [ - {id: 'router', partId: 'left', activateView: true}, - {id: 'other', partId: 'right', activateView: true}, - ], }); - await perspectivePage.view.tab.close(); + }); + + test('should open view in main area (view is in peripheral area, target="blank")', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); - // Switch to the newly created perspective. - await appPO.switchPerspective('test'); + await workbenchNavigator.createPerspective(factory => factory + .addPart(MAIN_AREA) + .addPart('left', {align: 'left'}) + .addView('view.101', {partId: 'left', activateView: true}), + ); + + // Add state via separate navigation as not supported when adding views to the perspective. + await workbenchNavigator.modifyLayout(layout => layout + .navigateView('view.101', ['test-router'], {state: {navigated: 'false'}}), + ); + + const testView = appPO.view({viewId: 'view.1'}); + + // Open test view via router link. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.101'}); + await routerPage.navigateViaRouterLink(['/test-view'], { + target: 'blank', + state: {navigated: 'true'}, + }); + + // Expect test view to be opened + await expect.poll(() => testView.getInfo()).toMatchObject( + { + viewId: 'view.1', + urlSegments: 'test-view', + state: {navigated: 'true'}, + } satisfies Partial, + ); + + // Expect router page not to be replaced + await expect.poll(() => routerPage.view.getInfo()).toMatchObject( + { + viewId: 'view.101', + urlSegments: 'test-router', + state: {navigated: 'false'}, + } satisfies Partial, + ); - // Expect layout to match the perspective definition. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ direction: 'row', ratio: .5, - child1: new MPart({id: 'left', views: [{id: 'router'}], activeViewId: 'router'}), - child2: new MPart({id: 'right', views: [{id: 'other'}], activeViewId: 'other'}), + child1: new MPart({id: 'left', views: [{id: 'view.101'}], activeViewId: 'view.101'}), + child2: new MPart({id: MAIN_AREA}), + }), + }, + mainAreaGrid: { + root: new MPart({ + views: [{id: 'view.1'}], + activeViewId: 'view.1', }), }, }); + }); + + test('should open view in main area (view is in peripheral area, target=viewId)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); - // Open new view via workbench router link. - const routerPage = new RouterPagePO(appPO, {viewId: 'router'}); - await routerPage.enterPath('/testee'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigateViaRouterLink(); + await workbenchNavigator.createPerspective(factory => factory + .addPart(MAIN_AREA) + .addPart('left', {align: 'left'}) + .addView('view.101', {partId: 'left', activateView: true}), + ); - // Expect new view to be opened. - const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); - await expectView(testeeViewPage).toBeActive(); + // Add state via separate navigation as not supported when adding views to the perspective. + await workbenchNavigator.modifyLayout(layout => layout + .navigateView('view.101', ['test-router'], {state: {navigated: 'false'}}), + ); + + const testView = appPO.view({viewId: 'view.102'}); + + // Open test view via router link. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.101'}); + await routerPage.navigateViaRouterLink(['/test-view'], { + target: 'view.102', + state: {navigated: true}, + }); + + // Expect test view to be opened + await expect.poll(() => testView.getInfo()).toMatchObject( + { + viewId: 'view.102', + urlSegments: 'test-view', + state: {navigated: 'true'}, + } satisfies Partial, + ); + + // Expect router page not to be replaced + await expect.poll(() => routerPage.view.getInfo()).toMatchObject( + { + viewId: 'view.101', + urlSegments: 'test-router', + state: {navigated: 'false'}, + } satisfies Partial, + ); - // Expect new view to be opened in active part of the contextual view i.e. left - const testeeViewId = await testeeViewPage.view.getViewId(); await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ direction: 'row', ratio: .5, - child1: new MPart({id: 'left', views: [{id: 'router'}, {id: testeeViewId}], activeViewId: testeeViewId}), - child2: new MPart({id: 'right', views: [{id: 'other'}], activeViewId: 'other'}), + child1: new MPart({id: 'left', views: [{id: 'view.101'}], activeViewId: 'view.101'}), + child2: new MPart({id: MAIN_AREA}), + }), + }, + mainAreaGrid: { + root: new MPart({ + views: [{id: 'view.102'}], + activeViewId: 'view.102', }), }, }); diff --git a/projects/scion/e2e-testing/src/workbench/router.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/router.e2e-spec.ts index 9dd7bd04f..63d55e52a 100644 --- a/projects/scion/e2e-testing/src/workbench/router.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/router.e2e-spec.ts @@ -12,11 +12,11 @@ import {expect} from '@playwright/test'; import {test} from '../fixtures'; import {RouterPagePO} from './page-object/router-page.po'; import {ViewPagePO} from './page-object/view-page.po'; -import {LayoutPagePO} from './page-object/layout-page.po'; import {MPart, MTreeNode} from '../matcher/to-equal-workbench-layout.matcher'; import {MAIN_AREA} from '../workbench.model'; import {expectView} from '../matcher/view-matcher'; import {NavigationTestPagePO} from './page-object/test-pages/navigation-test-page.po'; +import {ViewInfo} from './page-object/view-info-dialog.po'; test.describe('Workbench Router', () => { @@ -25,11 +25,9 @@ test.describe('Workbench Router', () => { // open test view 1 const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterMatrixParams({param: '1'}); - await routerPage.checkActivate(true); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: '1'}], { + target: 'view.101', + }); const testeeViewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); await expectView(testeeViewPage).toBeActive(); @@ -37,22 +35,14 @@ test.describe('Workbench Router', () => { // open test view 2 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterMatrixParams({param: '2'}); - await routerPage.checkActivate(true); - await routerPage.enterTarget('auto'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: '2'}]); await expectView(testeeViewPage).toBeActive(); await expect(appPO.views()).toHaveCount(2); // open test view 3 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterMatrixParams({param: '3'}); - await routerPage.checkActivate(true); - await routerPage.enterTarget('auto'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: '3'}]); await expectView(testeeViewPage).toBeActive(); await expect(appPO.views()).toHaveCount(2); @@ -63,10 +53,9 @@ test.describe('Workbench Router', () => { // open test view 1 const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterMatrixParams({param: '1'}); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: '1'}], { + target: 'view.101', + }); const testee1ViewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); await expectView(testee1ViewPage).toBeActive(); @@ -74,10 +63,9 @@ test.describe('Workbench Router', () => { // open test view 2 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterMatrixParams({param: '2'}); - await routerPage.enterTarget('view.102'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: '2'}], { + target: 'view.102', + }); const testee2ViewPage = new ViewPagePO(appPO, {viewId: 'view.102'}); await expectView(testee2ViewPage).toBeActive(); @@ -85,10 +73,9 @@ test.describe('Workbench Router', () => { // open test view 3 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterMatrixParams({param: '3'}); - await routerPage.enterTarget('view.103'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: '3'}], { + target: 'view.103', + }); const testee3ViewPage = new ViewPagePO(appPO, {viewId: 'view.103'}); await expectView(testee3ViewPage).toBeActive(); @@ -96,46 +83,14 @@ test.describe('Workbench Router', () => { // close all test views await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget(undefined); - await routerPage.enterMatrixParams({param: '1'}); // matrix param is ignored when closing views - await routerPage.checkClose(true); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: '1'}], { // matrix param is ignored when closing views + close: true, + }); await expectView(routerPage).toBeActive(); await expect(appPO.views()).toHaveCount(1); }); - test('should show title of inactive views when reloading the application', async ({appPO, workbenchNavigator}) => { - await appPO.navigateTo({microfrontendSupport: false}); - - // open test view 1 - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterMatrixParams({title: 'view-1-title'}); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); - - // open test view 2 - await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('view.102'); - await routerPage.enterMatrixParams({title: 'view-2-title'}); - await routerPage.clickNavigate(); - - const testee1ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.101'}); - const testee2ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.102'}); - - // reload the application - await appPO.reload(); - - await expectView(testee1ViewPage).toBeInactive(); - await expect(testee1ViewPage.view.tab.title).toHaveText('view-1-title'); - - await expectView(testee2ViewPage).toBeActive(); - await expect(testee2ViewPage.view.tab.title).toHaveText('view-2-title'); - }); - test('should not throw outlet activation error when opening a new view tab once a view tab was closed', async ({appPO, consoleLogs}) => { await appPO.navigateTo({microfrontendSupport: false}); @@ -205,9 +160,9 @@ test.describe('Workbench Router', () => { // close all test views via router await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.checkClose(true); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + close: true, + }); await expect(appPO.views()).toHaveCount(1); }); @@ -218,16 +173,15 @@ test.describe('Workbench Router', () => { // open the test view in a new view tab const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('blank'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'view.101', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('view.102'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'view.102', + }); const testee1ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.101'}); const testee2ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.102'}); @@ -237,10 +191,10 @@ test.describe('Workbench Router', () => { // close the view 1 await routerPage.view.tab.click(); - await routerPage.enterPath(''); - await routerPage.enterTarget('view.101'); - await routerPage.checkClose(true); - await routerPage.clickNavigate(); + await routerPage.navigate([], { + target: 'view.101', + close: true, + }); // expect the view 1 to be closed await expect(appPO.views()).toHaveCount(2); @@ -248,78 +202,60 @@ test.describe('Workbench Router', () => { await expectView(testee2ViewPage).toBeInactive(); }); - test('should ignore closing a view with an unknown viewId via router', async ({appPO, workbenchNavigator}) => { + test('should error when trying to close a view that does not exist', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); // open the test view in a new view tab const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('testee-1'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'blank', + cssClass: 'testee-1', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('testee-2'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'blank', + cssClass: 'testee-2', + }); // expect the test views to be opened await expect(appPO.views()).toHaveCount(3); - // close the unknown view 99 + // try closing view 100 await routerPage.view.tab.click(); - await routerPage.enterPath(''); - await routerPage.enterTarget('view.99'); - await routerPage.checkClose(true); - await routerPage.clickNavigate(); - - const testee1ViewPage = new NavigationTestPagePO(appPO, {cssClass: 'testee-1'}); - const testee2ViewPage = new NavigationTestPagePO(appPO, {cssClass: 'testee-2'}); - - // expect no view to be closed - await expect(appPO.views()).toHaveCount(3); - await expectView(routerPage).toBeActive(); - await expectView(testee1ViewPage).toBeInactive(); - await expectView(testee2ViewPage).toBeInactive(); + await expect(routerPage.navigate([], { + target: 'view.100', + close: true, + })).rejects.toThrow(/NullViewError/); }); test('should reject closing a view by viewId via router if a path is also given', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // open the test view in a new view tab const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); - - // open the test view in a new view tab - await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('view.102'); - await routerPage.clickNavigate(); - - // expect the test views to be opened - await expect(appPO.views()).toHaveCount(3); - - // try closing view by providing viewId and path - await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('view.101'); - await routerPage.checkClose(true); - - // expect closing to be rejected - await expect(routerPage.clickNavigate()).rejects.toThrow(/\[WorkbenchRouterError]\[IllegalArgumentError]/); - - const testee1ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.101'}); - const testee2ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.102'}); + await routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'view.100', + activate: false, + }); - await expect(appPO.views()).toHaveCount(3); - await expectView(testee1ViewPage).toBeInactive(); - await expectView(testee2ViewPage).toBeInactive(); + // Expect to error when passing viewId and path + await expect(routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'view.100', + close: true, + })).rejects.toThrow(/\[NavigateError]/); + + // Expect to error when passing viewId and hint + await expect(routerPage.navigate([], { + target: 'view.100', + hint: 'hint', + close: true, + })).rejects.toThrow(/\[NavigateError]/); + + // Expect view not to be closed. + const testeeViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.100'}); + await expectView(testeeViewPage).toBeInactive(); }); test('should allow closing all views matching the path `test-pages/navigation-test-page` via router', async ({appPO, workbenchNavigator}) => { @@ -328,31 +264,30 @@ test.describe('Workbench Router', () => { // open the test view in a new view tab const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'view.101', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1'); - await routerPage.enterTarget('view.102'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1'], { + target: 'view.102', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('view.103'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'view.103', + }); // expect the test views to be opened await expect(appPO.views()).toHaveCount(4); // close the views with the path 'test-pages/navigation-test-page' await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget(undefined); - await routerPage.checkClose(true); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page'], { + close: true, + }); const testee1ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.101'}); const testee2ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.102'}); @@ -372,31 +307,30 @@ test.describe('Workbench Router', () => { // open the test view in a new view tab const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'view.101', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1'); - await routerPage.enterTarget('view.102'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1'], { + target: 'view.102', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1'); - await routerPage.enterTarget('view.103'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1'], { + target: 'view.103', + }); // expect the test views to be opened await expect(appPO.views()).toHaveCount(4); // close the views with the path 'test-pages/navigation-test-page/1' await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1'); - await routerPage.enterTarget(undefined); - await routerPage.checkClose(true); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1'], { + close: true, + }); const testee1ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.101'}); const testee2ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.102'}); @@ -416,31 +350,30 @@ test.describe('Workbench Router', () => { // open the test view in a new view tab const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'view.101', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1'); - await routerPage.enterTarget('view.102'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1'], { + target: 'view.102', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/2'); - await routerPage.enterTarget('view.103'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/2'], { + target: 'view.103', + }); // expect the test views to be opened await expect(appPO.views()).toHaveCount(4); // close the views with the path 'test-pages/navigation-test-page/*' await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/*'); - await routerPage.enterTarget(undefined); - await routerPage.checkClose(true); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/*'], { + close: true, + }); const testee1ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.101'}); const testee2ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.102'}); @@ -460,49 +393,48 @@ test.describe('Workbench Router', () => { // open the test view in a new view tab const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'view.101', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1'); - await routerPage.enterTarget('view.102'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1'], { + target: 'view.102', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1/1'); - await routerPage.enterTarget('view.103'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1/1'], { + target: 'view.103', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1/2'); - await routerPage.enterTarget('view.104'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1/2'], { + target: 'view.104', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/2/1'); - await routerPage.enterTarget('view.105'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/2/1'], { + target: 'view.105', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/2/2'); - await routerPage.enterTarget('view.106'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/2/2'], { + target: 'view.106', + }); // expect the test views to be opened await expect(appPO.views()).toHaveCount(7); // close the views with the path 'test-pages/navigation-test-page/1/1' await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1/1'); - await routerPage.enterTarget(undefined); - await routerPage.checkClose(true); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1/1'], { + close: true, + }); const testee1ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.101'}); const testee2ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.102'}); @@ -528,49 +460,48 @@ test.describe('Workbench Router', () => { // open the test view in a new view tab const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'view.101', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1'); - await routerPage.enterTarget('view.102'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1'], { + target: 'view.102', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1/1'); - await routerPage.enterTarget('view.103'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1/1'], { + target: 'view.103', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1/2'); - await routerPage.enterTarget('view.104'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1/2'], { + target: 'view.104', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/2/1'); - await routerPage.enterTarget('view.105'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/2/1'], { + target: 'view.105', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/2/2'); - await routerPage.enterTarget('view.106'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/2/2'], { + target: 'view.106', + }); // expect the test views to be opened await expect(appPO.views()).toHaveCount(7); // close the views with the path 'test-pages/navigation-test-page/*/1' await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/*/1'); - await routerPage.enterTarget(undefined); - await routerPage.checkClose(true); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/*/1'], { + close: true, + }); const testee1ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.101'}); const testee2ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.102'}); @@ -596,49 +527,48 @@ test.describe('Workbench Router', () => { // open the test view in a new view tab const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page'); - await routerPage.enterTarget('view.101'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page'], { + target: 'view.101', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1'); - await routerPage.enterTarget('view.102'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1'], { + target: 'view.102', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1/1'); - await routerPage.enterTarget('view.103'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1/1'], { + target: 'view.103', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/1/2'); - await routerPage.enterTarget('view.104'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1/2'], { + target: 'view.104', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/2/1'); - await routerPage.enterTarget('view.105'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/2/1'], { + target: 'view.105', + }); // open the test view in a new view tab await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/2/2'); - await routerPage.enterTarget('view.106'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/2/2'], { + target: 'view.106', + }); // expect the test views to be opened await expect(appPO.views()).toHaveCount(7); // close the views with the path 'test-pages/navigation-test-page/*/*' await routerPage.view.tab.click(); - await routerPage.enterPath('test-pages/navigation-test-page/*/*'); - await routerPage.enterTarget(undefined); - await routerPage.checkClose(true); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/*/*'], { + close: true, + }); const testee1ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.101'}); const testee2ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.102'}); @@ -663,10 +593,10 @@ test.describe('Workbench Router', () => { // navigate to the test view const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + cssClass: 'testee', + }); const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); @@ -685,10 +615,10 @@ test.describe('Workbench Router', () => { const routerViewId = await routerPage.view.getViewId(); // navigate to the test view - await routerPage.enterPath('test-view'); - await routerPage.enterTarget(routerViewId); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: routerViewId, + cssClass: 'testee', + }); const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); @@ -705,10 +635,10 @@ test.describe('Workbench Router', () => { // navigate to the test view const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee-1'); - await routerPage.enterTarget('blank'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + cssClass: 'testee-1', + }); const testee1ViewPage = new ViewPagePO(appPO, {cssClass: 'testee-1'}); @@ -719,10 +649,10 @@ test.describe('Workbench Router', () => { // navigate to a new test view await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee-2'); - await routerPage.enterTarget('blank'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + cssClass: 'testee-2', + }); const testee2ViewPage = new ViewPagePO(appPO, {cssClass: 'testee-2'}); @@ -733,14 +663,182 @@ test.describe('Workbench Router', () => { await expectView(testee2ViewPage).toBeActive(); }); - test('should activate a present view if setting `checkActivate` to `true`', async ({appPO, workbenchNavigator}) => { + test('should open view in the specified part', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Add part on the right. + await workbenchNavigator.modifyLayout(layout => layout.addPart('right', {align: 'right'})); + + // Open view in the right part. + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-view'], { + target: 'blank', + partId: 'right', + cssClass: 'testee', + }); + + // Expect view to be opened in the right part. + const view = appPO.view({cssClass: 'testee'}); + await expect.poll(() => view.getInfo()).toMatchObject( + { + urlSegments: 'test-view', + partId: 'right', + } satisfies Partial, + ); + await expect(appPO.views()).toHaveCount(2); + }); + + test('should navigate existing view(s) in the specified part, or open a new view in the specified part otherwise', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Add left and right part. + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left'}) + .addPart('right', {align: 'right'}) + .addView('view.100', {partId: 'left'}) + .navigateView('view.100', ['test-view']), + ); + + // Navigate view in the left part. + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-view'], { + partId: 'left', + state: {navigated: '1'}, + cssClass: 'testee-1', + }); + + // Expect existing view in the left part to be navigated. + const view1 = appPO.view({cssClass: 'testee-1'}); + await expect.poll(() => view1.getInfo()).toMatchObject( + { + viewId: 'view.100', + urlSegments: 'test-view', + state: {navigated: '1'}, + partId: 'left', + } satisfies Partial, + ); + await expect(appPO.views()).toHaveCount(2); + + // Navigate view in the right part. + await routerPage.navigate(['test-view'], { + partId: 'right', + state: {navigated: '2'}, + cssClass: 'testee-2', + }); + + // Expect view in the left part not to be navigated. + await expect.poll(() => view1.getInfo()).toMatchObject( + { + viewId: 'view.100', + urlSegments: 'test-view', + state: {navigated: '1'}, + partId: 'left', + } satisfies Partial, + ); + + // Expect new view to be opened in the right part. + const view2 = appPO.view({cssClass: 'testee-2'}); + await expect.poll(() => view2.getInfo()).toMatchObject( + { + urlSegments: 'test-view', + state: {navigated: '2'}, + partId: 'right', + } satisfies Partial, + ); + await expect(appPO.views()).toHaveCount(3); + }); + + test('should open view in the active part of the main area if specified part is not in the layout', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Add left part. + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left'}) + .addView('view.100', {partId: 'left', activatePart: true, activateView: true}) + .navigateView('view.100', ['test-view'], {state: {navigated: 'false'}}), + ); + + // Open view in a part not contained in the layout. + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-view'], { + partId: 'does-not-exist', + state: {navigated: 'true'}, + cssClass: 'testee', + }); + + // Expect view to be opened in the main area. + await expect.poll(() => appPO.view({cssClass: 'testee'}).getInfo()).toMatchObject( + { + urlSegments: 'test-view', + partId: await appPO.activePart({inMainArea: true}).getPartId(), + state: {navigated: 'true'}, + } satisfies Partial, + ); + + // Expect view in the left part not to be navigated. + await expect.poll(() => appPO.view({viewId: 'view.100'}).getInfo()).toMatchObject( + { + viewId: 'view.100', + urlSegments: 'test-view', + partId: 'left', + state: {navigated: 'false'}, + } satisfies Partial, + ); + await expect(appPO.views()).toHaveCount(3); + }); + + test('should open view in the active part of the workbench grid if specified part is not in the layout', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Create perspective with a left and right part. + await workbenchNavigator.createPerspective(factory => factory + .addPart('left') + .addPart('right', {align: 'right'}) + .addView('router', {partId: 'left', cssClass: 'router'}) + .addView('view.100', {partId: 'right', activateView: true}) + .navigateView('router', ['test-router']), + ); + + // Add state via separate navigation as not supported when adding views to the perspective. + await workbenchNavigator.modifyLayout(layout => layout.navigateView('view.100', ['test-view'], {state: {navigated: 'false'}})); + + // Open view in a part not contained in the layout. + const routerPage = new RouterPagePO(appPO, {cssClass: 'router'}); + await routerPage.navigate(['test-view'], { + partId: 'does-not-exist', + state: {navigated: 'true'}, + cssClass: 'testee', + }); + + // Expect view to be opened in the active part. + await expect.poll(() => appPO.view({cssClass: 'testee'}).getInfo()).toMatchObject( + { + urlSegments: 'test-view', + partId: await appPO.activePart({inMainArea: false}).getPartId(), + state: {navigated: 'true'}, + } satisfies Partial, + ); + + // Expect view in the right part not to be navigated. + await expect.poll(() => appPO.view({viewId: 'view.100'}).getInfo()).toMatchObject( + { + viewId: 'view.100', + urlSegments: 'test-view', + partId: 'right', + state: {navigated: 'false'}, + } satisfies Partial, + ); + await expect(appPO.views()).toHaveCount(3); + }); + + test('should activate a present view if setting `activate` to `true`', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); // navigate to the test view const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.100'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.100', + }); const testeeViewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); @@ -751,10 +849,9 @@ test.describe('Workbench Router', () => { // activate the view via routing await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('auto'); - await routerPage.checkActivate(true); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + activate: true, + }); // expect the view to be activated and no new view to be opened await expect(appPO.views()).toHaveCount(2); @@ -768,11 +865,10 @@ test.describe('Workbench Router', () => { const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); // navigate to the test view - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee-1'); - await routerPage.checkActivate(true); // activate the view - await routerPage.enterTarget('blank'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + cssClass: 'testee-1', + }); const testee1ViewPage = new ViewPagePO(appPO, {cssClass: 'testee-1'}); @@ -783,11 +879,11 @@ test.describe('Workbench Router', () => { // navigate to a new test view await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee-2'); - await routerPage.enterTarget('blank'); - await routerPage.checkActivate(false); // do not activate the view - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + activate: false, + cssClass: 'testee-2', + }); const testee2ViewPage = new ViewPagePO(appPO, {cssClass: 'testee-2'}); @@ -798,37 +894,14 @@ test.describe('Workbench Router', () => { await expectView(testee2ViewPage).toBeInactive(); }); - test('should not destroy the component of the view when it is inactivated', async ({appPO, workbenchNavigator}) => { - await appPO.navigateTo({microfrontendSupport: false}); - - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - - const componentInstanceId = await viewPage.getComponentInstanceId(); - - // activate the router test view - await routerPage.view.tab.click(); - await expectView(routerPage).toBeActive(); - await expectView(viewPage).toBeInactive(); - - // activate the test view - await viewPage.view.tab.click(); - await expectView(viewPage).toBeActive(); - await expectView(routerPage).toBeInactive(); - - // expect the component not to be constructed anew - await expect.poll(() => viewPage.getComponentInstanceId()).toEqual(componentInstanceId); - }); - test('should open a new view if no present view can be found [target=auto]', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); // navigate to the test view const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.enterTarget('auto'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + cssClass: 'testee', + }); const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); @@ -843,9 +916,9 @@ test.describe('Workbench Router', () => { // navigate to the test view const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.100'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.100', + }); const testee1ViewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); @@ -856,9 +929,9 @@ test.describe('Workbench Router', () => { // activate the router test view await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('auto'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'auto', + }); // expect the present view to be activated await expect(appPO.views()).toHaveCount(2); @@ -871,10 +944,9 @@ test.describe('Workbench Router', () => { // navigate to the test view 1 const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.101'); - await routerPage.enterMatrixParams({param: 'value1'}); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: 'value1'}], { + target: 'view.101', + }); const testee1ViewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); @@ -883,10 +955,9 @@ test.describe('Workbench Router', () => { // navigate to the test view 2 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.102'); - await routerPage.enterMatrixParams({param: 'value1'}); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: 'value1'}], { + target: 'view.102', + }); const testee2ViewPage = new ViewPagePO(appPO, {viewId: 'view.102'}); @@ -895,10 +966,9 @@ test.describe('Workbench Router', () => { // update all matching present views await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('auto'); - await routerPage.enterMatrixParams({param: 'value2'}); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: 'value2'}], { + target: 'auto', + }); // expect the present views to be updated await expect(appPO.views()).toHaveCount(3); @@ -915,9 +985,9 @@ test.describe('Workbench Router', () => { // navigate to the test view const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + cssClass: 'testee', + }); const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); @@ -932,9 +1002,9 @@ test.describe('Workbench Router', () => { // navigate to the test view const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.100'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.100', + }); const testeeView = new ViewPagePO(appPO, {viewId: 'view.100'}); @@ -945,9 +1015,7 @@ test.describe('Workbench Router', () => { // navigate to a present view await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget(''); // will be interpreted as undefined - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view']); // expect the present view to be activated await expect(appPO.views()).toHaveCount(2); @@ -956,10 +1024,7 @@ test.describe('Workbench Router', () => { // navigate to a present view updating its matrix params await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget(''); // will be interpreted as undefined - await routerPage.enterMatrixParams({param: 'value1'}); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: 'value1'}]); // expect the present view to be updated await expect(appPO.views()).toHaveCount(2); @@ -973,10 +1038,9 @@ test.describe('Workbench Router', () => { // navigate to the test view 1 const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.101'); - await routerPage.enterMatrixParams({param: 'value1'}); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: 'value1'}], { + target: 'view.101', + }); const testee1ViewPage = new ViewPagePO(appPO, {viewId: 'view.101'}); @@ -985,10 +1049,9 @@ test.describe('Workbench Router', () => { // navigate to the test view 2 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.102'); - await routerPage.enterMatrixParams({param: 'value1'}); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: 'value1'}], { + target: 'view.102', + }); const testee2ViewPage = new ViewPagePO(appPO, {viewId: 'view.102'}); @@ -997,10 +1060,7 @@ test.describe('Workbench Router', () => { // update all matching present views await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget(''); - await routerPage.enterMatrixParams({param: 'value2'}); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {param: 'value2'}]); // expect the present views to be updated await expect(appPO.views()).toHaveCount(3); @@ -1017,9 +1077,9 @@ test.describe('Workbench Router', () => { // navigate to the test view const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.99'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.99', + }); const testee1ViewPage = new ViewPagePO(appPO, {viewId: 'view.99'}); @@ -1029,20 +1089,314 @@ test.describe('Workbench Router', () => { await expectView(testee1ViewPage).toBeActive(); }); - test('should support app URL to contain view outlets of views in the workbench grid', async ({appPO, workbenchNavigator, page}) => { - await appPO.navigateTo({microfrontendSupport: false, perspectives: ['perspective']}); + test('should navigate views of the same path and hint [target=auto] (1/4)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right-top', {relativeTo: MAIN_AREA, align: 'right'}) + .addPart('right-bottom', {relativeTo: 'right-top', align: 'bottom'}) + .addView('view.101', {partId: 'right-top', activateView: true}) + .addView('view.102', {partId: 'right-bottom', activateView: true}) + .navigateView('view.101', [], {hint: 'test-view', state: {navigated: 'false'}}) + .navigateView('view.102', [], {hint: 'test-router', state: {navigated: 'false'}}), + ); - // Define perspective with a part on the left. - await appPO.switchPerspective('perspective'); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {align: 'left', ratio: .25}); + const testView1 = appPO.view({viewId: 'view.101'}); + const testView2 = appPO.view({viewId: 'view.102'}); - // Add view to the left part in the workbench grid. + // Navigate to empty-path route with hint 'test-view'. const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterBlankPartId('left'); - await routerPage.clickNavigate(); + await routerPage.navigate([], { + target: 'auto', + hint: 'test-view', + state: {navigated: 'true'}, + }); + + // Expect view.101 to be navigated. + await expect.poll(() => testView1.getInfo()).toMatchObject( + { + routeData: {path: '', navigationHint: 'test-view'}, + state: {navigated: 'true'}, + } satisfies Partial, + ); + // Expect view.102 not to be navigated. + await expect.poll(() => testView2.getInfo()).toMatchObject( + { + routeData: {path: '', navigationHint: 'test-router'}, + state: {navigated: 'false'}, + } satisfies Partial, + ); + }); + + test('should navigate views of the same path and hint [target=auto] (2/4)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right-top', {relativeTo: MAIN_AREA, align: 'right'}) + .addPart('right-bottom', {relativeTo: 'right-top', align: 'bottom'}) + .addView('view.101', {partId: 'right-top', activateView: true}) + .addView('view.102', {partId: 'right-bottom', activateView: true}) + .navigateView('view.101', ['test-view'], {hint: 'test-view', state: {navigated: 'false'}}) + .navigateView('view.102', ['test-view'], {state: {navigated: 'false'}}), + ); + + const testView1 = appPO.view({viewId: 'view.101'}); + const testView2 = appPO.view({viewId: 'view.102'}); + + // Navigate to 'test-view' route without hint. + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-view'], { + target: 'auto', + state: {navigated: 'true'}, + }); + + // Expect view.101 not to be navigated. + await expect.poll(() => testView1.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: 'test-view'}, + state: {navigated: 'false'}, + } satisfies Partial, + ); + // Expect view.102 to be navigated. + await expect.poll(() => testView2.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: ''}, + state: {navigated: 'true'}, + } satisfies Partial, + ); + }); + + test('should navigate views of the same path and hint [target=auto] (3/4)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right-top', {relativeTo: MAIN_AREA, align: 'right'}) + .addPart('right-bottom', {relativeTo: 'right-top', align: 'bottom'}) + .addView('view.101', {partId: 'right-top', activateView: true}) + .addView('view.102', {partId: 'right-bottom', activateView: true}) + .navigateView('view.101', ['test-view'], {hint: 'test-view', state: {navigated: 'false'}}) + .navigateView('view.102', ['test-view'], {hint: 'test-view', state: {navigated: 'false'}}), + ); + + const testView1 = appPO.view({viewId: 'view.101'}); + const testView2 = appPO.view({viewId: 'view.102'}); + + // Navigate to 'test-view' route without hint. + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-view'], { + target: 'auto', + hint: 'test-view', + state: {navigated: 'true'}, + }); + + // Expect view.101 to be navigated. + await expect.poll(() => testView1.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: 'test-view'}, + state: {navigated: 'true'}, + } satisfies Partial, + ); + // Expect view.102 to be navigated. + await expect.poll(() => testView2.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: 'test-view'}, + state: {navigated: 'true'}, + } satisfies Partial, + ); + }); + + test('should navigate views of the same path and hint [target=auto] (4/4)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: MAIN_AREA, align: 'right'}) + .addView('view.101', {partId: 'right', activateView: true}) + .navigateView('view.101', [], {hint: 'test-router', state: {navigated: 'false'}}), + ); + + const testView1 = appPO.view({viewId: 'view.101'}); + const testView2 = appPO.view({cssClass: 'testee'}); + + // Navigate to empty-path route with hint 'test-view'. + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate([], { + target: 'auto', + hint: 'test-view', + state: {navigated: 'true'}, + cssClass: 'testee', + }); + + // Expect view.101 not to be navigated. + await expect.poll(() => testView1.getInfo()).toMatchObject( + { + routeData: {path: '', navigationHint: 'test-router'}, + state: {navigated: 'false'}, + } satisfies Partial, + ); + // Expect testee to be navigated. + await expect.poll(() => testView2.getInfo()).toMatchObject( + { + routeData: {path: '', navigationHint: 'test-view'}, + state: {navigated: 'true'}, + } satisfies Partial, + ); + }); + + test('should close views of the same path and hint [target=auto] (1/4)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right-top', {relativeTo: MAIN_AREA, align: 'right'}) + .addPart('right-bottom', {relativeTo: 'right-top', align: 'bottom'}) + .addView('view.101', {partId: 'right-top', activateView: true}) + .addView('view.102', {partId: 'right-bottom', activateView: true}) + .navigateView('view.101', [], {hint: 'test-view'}) + .navigateView('view.102', [], {hint: 'test-router'}), + ); + + const testView1 = new ViewPagePO(appPO, {viewId: 'view.101'}); + const testView2 = new RouterPagePO(appPO, {viewId: 'view.102'}); + + await expectView(testView1).toBeActive(); + await expectView(testView2).toBeActive(); + + // Close views navigated to empty-path route with hint 'test-view'. + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate([], { + hint: 'test-view', + close: true, + }); + + // Expect view.101 to be closed. + await expectView(testView1).not.toBeAttached(); + // Expect view.102 not to be closed. + await expectView(testView2).toBeActive(); + }); + + test('should close views of the same path and hint [target=auto] (2/4)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right-top', {relativeTo: MAIN_AREA, align: 'right'}) + .addPart('right-bottom', {relativeTo: 'right-top', align: 'bottom'}) + .addView('view.101', {partId: 'right-top', activateView: true}) + .addView('view.102', {partId: 'right-bottom', activateView: true}) + .navigateView('view.101', ['test-view'], {hint: 'test-view'}) + .navigateView('view.102', ['test-view']), + ); + + const testView1 = new ViewPagePO(appPO, {viewId: 'view.101'}); + const testView2 = new ViewPagePO(appPO, {viewId: 'view.102'}); + + await expectView(testView1).toBeActive(); + await expectView(testView2).toBeActive(); + + // Close views navigated to 'test-view' route without hint. + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-view'], { + close: true, + }); + + // Expect view.101 not to be closed. + await expectView(testView1).toBeActive(); + // Expect view.102 to be closed. + await expectView(testView2).not.toBeAttached(); + }); + + test('should close views of the same path and hint [target=auto] (3/4)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right-top', {relativeTo: MAIN_AREA, align: 'right'}) + .addPart('right-bottom', {relativeTo: 'right-top', align: 'bottom'}) + .addView('view.101', {partId: 'right-top', activateView: true}) + .addView('view.102', {partId: 'right-bottom', activateView: true}) + .navigateView('view.101', ['test-view'], {hint: 'test-view'}) + .navigateView('view.102', ['test-view'], {hint: 'test-view'}), + ); + + const testView1 = new ViewPagePO(appPO, {viewId: 'view.101'}); + const testView2 = new ViewPagePO(appPO, {viewId: 'view.102'}); + + await expectView(testView1).toBeActive(); + await expectView(testView2).toBeActive(); + + // Close views navigated to 'test-view' route without hint. + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-view'], { + hint: 'test-view', + close: true, + }); + + // Expect view.101 to be closed. + await expectView(testView1).not.toBeAttached(); + // Expect view.102 to be closed. + await expectView(testView2).not.toBeAttached(); + }); + + test('should close views of the same path and hint [target=auto] (4/4)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: MAIN_AREA, align: 'right'}) + .addView('view.101', {partId: 'right', activateView: true}) + .navigateView('view.101', [], {hint: 'test-router'}), + ); + + const testView1 = new RouterPagePO(appPO, {viewId: 'view.101'}); + + // Close views navigated to empty-path route with hint 'test-view'. + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate([], { + hint: 'test-view', + close: true, + }); + + // Expect view.101 not to be closed. + await expectView(testView1).toBeActive(); + }); + + test('should close views in the specified part', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Add left and right part. + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left'}) + .addPart('right', {align: 'right'}) + .addView('view.101', {partId: 'left'}) + .addView('view.102', {partId: 'left'}) + .addView('view.103', {partId: 'left'}) + .addView('view.104', {partId: 'right'}) + .navigateView('view.101', ['test-view/1']) + .navigateView('view.102', ['test-view/1']) + .navigateView('view.103', ['test-view/2']) + .navigateView('view.104', ['test-view/1']), + ); + + // Close views in the left part + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-view/1'], { + partId: 'left', + close: true, + }); + + // Expect views in the left part to be closed. + await expect.poll(() => appPO.part({partId: 'left'}).getViewIds()).toEqual(['view.103']); + + // Expect view in the right part not to be closed. + await expect.poll(() => appPO.part({partId: 'right'}).getViewIds()).toEqual(['view.104']); + }); + + test('should support app URL to contain view outlets of views in the workbench grid', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left', ratio: .25}) + .addView('view.100', {partId: 'left', activateView: true}) + .navigateView('view.100', ['test-view']), + ); + + const viewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); // Expect the view to be opened in the left part. await expect(appPO.workbench).toEqualWorkbenchLayout({ @@ -1052,30 +1406,16 @@ test.describe('Workbench Router', () => { ratio: .25, child1: new MPart({ id: 'left', - views: [{id: 'view.3'}], // test view page - activeViewId: 'view.3', + views: [{id: 'view.100'}], + activeViewId: 'view.100', }), child2: new MPart({id: MAIN_AREA}), }), }, - mainAreaGrid: { - root: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}, {id: 'view.2'}], // layout page, router page - activeViewId: 'view.2', - }), - activePartId: await layoutPage.view.part.getPartId(), - }, }); - // Capture current URL. - const url = page.url(); - - // Clear the browser URL. - await page.goto('about:blank'); - // WHEN: Opening the app with a URL that contains view outlets of views from the workbench grid - await appPO.navigateTo({url, microfrontendSupport: false, perspectives: ['perspective']}); + await appPO.reload(); // THEN: Expect the workbench layout to be restored. await expect(appPO.workbench).toEqualWorkbenchLayout({ @@ -1085,44 +1425,26 @@ test.describe('Workbench Router', () => { ratio: .25, child1: new MPart({ id: 'left', - views: [{id: 'view.3'}], // test view page - activeViewId: 'view.3', + views: [{id: 'view.100'}], + activeViewId: 'view.100', }), child2: new MPart({id: MAIN_AREA}), }), }, - mainAreaGrid: { - root: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}, {id: 'view.2'}], // layout page, router page - activeViewId: 'view.2', - }), - activePartId: await layoutPage.view.part.getPartId(), - }, }); // Expect the test view to display. - const viewPage = new ViewPagePO(appPO, {viewId: 'view.3'}); await expectView(viewPage).toBeActive(); }); - test('should allow for navigation to an empty path auxiliary route in the workbench grid', async ({appPO, workbenchNavigator}) => { - await appPO.navigateTo({microfrontendSupport: false, perspectives: ['perspective']}); - - // Define perspective with a part on the left. - await appPO.switchPerspective('perspective'); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {align: 'left', ratio: .25}); - - // Register auxiliary route. - await layoutPage.registerRoute({path: '', outlet: 'testee', component: 'view-page'}); + test('should allow for navigation to an empty-path route in the workbench grid', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); - // Open view in the left part. - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath(''); - await routerPage.enterTarget('testee'); - await routerPage.enterBlankPartId('left'); - await routerPage.clickNavigate(); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left', ratio: .25}) + .addView('view.100', {partId: 'left', activateView: true}) + .navigateView('view.100', [], {hint: 'test-view'}), + ); // Expect the view to be opened in the left part. await expect(appPO.workbench).toEqualWorkbenchLayout({ @@ -1132,57 +1454,309 @@ test.describe('Workbench Router', () => { ratio: .25, child1: new MPart({ id: 'left', - views: [{id: 'testee'}], - activeViewId: 'testee', + views: [{id: 'view.100'}], + activeViewId: 'view.100', }), child2: new MPart({id: MAIN_AREA}), }), }, - mainAreaGrid: { - root: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}, {id: 'view.2'}], // layout page, router page - activeViewId: 'view.2', - }), - activePartId: await layoutPage.view.part.getPartId(), - }, }); // Expect the view to display. - const viewPage = new ViewPagePO(appPO, {viewId: 'testee'}); + const viewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); await expectView(viewPage).toBeActive(); }); - test('should allow for navigation to an empty path auxiliary route in the main area', async ({appPO, workbenchNavigator}) => { + test('should allow for navigation to an empty-path route in the main area', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Register auxiliary route. - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.registerRoute({path: '', outlet: 'testee', component: 'view-page'}); - - // Open view in the left part. + // Open view in the main area. const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath(''); - await routerPage.enterTarget('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate([], { + target: 'view.100', + hint: 'test-view', + }); + await routerPage.view.tab.close(); - // Expect the view to be opened in the left part. + // Expect the view to be opened in the main area. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MPart({id: MAIN_AREA}), }, mainAreaGrid: { root: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}, {id: 'view.2'}, {id: 'testee'}], // layout page, router page, testee view - activeViewId: 'testee', + views: [{id: 'view.100'}], + activeViewId: 'view.100', }), - activePartId: await layoutPage.view.part.getPartId(), }, }); // Expect the view to display. - const viewPage = new ViewPagePO(appPO, {viewId: 'testee'}); + const viewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); await expectView(viewPage).toBeActive(); }); + + test('should navigate from path-based route to path-based route', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open router page as path-based route. + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addView('view.100', {partId: activePartId}) + .navigateView('view.100', ['test-router']), + ); + + // Navigate to path-based route. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.100'}); + await routerPage.navigate(['test-view'], { + target: 'view.100', + }); + + const testeeViewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); + + // Expect view to display path-based route. + await expectView(testeeViewPage).toBeActive(); + await expect(appPO.views()).toHaveCount(1); + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: ''}, + } satisfies Partial, + ); + }); + + test('should navigate from path-based route to empty-path route', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open router page as path-based route. + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addView('view.100', {partId: activePartId}) + .navigateView('view.100', ['test-router']), + ); + + // Navigate to empty-path route. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.100'}); + await routerPage.navigate([], { + target: 'view.100', + hint: 'test-view', + }); + + const testeeViewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); + + // Expect view to display empty-path route. + await expectView(testeeViewPage).toBeActive(); + await expect(appPO.views()).toHaveCount(1); + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: '', navigationHint: 'test-view'}, + } satisfies Partial, + ); + }); + + test('should navigate from empty-path route to empty-path route', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open router page as empty-path route. + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addView('view.100', {partId: activePartId}) + .navigateView('view.100', [], {hint: 'test-router'}), + ); + + // Navigate to empty-path route. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.100'}); + await routerPage.navigate([], { + target: 'view.100', + hint: 'test-view', + }); + + const testeeViewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); + + // Expect view to display empty-path route. + await expectView(testeeViewPage).toBeActive(); + await expect(appPO.views()).toHaveCount(1); + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: '', navigationHint: 'test-view'}, + } satisfies Partial, + ); + }); + + test('should navigate from empty-path route to path-based route', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open router page as empty-path route. + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addView('view.100', {partId: activePartId}) + .navigateView('view.100', [], {hint: 'test-router'}), + ); + + // Navigate to path-based route. + const routerPage = new RouterPagePO(appPO, {viewId: 'view.100'}); + await routerPage.navigate(['test-view'], { + target: 'view.100', + }); + + const testeeViewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); + + // Expect view to display path-based route. + await expectView(testeeViewPage).toBeActive(); + await expect(appPO.views()).toHaveCount(1); + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: ''}, + } satisfies Partial, + ); + }); + + test('should resolve to correct route', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open test-view with {path: 'test-view'} + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-view'], { + cssClass: 'testee', + }); + + const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); + + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: ''}, + } satisfies Partial, + ); + await testeeViewPage.view.tab.close(); + + // Open test-view with {path: 'test-view', hint: 'test-view'} + await routerPage.view.tab.click(); + await routerPage.navigate(['test-view'], { + hint: 'test-view', + cssClass: 'testee', + }); + + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: 'test-view'}, + } satisfies Partial, + ); + await testeeViewPage.view.tab.close(); + + // Open test-view with {path: '', hint: 'test-view'} + await routerPage.view.tab.click(); + await routerPage.navigate([], { + hint: 'test-view', + cssClass: 'testee', + }); + + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject( + { + routeData: {path: '', navigationHint: 'test-view'}, + } satisfies Partial, + ); + }); + + test('should reject if no path or hint is set', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open view without specifying path or hint. + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await expect(routerPage.navigate([])).rejects.toThrow(/\[NavigateError]/); + }); + + test.describe('Navigate by alternativeViewId', () => { + + test('should navigate view by alternative view id', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.createPerspective(factory => factory + .addPart('left') + .addPart('right', {relativeTo: 'left', align: 'right'}) + .addView('router', {partId: 'left', activateView: true, cssClass: 'router'}) + .addView('testee', {partId: 'right', cssClass: 'testee'}) + .navigateView('router', ['test-router']), + ); + + const routerPage = new RouterPagePO(appPO, {cssClass: 'router'}); + const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); + + // Open test view, assigning it an alternative view id + await routerPage.navigate(['test-pages/navigation-test-page/1'], { + target: 'testee', // alternative view id + }); + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject({urlSegments: 'test-pages/navigation-test-page/1'} satisfies Partial); + + // Navigate the test view by its alternative view id + await routerPage.navigate(['test-pages/navigation-test-page/2'], { + target: 'testee', // alternative view id + }); + await expect.poll(() => testeeViewPage.view.getInfo()).toMatchObject({urlSegments: 'test-pages/navigation-test-page/2'} satisfies Partial); + }); + + test('should close single view by alternative view id', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.createPerspective(factory => factory + .addPart('left') + .addPart('right', {relativeTo: 'left', align: 'right'}) + .addView('test-router', {partId: 'left', activateView: true, cssClass: 'router'}) + .addView('testee-1', {partId: 'right', cssClass: 'testee-1'}) + .addView('testee-2', {partId: 'right', cssClass: 'testee-2', activateView: true}) + .navigateView('test-router', ['test-router']) + .navigateView('testee-1', ['test-view']) + .navigateView('testee-2', ['test-view']), + ); + + const routerPage = new RouterPagePO(appPO, {cssClass: 'router'}); + const testee1ViewPage = new ViewPagePO(appPO, {cssClass: 'testee-1'}); + const testee2ViewPage = new ViewPagePO(appPO, {cssClass: 'testee-2'}); + + // Expect test views to be opened. + await expect(appPO.views()).toHaveCount(3); + + // Close the view with alternative id 'testee-1'. + await routerPage.navigate([], { + target: 'testee-1', + close: true, + }); + + // Expect the view with alternative id 'testee-1' to be closed. + await expect(appPO.views()).toHaveCount(2); + await expectView(routerPage).toBeActive(); + await expectView(testee1ViewPage).not.toBeAttached(); + await expectView(testee2ViewPage).toBeActive(); + }); + + test('should close multiple views by alternative view id', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.createPerspective(factory => factory + .addPart('left') + .addPart('right', {relativeTo: 'left', align: 'right'}) + .addView('test-router', {partId: 'left', activateView: true, cssClass: 'router'}) + .addView('testee-1', {partId: 'right', cssClass: 'testee-1'}) + .addView('testee-1', {partId: 'right', cssClass: 'testee-2'}) + .addView('testee-2', {partId: 'right', cssClass: 'testee-3', activateView: true}) + .navigateView('test-router', ['test-router']) + .navigateView('testee-1', ['test-view']) + .navigateView('testee-2', ['test-view']), + ); + + const routerPage = new RouterPagePO(appPO, {cssClass: 'router'}); + const testee1ViewPage = new ViewPagePO(appPO, {cssClass: 'testee-1'}); + const testee2ViewPage = new ViewPagePO(appPO, {cssClass: 'testee-2'}); + const testee3ViewPage = new ViewPagePO(appPO, {cssClass: 'testee-3'}); + + // Expect test views to be opened + await expect(appPO.views()).toHaveCount(4); + + // Close the views with alternative id 'testee-1'. + await routerPage.navigate([], { + target: 'testee-1', + close: true, + }); + + // Expect the views with alterantive id 'testee-1' to be closed. + await expect(appPO.views()).toHaveCount(2); + await expectView(testee1ViewPage).not.toBeAttached(); + await expectView(testee2ViewPage).not.toBeAttached(); + await expectView(testee3ViewPage).toBeActive(); + }); + }); }); diff --git a/projects/scion/e2e-testing/src/workbench/startup.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/startup.e2e-spec.ts index d5df1b28c..913f5ceff 100644 --- a/projects/scion/e2e-testing/src/workbench/startup.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/startup.e2e-spec.ts @@ -23,7 +23,7 @@ test.describe('Startup', () => { await appPO.waitUntilWorkbenchStarted(); // Open the test view. This view would error when constructed before the workbench startup completed. - await workbenchNavigator.openInNewTab(ViewPagePO); + const testeeViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); // Reload the app with the current layout to simulate views being instantiated right at startup. await appPO.reload(); @@ -39,7 +39,6 @@ test.describe('Startup', () => { await expect.poll(() => consoleLogs.get({severity: 'error'})).toEqual([]); // Expect the test view to show. - const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'e2e-test-view'}); await expectView(testeeViewPage).toBeActive(); await expect(appPO.views()).toHaveCount(1); }); diff --git a/projects/scion/e2e-testing/src/workbench/view-css-class.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/view-css-class.e2e-spec.ts index e3519c294..9e80e134d 100644 --- a/projects/scion/e2e-testing/src/workbench/view-css-class.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/view-css-class.e2e-spec.ts @@ -12,7 +12,6 @@ import {test} from '../fixtures'; import {ViewPagePO} from './page-object/view-page.po'; import {expect} from '@playwright/test'; import {RouterPagePO} from './page-object/router-page.po'; -import {LayoutPagePO} from './page-object/layout-page.po'; test.describe('Workbench View CSS Class', () => { @@ -20,10 +19,10 @@ test.describe('Workbench View CSS Class', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.100'); - await routerPage.enterCssClass('testee-navigation'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.100', + cssClass: 'testee-navigation', + }); const viewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); @@ -67,48 +66,58 @@ test.describe('Workbench View CSS Class', () => { }); }); - test('should associate CSS classes with a navigation (WorkbenchRouter.navigate)', async ({appPO, workbenchNavigator}) => { + test('should associate CSS classes with a view (WorkbenchLayout.addView) and navigation (WorkbenchRouter.navigate)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('right', {align: 'right'}); - await layoutPage.addView('view.100', {partId: 'right', activateView: true}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {align: 'right'}) + .addView('view.100', {partId: 'right', activateView: true, cssClass: 'testee-layout'}), + ); const viewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); // Navigate to 'test-pages/navigation-test-page/1' passing CSS class 'testee-navigation-1'. const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-pages/navigation-test-page/1'); - await routerPage.enterTarget('view.100'); - await routerPage.enterCssClass('testee-navigation-1'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1'], { + target: 'view.100', + cssClass: 'testee-navigation-1', + }); + // Expect CSS classes of the navigation to be set. await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-navigation-1'); await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-navigation-1'); + // Expect CSS classes of the layout to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-layout'); // Expect CSS classes of the route to be set. await expect.poll(() => viewPage.view.getCssClasses()).toContain('e2e-navigation-test-page'); await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('e2e-navigation-test-page'); // Navigate to 'test-pages/navigation-test-page/2' passing CSS class 'testee-navigation-2'. - await routerPage.enterPath('test-pages/navigation-test-page/2'); - await routerPage.enterTarget('view.100'); - await routerPage.enterCssClass('testee-navigation-2'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/2'], { + target: 'view.100', + cssClass: 'testee-navigation-2', + }); + // Expect CSS classes of the navigation to be set. await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-navigation-2'); await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-navigation-2'); // Expect CSS classes of the previous navigation not to be set. await expect.poll(() => viewPage.view.getCssClasses()).not.toContain('testee-navigation-1'); await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-navigation-1'); + // Expect CSS classes of the layout to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-layout'); // Expect CSS classes of the route to be set. await expect.poll(() => viewPage.view.getCssClasses()).toContain('e2e-navigation-test-page'); await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('e2e-navigation-test-page'); // Navigate to 'test-pages/navigation-test-page/2' passing CSS class 'testee-navigation-3'. - await routerPage.enterPath('test-pages/navigation-test-page/2'); - await routerPage.enterTarget('view.100'); - await routerPage.enterCssClass('testee-navigation-3'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/2'], { + target: 'view.100', + cssClass: 'testee-navigation-3', + }); + // Expect CSS classes of the navigation to be set. await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-navigation-3'); await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-navigation-3'); @@ -117,15 +126,18 @@ test.describe('Workbench View CSS Class', () => { await expect.poll(() => viewPage.view.getCssClasses()).not.toContain('testee-navigation-2'); await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-navigation-1'); await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-navigation-2'); + // Expect CSS classes of the layout to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-layout'); // Expect CSS classes of the route to be set. await expect.poll(() => viewPage.view.getCssClasses()).toContain('e2e-navigation-test-page'); await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('e2e-navigation-test-page'); // Navigate to 'test-pages/navigation-test-page/1' without passing CSS class. - await routerPage.enterPath('test-pages/navigation-test-page/1'); - await routerPage.enterTarget('view.100'); - await routerPage.enterCssClass([]); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-pages/navigation-test-page/1'], { + target: 'view.100', + }); + // Expect CSS classes of the previous navigations not to be set. await expect.poll(() => viewPage.view.getCssClasses()).not.toContain('testee-navigation-1'); await expect.poll(() => viewPage.view.getCssClasses()).not.toContain('testee-navigation-2'); @@ -133,6 +145,80 @@ test.describe('Workbench View CSS Class', () => { await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-navigation-1'); await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-navigation-2'); await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-navigation-3'); + // Expect CSS classes of the layout to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-layout'); + // Expect CSS classes of the route to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('e2e-navigation-test-page'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('e2e-navigation-test-page'); + }); + + test('should associate CSS classes with a view (WorkbenchLayout.addView) and navigation (WorkbenchLayout.navigateView)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {align: 'right'}) + .addView('view.100', {partId: 'right', activateView: true, cssClass: 'testee-layout'}), + ); + + const viewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); + + // Navigate to 'test-pages/navigation-test-page/1' passing CSS class 'testee-navigation-1'. + await workbenchNavigator.modifyLayout(layout => layout.navigateView('view.100', ['test-pages/navigation-test-page/1'], {cssClass: 'testee-navigation-1'})); + // Expect CSS classes of the navigation to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-navigation-1'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-navigation-1'); + // Expect CSS class(es) of the view to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-layout'); + // Expect CSS class(es) of the route to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('e2e-navigation-test-page'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('e2e-navigation-test-page'); + + // Navigate to 'test-pages/navigation-test-page/2' passing CSS class 'testee-navigation-2'. + await workbenchNavigator.modifyLayout(layout => layout.navigateView('view.100', ['test-pages/navigation-test-page/2'], {cssClass: 'testee-navigation-2'})); + // Expect CSS classes of the navigation to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-navigation-2'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-navigation-2'); + // Expect CSS classes of the previous navigation not to be set. + await expect.poll(() => viewPage.view.getCssClasses()).not.toContain('testee-navigation-1'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-navigation-1'); + // Expect CSS classes of the layout to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-layout'); + // Expect CSS classes of the route to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('e2e-navigation-test-page'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('e2e-navigation-test-page'); + + // Navigate to 'test-pages/navigation-test-page/2' passing CSS class 'testee-navigation-3'. + await workbenchNavigator.modifyLayout(layout => layout.navigateView('view.100', ['test-pages/navigation-test-page/2'], {cssClass: 'testee-navigation-3'})); + // Expect CSS classes of the navigation to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-navigation-3'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-navigation-3'); + // Expect CSS classes of the previous navigations not to be set. + await expect.poll(() => viewPage.view.getCssClasses()).not.toContain('testee-navigation-1'); + await expect.poll(() => viewPage.view.getCssClasses()).not.toContain('testee-navigation-2'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-navigation-1'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-navigation-2'); + // Expect CSS classes of the layout to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-layout'); + // Expect CSS classes of the route to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('e2e-navigation-test-page'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('e2e-navigation-test-page'); + + // Navigate to 'test-pages/navigation-test-page/1' without passing CSS class. + await workbenchNavigator.modifyLayout(layout => layout.navigateView('view.100', ['test-pages/navigation-test-page/1'])); + // Expect CSS classes of the previous navigations not to be set. + await expect.poll(() => viewPage.view.getCssClasses()).not.toContain('testee-navigation-1'); + await expect.poll(() => viewPage.view.getCssClasses()).not.toContain('testee-navigation-2'); + await expect.poll(() => viewPage.view.getCssClasses()).not.toContain('testee-navigation-3'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-navigation-1'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-navigation-2'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).not.toContain('testee-navigation-3'); + // Expect CSS classes of the layout to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-layout'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-layout'); // Expect CSS classes of the route to be set. await expect.poll(() => viewPage.view.getCssClasses()).toContain('e2e-navigation-test-page'); await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('e2e-navigation-test-page'); @@ -142,10 +228,10 @@ test.describe('Workbench View CSS Class', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.100'); - await routerPage.enterCssClass('testee-navigation'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.100', + cssClass: 'testee-navigation', + }); const viewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); await viewPage.view.tab.dragTo({partId: await viewPage.view.part.getPartId(), region: 'east'}); @@ -155,21 +241,98 @@ test.describe('Workbench View CSS Class', () => { await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-navigation'); }); + test('should retain navigational CSS classes when moving view to new window', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-view'], { + target: 'view.100', + cssClass: 'testee-navigation', + }); + + const viewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); + + // Move view to new window. + const newAppPO = await viewPage.view.tab.moveToNewWindow(); + const newViewPage = new ViewPagePO(newAppPO, {viewId: 'view.1'}); + + // Expect CSS classes of the navigation to be retained + await expect.poll(() => newViewPage.view.getCssClasses()).toContain('testee-navigation'); + await expect.poll(() => newViewPage.view.tab.getCssClasses()).toContain('testee-navigation'); + }); + + test('should retain navigational CSS classes when moving view to other window', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open view 1 + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-view'], { + target: 'view.101', + activate: false, + cssClass: 'testee-navigation-1', + }); + + // Open view 2 + await routerPage.navigate(['test-view'], { + target: 'view.102', + activate: false, + cssClass: 'testee-navigation-2', + }); + + const viewPage1 = new ViewPagePO(appPO, {viewId: 'view.101'}); + const viewPage2 = new ViewPagePO(appPO, {viewId: 'view.102'}); + + // Move view 1 to new window. + const newAppPO = await viewPage1.view.tab.moveToNewWindow(); + const newViewPage1 = new ViewPagePO(newAppPO, {viewId: 'view.1'}); + + // Move view 2 to the window. + await viewPage2.view.tab.moveTo(await newViewPage1.view.part.getPartId(), {workbenchId: await newAppPO.getWorkbenchId()}); + const newViewPage2 = new ViewPagePO(newAppPO, {viewId: 'view.2'}); + + // Expect CSS classes of the navigation to be retained + await expect.poll(() => newViewPage2.view.getCssClasses()).toContain('testee-navigation-2'); + await expect.poll(() => newViewPage2.view.tab.getCssClasses()).toContain('testee-navigation-2'); + }); + + test('should retain navigational CSS classes when reloading the application', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-view'], { + target: 'view.100', + cssClass: 'testee-navigation', + }); + + const viewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); + + // Expect CSS classes of the navigation to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-navigation'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-navigation'); + + // Reload the application. + await appPO.reload(); + + // Expect CSS classes of the navigation to be set. + await expect.poll(() => viewPage.view.getCssClasses()).toContain('testee-navigation'); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee-navigation'); + }); + test('should retain navigational CSS classes when switching view tabs', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.101'); - await routerPage.enterCssClass('testee-1'); - await routerPage.checkActivate(false); - await routerPage.clickNavigate(); - - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('view.102'); - await routerPage.enterCssClass('testee-2'); - await routerPage.checkActivate(false); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.101', + activate: false, + cssClass: 'testee-1', + }); + + await routerPage.navigate(['test-view'], { + target: 'view.102', + activate: false, + cssClass: 'testee-2', + }); const viewPage1 = new ViewPagePO(appPO, {viewId: 'view.101'}); const viewPage2 = new ViewPagePO(appPO, {viewId: 'view.102'}); @@ -189,11 +352,23 @@ test.describe('Workbench View CSS Class', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterTarget('view.100'); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.checkActivate(false); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'view.100', + activate: false, + cssClass: 'testee', + }); + + const viewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); + await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee'); + }); + + test('should add CSS classes to inactive view (WorkbenchLayout.navigateView)', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addView('view.100', {partId: activePartId, activateView: false}) + .navigateView('view.100', ['test-view'], {cssClass: 'testee'}), + ); const viewPage = new ViewPagePO(appPO, {viewId: 'view.100'}); await expect.poll(() => viewPage.view.tab.getCssClasses()).toContain('testee'); diff --git a/projects/scion/e2e-testing/src/workbench/view-drag-main-area-grid.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/view-drag-main-area-grid.e2e-spec.ts index b78517160..21b55936a 100644 --- a/projects/scion/e2e-testing/src/workbench/view-drag-main-area-grid.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/view-drag-main-area-grid.e2e-spec.ts @@ -11,7 +11,6 @@ import {expect} from '@playwright/test'; import {test} from '../fixtures'; import {ViewPagePO} from './page-object/view-page.po'; -import {LayoutPagePO} from './page-object/layout-page.po'; import {fromRect} from '../helper/testing.util'; import {MAIN_AREA} from '../workbench.model'; import {MPart, MTreeNode} from '../matcher/to-equal-workbench-layout.matcher'; @@ -21,26 +20,31 @@ test.describe('View Drag Main Area', () => { test.describe('should allow dragging a view to the side of the main area', () => { /** - * +-----------------+ +----------+-----------------+ - * | INITIAL | | | INITIAL | - * | [view.1,view.2] | | WEST | [view.1] | - * +-----------------+ => | [view.2] +-----------------+ - * | BOTTOM | | | BOTTOM | - * | [view.3] | | | [view.3] | - * +-----------------+ +----------+-----------------+ + * +----------------------+ +-------------+-------------------+ + * | INITIAL | | | INITIAL | + * | [test-view,view.101] | | WEST | [view.101] | + * +----------------------+ => | [test-view] +-------------------+ + * | BOTTOM | | | BOTTOM | + * | [view.102] | | | [view.102] | + * +----------------------+ +-------------+-------------------+ */ test('should allow dragging a view to the west in the main area', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom', ratio: .25}); - await layoutPage.addView('view.3', {partId: 'bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); - // Move view 2 to the west of the main area. - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.dragTo({grid: 'mainArea', region: 'west'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom', {relativeTo: initialPartId, align: 'bottom', ratio: .25}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom', activateView: true}), + ); - // Expect view 2 to be moved to the west of the main area. + // Move test view to the west of the main area. + await testView.view.tab.dragTo({grid: 'mainArea', region: 'west'}); + const testViewInfo = await testView.view.getInfo(); + + // Expect test view to be moved to the west of the main area. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MPart({ @@ -52,51 +56,56 @@ test.describe('View Drag Main Area', () => { direction: 'row', ratio: .2, child1: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: testViewInfo.viewId}], + activeViewId: testViewInfo.viewId, }), child2: new MTreeNode({ direction: 'column', ratio: .75, child1: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}], - activeViewId: 'view.1', + id: initialPartId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), child2: new MPart({ - id: await appPO.view({viewId: 'view.3'}).part.getPartId(), - views: [{id: 'view.3'}], - activeViewId: 'view.3', + id: 'bottom', + views: [{id: 'view.102'}], + activeViewId: 'view.102', }), }), }), - activePartId: await view2.part.getPartId(), + activePartId: testViewInfo.partId, }, }); }); /** - * +-----------------+ +-----------------+----------+ - * | INITIAL | | INITIAL | | - * | [view.1,view.2] | | [view.1] | EAST | - * +-----------------| => +-----------------+ [view.2] | - * | BOTTOM | | BOTTOM | | - * | [view.3] | | [view.3] | | - * +-----------------+ +-----------------+----------+ + * +-------------------+ +-------------------+----------+ + * | INITIAL | | INITIAL | | + * | [view.1,view.101] | | [view.101] | EAST | + * +-------------------| => +-------------------+ [view.1] | + * | BOTTOM | | BOTTOM | | + * | [view.102] | | [view.102] | | + * +-------------------+ +-------------------+----------+ */ test('should allow dragging a view to the east in the main area', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom', ratio: .25}); - await layoutPage.addView('view.3', {partId: 'bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom', {relativeTo: initialPartId, align: 'bottom', ratio: .25}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom', activateView: true}), + ); - // Move view 2 to the east of the main area. - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.dragTo({grid: 'mainArea', region: 'east'}); + // Move test view to the east of the main area. + await testView.view.tab.dragTo({grid: 'mainArea', region: 'east'}); + const testViewInfo = await testView.view.getInfo(); - // Expect view 2 to be moved to the east of the main area. + // Expect test view to be moved to the east of the main area. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MPart({ @@ -111,48 +120,53 @@ test.describe('View Drag Main Area', () => { direction: 'column', ratio: .75, child1: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}], - activeViewId: 'view.1', + id: initialPartId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), child2: new MPart({ - id: await appPO.view({viewId: 'view.3'}).part.getPartId(), - views: [{id: 'view.3'}], - activeViewId: 'view.3', + id: 'bottom', + views: [{id: 'view.102'}], + activeViewId: 'view.102', }), }), child2: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: testViewInfo.viewId}], + activeViewId: testViewInfo.viewId, }), }), - activePartId: await view2.part.getPartId(), + activePartId: testViewInfo.partId, }, }); }); /** - * +-----------------+----------+ +-------------+----------+ - * | | | | INITIAL | RIGHT | - * | INITIAL | RIGHT | | [view.1] | [view.3] | - * | [view.1,view.2] | [view.3] | => +-------------+----------+ - * | | | | SOUTH | - * | | | | [view.2] | - * +-----------------+----------+ +------------------------+ + * +-------------------+------------+ +-------------+------------+ + * | | | | INITIAL | RIGHT | + * | INITIAL | RIGHT | | [view.101] | [view.102] | + * | [view.1,view.101] | [view.102] | => +-------------+------------+ + * | | | | SOUTH | + * | | | | [view.1] | + * +-------------------+------------+ +--------------------------+ */ test('should allow dragging a view to the south in the main area', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('right', {relativeTo: await layoutPage.view.part.getPartId(), align: 'right', ratio: .25}); - await layoutPage.addView('view.3', {partId: 'right', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); - // Move view 2 to the south of the main area. - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.dragTo({grid: 'mainArea', region: 'south'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: initialPartId, align: 'right', ratio: .25}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'right', activateView: true}), + ); - // Expect view 2 to be moved to the south of the main area. + // Move test view to the south of the main area. + await testView.view.tab.dragTo({grid: 'mainArea', region: 'south'}); + const testViewInfo = await testView.view.getInfo(); + + // Expect test view to be moved to the south of the main area. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MPart({ @@ -167,44 +181,48 @@ test.describe('View Drag Main Area', () => { direction: 'row', ratio: .75, child1: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}], - activeViewId: 'view.1', + id: initialPartId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), child2: new MPart({ - id: await appPO.view({viewId: 'view.3'}).part.getPartId(), - views: [{id: 'view.3'}], - activeViewId: 'view.3', + id: 'right', + views: [{id: 'view.102'}], + activeViewId: 'view.102', }), }), child2: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: testViewInfo.viewId}], + activeViewId: testViewInfo.viewId, }), }), - activePartId: await view2.part.getPartId(), + activePartId: testViewInfo.partId, }, }); }); /** - * +-----------------+----------+ - * | | | - * | INITIAL | RIGHT | - * | [view.1,view.2] | [view.3] | - * | | | - * +-----------------+----------+ + * +-------------------+------------+ + * | | | + * | INITIAL | RIGHT | + * | [view.1,view.101] | [view.102] | + * | | | + * +-------------------+------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the main area (1)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('right', {relativeTo: await layoutPage.view.part.getPartId(), align: 'right'}); - await layoutPage.addView('view.3', {partId: 'right', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: initialPartId, align: 'right'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'right', activateView: true}), + ); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'mainArea'}); + await testView.view.tab.activateDropZones({grid: 'mainArea'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'south'})).toBe(true); @@ -213,23 +231,27 @@ test.describe('View Drag Main Area', () => { }); /** - * +-----------------+ - * | INITIAL | - * | [view.1,view.2] | - * +-----------------| - * | BOTTOM | - * | [view.3] | - * +-----------------+ + * +-------------------+ + * | INITIAL | + * | [view.1,view.101] | + * +-------------------| + * | BOTTOM | + * | [view.102] | + * +-------------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the main area (2)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'mainArea'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom', {relativeTo: initialPartId, align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom', activateView: true}), + ); + + await testView.view.tab.activateDropZones({grid: 'mainArea'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'south'})).toBe(false); @@ -238,25 +260,29 @@ test.describe('View Drag Main Area', () => { }); /** - * +-----------------+----------+ - * | INITIAL | | - * | [view.1,view.2] | RIGHT | - * +-----------------| [view.4] | - * | BOTTOM-LEFT | | - * | [view.3] | | - * +-----------------+----------+ + * +-------------------+------------+ + * | INITIAL | | + * | [view.1,view.101] | RIGHT | + * +-------------------| [view.103] | + * | BOTTOM-LEFT | | + * | [view.102] | | + * +-------------------+------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the main area (3)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('right', {relativeTo: await layoutPage.view.part.getPartId(), align: 'right'}); - await layoutPage.addPart('bottom-left', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'bottom-left', activateView: true}); - await layoutPage.addView('view.4', {partId: 'right', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: initialPartId, align: 'right'}) + .addPart('bottom-left', {relativeTo: initialPartId, align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom-left', activateView: true}) + .addView('view.103', {partId: 'right', activateView: true}), + ); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'mainArea'}); + await testView.view.tab.activateDropZones({grid: 'mainArea'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'south'})).toBe(true); @@ -265,25 +291,29 @@ test.describe('View Drag Main Area', () => { }); /** - * +----------+-----------------+ - * | | INITIAL | - * | LEFT | [view.1,view.2] | - * | [view.4] +-----------------| - * | | BOTTOM-RIGHT | - * | | [view.3] | - * +----------+-----------------+ + * +------------+-------------------+ + * | | INITIAL | + * | LEFT | [view.1,view.101] | + * | [view.103] +-------------------| + * | | BOTTOM-RIGHT | + * | | [view.102] | + * +------------+-------------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the main area (4)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {relativeTo: await layoutPage.view.part.getPartId(), align: 'left'}); - await layoutPage.addPart('bottom-right', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'bottom-right', activateView: true}); - await layoutPage.addView('view.4', {partId: 'left', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {relativeTo: initialPartId, align: 'left'}) + .addPart('bottom-right', {relativeTo: initialPartId, align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom-right', activateView: true}) + .addView('view.103', {partId: 'left', activateView: true}), + ); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'mainArea'}); + await testView.view.tab.activateDropZones({grid: 'mainArea'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'south'})).toBe(true); @@ -292,30 +322,34 @@ test.describe('View Drag Main Area', () => { }); /** - * +----------+-----------------+ - * | | INITIAL | - * | LEFT | [view.1,view.2] | - * | [view.4] +-----------------| - * | | BOTTOM-RIGHT | - * | | [view.3] | - * +----------+-----------------+ - * | BOTTOM | - * | [view.5] | - * +----------------------------+ + * +------------+-------------------+ + * | | INITIAL | + * | LEFT | [view.1,view.101] | + * | [view.103] +-------------------| + * | | BOTTOM-RIGHT | + * | | [view.102] | + * +------------+-------------------+ + * | BOTTOM | + * | [view.104] | + * +--------------------------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the main area (5)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom'}); - await layoutPage.addPart('left', {relativeTo: await layoutPage.view.part.getPartId(), align: 'left'}); - await layoutPage.addPart('bottom-right', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'bottom-right', activateView: true}); - await layoutPage.addView('view.4', {partId: 'left', activateView: true}); - await layoutPage.addView('view.5', {partId: 'bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'mainArea'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom', {relativeTo: initialPartId, align: 'bottom'}) + .addPart('left', {relativeTo: initialPartId, align: 'left'}) + .addPart('bottom-right', {relativeTo: initialPartId, align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom-right', activateView: true}) + .addView('view.103', {partId: 'left', activateView: true}) + .addView('view.104', {partId: 'bottom', activateView: true}), + ); + + await testView.view.tab.activateDropZones({grid: 'mainArea'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'south'})).toBe(false); @@ -324,32 +358,36 @@ test.describe('View Drag Main Area', () => { }); /** - * +----------+-----------------+----------+ - * | | INITIAL | | - * | LEFT | [view.1,view.2] | | - * | [view.4] +-----------------| | - * | | MIDDLE | RIGHT | - * | | [view.3] | [view.6] | - * +----------+-----------------+ | - * | BOTTOM | | - * | [view.5] | | - * +----------------------------+----------+ + * +------------+-------------------+------------+ + * | | INITIAL | | + * | LEFT | [view.1,view.101] | | + * | [view.103] +-------------------| | + * | | MIDDLE | RIGHT | + * | | [view.102] | [view.105] | + * +------------+-------------------+ | + * | BOTTOM | | + * | [view.104] | | + * +--------------------------------+------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the main area (6)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('right', {relativeTo: await layoutPage.view.part.getPartId(), align: 'right'}); - await layoutPage.addPart('bottom', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom'}); - await layoutPage.addPart('left', {relativeTo: await layoutPage.view.part.getPartId(), align: 'left'}); - await layoutPage.addPart('middle', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'middle', activateView: true}); - await layoutPage.addView('view.4', {partId: 'left', activateView: true}); - await layoutPage.addView('view.5', {partId: 'bottom', activateView: true}); - await layoutPage.addView('view.6', {partId: 'right', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: initialPartId, align: 'right'}) + .addPart('bottom', {relativeTo: initialPartId, align: 'bottom'}) + .addPart('left', {relativeTo: initialPartId, align: 'left'}) + .addPart('middle', {relativeTo: initialPartId, align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'middle', activateView: true}) + .addView('view.103', {partId: 'left', activateView: true}) + .addView('view.104', {partId: 'bottom', activateView: true}) + .addView('view.105', {partId: 'right', activateView: true}), + ); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'mainArea'}); + await testView.view.tab.activateDropZones({grid: 'mainArea'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'south'})).toBe(true); @@ -358,25 +396,29 @@ test.describe('View Drag Main Area', () => { }); /** - * +------------+-----------------+----------+ - * | | INITIAL | | - * | LEFT | [view.1,view.2] | RIGHT | - * | +-----------------|| - * | | BOTTOM-MIDDLE | | - * | | [view.3] | | - * +------------+-----------------+----------+ + * +------------+-------------------+----------+ + * | | INITIAL | | + * | LEFT | [view.1,view.101] | RIGHT | + * | +-------------------|| + * | | BOTTOM-MIDDLE | | + * | | [view.102] | | + * +------------+-------------------+----------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the main area (7)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {relativeTo: await layoutPage.view.part.getPartId(), align: 'left'}); - await layoutPage.addPart('right', {relativeTo: await layoutPage.view.part.getPartId(), align: 'right'}); - await layoutPage.addPart('bottom-middle', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'bottom-middle', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'mainArea'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {relativeTo: initialPartId, align: 'left'}) + .addPart('right', {relativeTo: initialPartId, align: 'right'}) + .addPart('bottom-middle', {relativeTo: initialPartId, align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom-middle', activateView: true}), + ); + + await testView.view.tab.activateDropZones({grid: 'mainArea'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'south'})).toBe(false); @@ -385,26 +427,30 @@ test.describe('View Drag Main Area', () => { }); /** - * +-------------+-----------------+--------------+ - * | LEFT-TOP | | RIGHT-TOP | - * | | INITIAL | | - * +-------------+ [view.1,view.2] +--------------+ - * | LEFT-BOTTOM | | RIGHT-BOTTOM | - * | | | [view.3] | - * +-------------+-----------------+--------------+ + * +-------------+-------------------+--------------+ + * | LEFT-TOP | | RIGHT-TOP | + * | | INITIAL | | + * +-------------+ [view.1,view.101] +--------------+ + * | LEFT-BOTTOM | | RIGHT-BOTTOM | + * | | | [view.102] | + * +-------------+-------------------+--------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the main area (8)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left-top', {relativeTo: await layoutPage.view.part.getPartId(), align: 'left'}); - await layoutPage.addPart('right-top', {relativeTo: await layoutPage.view.part.getPartId(), align: 'right'}); - await layoutPage.addPart('left-bottom', {relativeTo: 'left-top', align: 'bottom'}); - await layoutPage.addPart('right-bottom', {relativeTo: 'right-top', align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'right-bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left-top', {relativeTo: initialPartId, align: 'left'}) + .addPart('right-top', {relativeTo: initialPartId, align: 'right'}) + .addPart('left-bottom', {relativeTo: 'left-top', align: 'bottom'}) + .addPart('right-bottom', {relativeTo: 'right-top', align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'right-bottom', activateView: true}), + ); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'mainArea'}); + await testView.view.tab.activateDropZones({grid: 'mainArea'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'south'})).toBe(true); @@ -415,13 +461,16 @@ test.describe('View Drag Main Area', () => { test('should NOT allow dragging a view to the north or a fully adjacent side of the main area (9)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom'}); - await layoutPage.addPart('right', {relativeTo: await layoutPage.view.part.getPartId(), align: 'right'}); - await layoutPage.addView('view.3', {partId: 'right', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'mainArea'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom', {relativeTo: initialPartId, align: 'bottom'}) + .addPart('right', {relativeTo: initialPartId, align: 'right'}) + .addView('view.101', {partId: 'right', activateView: true}), + ); + + await testView.view.tab.activateDropZones({grid: 'mainArea'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'mainArea', region: 'south'})).toBe(true); @@ -430,20 +479,25 @@ test.describe('View Drag Main Area', () => { }); /** - * +------------------+ - * | INITIAL | - * | [view.1, view.2] | - * +------------------+ - * | BOTTOM | - * | [view.3] | - * +------------------+ + * +--------------------+ + * | INITIAL | + * | [view.1, view.101] | + * +--------------------+ + * | BOTTOM | + * | [view.102] | + * +--------------------+ */ test('should disable drop zone when dragging a view into the tabbar', async ({page, appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom', ratio: .25}); - await layoutPage.addView('view.3', {partId: 'bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom', {relativeTo: initialPartId, align: 'bottom', ratio: .25}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom', activateView: true}), + ); const workbenchGridDropZoneSize = 50; const mainAreaGridDropZoneSize = 100; @@ -468,20 +522,25 @@ test.describe('View Drag Main Area', () => { }); /** - * +------------------+ - * | INITIAL | - * | [view.1, view.2] | - * +------------------+ - * | BOTTOM | - * | [view.3] | - * +------------------+ + * +--------------------+ + * | INITIAL | + * | [view.1, view.101] | + * +--------------------+ + * | BOTTOM | + * | [view.102] | + * +--------------------+ */ test('should not disable drop zone when entering tabbar while dragging a view over the drop zone', async ({page, appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom', {relativeTo: await layoutPage.view.part.getPartId(), align: 'bottom', ratio: .25}); - await layoutPage.addView('view.3', {partId: 'bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom', {relativeTo: initialPartId, align: 'bottom', ratio: .25}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom', activateView: true}), + ); // Get bounding box of the tabbar of the 'bottom' part. const bottomPart = appPO.part({partId: 'bottom'}); diff --git a/projects/scion/e2e-testing/src/workbench/view-drag-workbench-grid.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/view-drag-workbench-grid.e2e-spec.ts index 0aa8e75c7..fa5dda26b 100644 --- a/projects/scion/e2e-testing/src/workbench/view-drag-workbench-grid.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/view-drag-workbench-grid.e2e-spec.ts @@ -11,7 +11,6 @@ import {expect} from '@playwright/test'; import {test} from '../fixtures'; import {ViewPagePO} from './page-object/view-page.po'; -import {LayoutPagePO} from './page-object/layout-page.po'; import {fromRect} from '../helper/testing.util'; import {MAIN_AREA} from '../workbench.model'; import {MPart, MTreeNode} from '../matcher/to-equal-workbench-layout.matcher'; @@ -21,77 +20,87 @@ test.describe('View Drag Workbench Grid', () => { test.describe('should allow dragging a view to the side of the workbench grid', () => { /** - * +------------------+ +----------+-----------+ - * | MAIN-AREA | => | WEST | MAIN-AREA | - * | [view.1, view.2] | | [view.2] | [view.1] | - * +------------------+ +----------+-----------+ + * +--------------------+ +----------+------------+ + * | MAIN-AREA | => | WEST | MAIN-AREA | + * | [view.1, view.101] | | [view.1] | [view.101] | + * +--------------------+ +----------+------------+ */ test('should allow dragging a view to the west in the workbench grid (1)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); - // Move view 2 to the west of the workbench grid. - await view2.tab.dragTo({grid: 'workbench', region: 'west'}); + await workbenchNavigator.modifyLayout(layout => layout + .addView('view.101', {partId: initialPartId}), + ); - // Expect view 2 to be moved to the west of the workbench grid. + // Move test view to the west of the workbench grid. + await testView.view.tab.dragTo({grid: 'workbench', region: 'west'}); + const testViewInfo = await testView.view.getInfo(); + + // Expect test view to be moved to the west of the workbench grid. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ direction: 'row', ratio: .2, child1: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: testViewInfo.viewId}], + activeViewId: testViewInfo.viewId, }), child2: new MPart({id: MAIN_AREA}), }), }, mainAreaGrid: { root: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}], - activeViewId: 'view.1', + id: initialPartId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), - activePartId: await layoutPage.view.part.getPartId(), + activePartId: initialPartId, }, }); }); /** - * +-------------+----------------+ +----------+-------------+------------+ - * | LEFT-TOP | | | | LEFT-TOP | | - * | [view.3] | | | | [view.3] | | - * +-------------+ MAIN-AREA | => | WEST +-------------+ MAIN-AREA | - * | LEFT-BOTTOM |[view.1, view.2]| | [view.2] | LEFT-BOTTOM | [view.1] | - * | [view.4] | | | | [view.4] | | - * +-------------+----------------+ +----------+-------------+------------+ + * +---------------+------------------+ +----------+---------------+------------+ + * | LEFT-TOP | | | | LEFT-TOP | | + * | [view.102] | | | | [view.102] | | + * +---------------+ MAIN-AREA | => | WEST +---------------+ MAIN-AREA | + * | LEFT-BOTTOM |[view.1, view.101]| | [view.1] | LEFT-BOTTOM | [view.101] | + * | [view.103] | | | | [view.103] | | + * +---------------+------------------+ +----------+---------------+------------+ */ test('should allow dragging a view to the west in the workbench grid (2)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left-top', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}); - await layoutPage.addPart('left-bottom', {relativeTo: 'left-top', align: 'bottom', ratio: .25}); - await layoutPage.addView('view.3', {partId: 'left-top', activateView: true}); - await layoutPage.addView('view.4', {partId: 'left-bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left-top', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) + .addPart('left-bottom', {relativeTo: 'left-top', align: 'bottom', ratio: .25}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'left-top', activateView: true}) + .addView('view.103', {partId: 'left-bottom', activateView: true}), + ); - // Move view 2 to the west of the workbench grid. - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.dragTo({grid: 'workbench', region: 'west'}); + // Move test view to the west of the workbench grid. + await testView.view.tab.dragTo({grid: 'workbench', region: 'west'}); + const testViewInfo = await testView.view.getInfo(); - // Expect view 2 to be moved to the west of the workbench grid. + // Expect test view to be moved to the west of the workbench grid. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ direction: 'row', ratio: .2, child1: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: testViewInfo.viewId}], + activeViewId: testViewInfo.viewId, }), child2: new MTreeNode({ direction: 'row', @@ -101,13 +110,13 @@ test.describe('View Drag Workbench Grid', () => { ratio: .75, child1: new MPart({ id: 'left-top', - views: [{id: 'view.3'}], - activeViewId: 'view.3', + views: [{id: 'view.102'}], + activeViewId: 'view.102', }), child2: new MPart({ id: 'left-bottom', - views: [{id: 'view.4'}], - activeViewId: 'view.4', + views: [{id: 'view.103'}], + activeViewId: 'view.103', }), }), child2: new MPart({id: MAIN_AREA}), @@ -116,31 +125,36 @@ test.describe('View Drag Workbench Grid', () => { }, mainAreaGrid: { root: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}], - activeViewId: 'view.1', + id: initialPartId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), - activePartId: await layoutPage.view.part.getPartId(), + activePartId: initialPartId, }, }); }); /** - * +------------------+ +----------+-----------+ - * | MAIN-AREA | => | WEST | MAIN-AREA | - * | [view.1, view.2] | | [view.2] | [view.1] | - * +------------------+ +----------+-----------+ + * +--------------------+ +------------+----------+ + * | MAIN-AREA | => | MAIN-AREA | EAST | + * | [view.1, view.101] | | [view.101] | [view.1] | + * +--------------------+ +------------+----------+ */ test('should allow dragging a view to the east in the workbench grid (1)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addView('view.101', {partId: initialPartId}), + ); - // Move view 2 to the east of the workbench grid. - await view2.tab.dragTo({grid: 'workbench', region: 'east'}); + // Move test view to the east of the workbench grid. + await testView.view.tab.dragTo({grid: 'workbench', region: 'east'}); + const testViewInfo = await testView.view.getInfo(); - // Expect view 2 to be moved to the east of the workbench grid. + // Expect test view to be moved to the east of the workbench grid. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ @@ -148,46 +162,51 @@ test.describe('View Drag Workbench Grid', () => { ratio: .8, child1: new MPart({id: MAIN_AREA}), child2: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: testViewInfo.viewId}], + activeViewId: testViewInfo.viewId, }), }), }, mainAreaGrid: { root: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}], - activeViewId: 'view.1', + id: initialPartId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), - activePartId: await layoutPage.view.part.getPartId(), + activePartId: initialPartId, }, }); }); /** - * +----------------+--------------+ +-----------+--------------+----------+ - * | | RIGHT-TOP | | | RIGHT-TOP | | - * | | [view.3] | | | [view.3] | | - * | MAIN-AREA +--------------+ => | MAIN-AREA +--------------+ EAST + - * |[view.1, view.2]| RIGHT-BOTTOM | | [view.1] | RIGHT-BOTTOM | [view.2] | - * | | [view.4] | | | [view.4] | | - * +----------------+--------------+ +-----------+--------------+----------+ + * +------------------+--------------+ +-------------+--------------+----------+ + * | | RIGHT-TOP | | | RIGHT-TOP | | + * | | [view.102] | | | [view.102] | | + * | MAIN-AREA +--------------+ => | MAIN-AREA +--------------+ EAST + + * |[view.1, view.101]| RIGHT-BOTTOM | | [view.101] | RIGHT-BOTTOM | [view.1] | + * | | [view.103] | | | [view.103] | | + * +------------------+--------------+ +-------------+--------------+----------+ */ test('should allow dragging a view to the east in the workbench grid (2)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('right-top', {relativeTo: MAIN_AREA, align: 'right', ratio: .25}); - await layoutPage.addPart('right-bottom', {relativeTo: 'right-top', align: 'bottom', ratio: .25}); - await layoutPage.addView('view.3', {partId: 'right-top', activateView: true}); - await layoutPage.addView('view.4', {partId: 'right-bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); - // Move view 2 to the east of the workbench grid. - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.dragTo({grid: 'workbench', region: 'east'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right-top', {relativeTo: MAIN_AREA, align: 'right', ratio: .25}) + .addPart('right-bottom', {relativeTo: 'right-top', align: 'bottom', ratio: .25}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'right-top', activateView: true}) + .addView('view.103', {partId: 'right-bottom', activateView: true}), + ); - // Expect view 2 to be moved to the east of the workbench grid. + // Move test view to the east of the workbench grid. + await testView.view.tab.dragTo({grid: 'workbench', region: 'east'}); + const testViewInfo = await testView.view.getInfo(); + + // Expect test view to be moved to the east of the workbench grid. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ @@ -202,53 +221,58 @@ test.describe('View Drag Workbench Grid', () => { ratio: .75, child1: new MPart({ id: 'right-top', - views: [{id: 'view.3'}], - activeViewId: 'view.3', + views: [{id: 'view.102'}], + activeViewId: 'view.102', }), child2: new MPart({ id: 'right-bottom', - views: [{id: 'view.4'}], - activeViewId: 'view.4', + views: [{id: 'view.103'}], + activeViewId: 'view.103', }), }), }), child2: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: testViewInfo.viewId}], + activeViewId: testViewInfo.viewId, }), }), }, mainAreaGrid: { root: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}], - activeViewId: 'view.1', + id: initialPartId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), - activePartId: await layoutPage.view.part.getPartId(), + activePartId: initialPartId, }, }); }); /** - * +------------------+ +-----------+ - * | | | MAIN-AREA | - * | MAIN-AREA | | [view.1] | - * | [view.1, view.2] | => +-----------+ - * | | | SOUTH | - * | | | [view.2] | - * +------------------+ +-----------+ + * +--------------------+ +-------------+ + * | | | MAIN-AREA | + * | MAIN-AREA | | [view.101] | + * | [view.1, view.101] | => +-------------+ + * | | | SOUTH | + * | | | [view.1] | + * +--------------------+ +-------------+ */ test('should allow dragging a view to the south in the workbench grid (1)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addView('view.101', {partId: initialPartId}), + ); - // Move view 2 to the south of the workbench grid. - await view2.tab.dragTo({grid: 'workbench', region: 'south'}); + // Move test view to the south of the workbench grid. + await testView.view.tab.dragTo({grid: 'workbench', region: 'south'}); + const testViewInfo = await testView.view.getInfo(); - // Expect view 2 to be moved to the south of the workbench grid. + // Expect test view to be moved to the south of the workbench grid. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ @@ -256,19 +280,19 @@ test.describe('View Drag Workbench Grid', () => { ratio: .8, child1: new MPart({id: MAIN_AREA}), child2: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: testViewInfo.viewId}], + activeViewId: testViewInfo.viewId, }), }), }, mainAreaGrid: { root: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}], - activeViewId: 'view.1', + id: initialPartId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), - activePartId: await layoutPage.view.part.getPartId(), + activePartId: initialPartId, }, }); }); @@ -276,29 +300,34 @@ test.describe('View Drag Workbench Grid', () => { /** * +----------------------------+ +----------------------------+ * | | | MAIN-AREA | - * | MAIN-AREA | | [view.1] | - * | [view.1, view.2] | => +-------------+--------------+ + * | MAIN-AREA | | [view.101] | + * | [view.1, view.101] | => +-------------+--------------+ * | | | BOTTOM-LEFT | BOTTOM-RIGHT | - * | | | [view.3] | [view.4 | + * | | | [view.102] | [view.103] | * +-------------+--------------+ +-------------+--------------+ * | BOTTOM-LEFT | BOTTOM-RIGHT | | SOUTH | - * | [view.3] | [view.4] | | [view.2] | + * | [view.102] | [view.103] | | [view.1] | * +----------------------------+ +----------------------------+ */ test('should allow dragging a view to the south in the workbench grid (2)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom-left', {relativeTo: MAIN_AREA, align: 'bottom', ratio: .25}); - await layoutPage.addPart('bottom-right', {relativeTo: 'bottom-left', align: 'right', ratio: .6}); - await layoutPage.addView('view.3', {partId: 'bottom-left', activateView: true}); - await layoutPage.addView('view.4', {partId: 'bottom-right', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); - // Move view 2 to the south of the workbench grid. - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.dragTo({grid: 'workbench', region: 'south'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom-left', {relativeTo: MAIN_AREA, align: 'bottom', ratio: .25}) + .addPart('bottom-right', {relativeTo: 'bottom-left', align: 'right', ratio: .6}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom-left', activateView: true}) + .addView('view.103', {partId: 'bottom-right', activateView: true}), + ); - // Expect view 2 to be moved to the south of the workbench grid. + // Move test view to the south of the workbench grid. + await testView.view.tab.dragTo({grid: 'workbench', region: 'south'}); + const testViewInfo = await testView.view.getInfo(); + + // Expect test view to be moved to the south of the workbench grid. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ @@ -313,51 +342,55 @@ test.describe('View Drag Workbench Grid', () => { ratio: .4, child1: new MPart({ id: 'bottom-left', - views: [{id: 'view.3'}], - activeViewId: 'view.3', + views: [{id: 'view.102'}], + activeViewId: 'view.102', }), child2: new MPart({ id: 'bottom-right', - views: [{id: 'view.4'}], - activeViewId: 'view.4', + views: [{id: 'view.103'}], + activeViewId: 'view.103', }), }), }), child2: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: testViewInfo.viewId}], + activeViewId: testViewInfo.viewId, }), }), }, mainAreaGrid: { root: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}], - activeViewId: 'view.1', + id: initialPartId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), - activePartId: await layoutPage.view.part.getPartId(), + activePartId: initialPartId, }, }); }); /** - * +-----------------+----------+ - * | | | - * | MAIN-AREA | RIGHT | - * | [view.1,view.2] | [view.3] | - * | | | - * +-----------------+----------+ + * +-------------------+------------+ + * | | | + * | MAIN-AREA | RIGHT | + * | [view.1,view.101] | [view.102] | + * | | | + * +-------------------+------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the workbench grid (1)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('right', {relativeTo: MAIN_AREA, align: 'right'}); - await layoutPage.addView('view.3', {partId: 'right', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: MAIN_AREA, align: 'right'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'right', activateView: true}), + ); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'workbench'}); + await testView.view.tab.activateDropZones({grid: 'workbench'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'south'})).toBe(true); @@ -366,23 +399,27 @@ test.describe('View Drag Workbench Grid', () => { }); /** - * +-----------------+ - * | MAIN-AREA | - * | [view.1,view.2] | - * +-----------------| - * | BOTTOM | - * | [view.3] | - * +-----------------+ + * +-------------------+ + * | MAIN-AREA | + * | [view.1,view.101] | + * +-------------------| + * | BOTTOM | + * | [view.102] | + * +-------------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the workbench grid (2)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom', activateView: true}), + ); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'workbench'}); + await testView.view.tab.activateDropZones({grid: 'workbench'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'south'})).toBe(false); @@ -391,25 +428,29 @@ test.describe('View Drag Workbench Grid', () => { }); /** - * +-----------------+----------+ - * | MAIN-AREA | | - * | [view.1,view.2] | RIGHT | - * +-----------------| [view.4] | - * | BOTTOM-LEFT | | - * | [view.3] | | - * +-----------------+----------+ + * +-------------------+------------+ + * | MAIN-AREA | | + * | [view.1,view.101] | RIGHT | + * +-------------------| [view.103] | + * | BOTTOM-LEFT | | + * | [view.102] | | + * +-------------------+------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the workbench grid (3)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('right', {relativeTo: MAIN_AREA, align: 'right'}); - await layoutPage.addPart('bottom-left', {relativeTo: MAIN_AREA, align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'bottom-left', activateView: true}); - await layoutPage.addView('view.4', {partId: 'right', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'workbench'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: MAIN_AREA, align: 'right'}) + .addPart('bottom-left', {relativeTo: MAIN_AREA, align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom-left', activateView: true}) + .addView('view.103', {partId: 'right', activateView: true}), + ); + + await testView.view.tab.activateDropZones({grid: 'workbench'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'south'})).toBe(true); @@ -418,25 +459,29 @@ test.describe('View Drag Workbench Grid', () => { }); /** - * +----------+-----------------+ - * | | MAIN-AREA | - * | LEFT | [view.1,view.2] | - * | [view.4] +-----------------| - * | | BOTTOM-RIGHT | - * | | [view.3] | - * +----------+-----------------+ + * +------------+-------------------+ + * | | MAIN-AREA | + * | LEFT | [view.1,view.101] | + * | [view.103] +-------------------| + * | | BOTTOM-RIGHT | + * | | [view.102] | + * +------------+-------------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the workbench grid (4)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {relativeTo: MAIN_AREA, align: 'left'}); - await layoutPage.addPart('bottom-right', {relativeTo: MAIN_AREA, align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'bottom-right', activateView: true}); - await layoutPage.addView('view.4', {partId: 'left', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {relativeTo: MAIN_AREA, align: 'left'}) + .addPart('bottom-right', {relativeTo: MAIN_AREA, align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom-right', activateView: true}) + .addView('view.103', {partId: 'left', activateView: true}), + ); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'workbench'}); + await testView.view.tab.activateDropZones({grid: 'workbench'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'south'})).toBe(true); @@ -445,30 +490,34 @@ test.describe('View Drag Workbench Grid', () => { }); /** - * +----------+-----------------+ - * | | MAIN-AREA | - * | LEFT | [view.1,view.2] | - * | [view.4] +-----------------| - * | | BOTTOM-RIGHT | - * | | [view.3] | - * +----------+-----------------+ - * | BOTTOM | - * | [view.5] | - * +----------------------------+ + * +------------+-------------------+ + * | | MAIN-AREA | + * | LEFT | [view.1,view.101] | + * | [view.103] +-------------------| + * | | BOTTOM-RIGHT | + * | | [view.102] | + * +------------+-------------------+ + * | BOTTOM | + * | [view.104] | + * +--------------------------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the workbench grid (5)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom'}); - await layoutPage.addPart('left', {relativeTo: MAIN_AREA, align: 'left'}); - await layoutPage.addPart('bottom-right', {relativeTo: MAIN_AREA, align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'bottom-right', activateView: true}); - await layoutPage.addView('view.4', {partId: 'left', activateView: true}); - await layoutPage.addView('view.5', {partId: 'bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'workbench'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom'}) + .addPart('left', {relativeTo: MAIN_AREA, align: 'left'}) + .addPart('bottom-right', {relativeTo: MAIN_AREA, align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom-right', activateView: true}) + .addView('view.103', {partId: 'left', activateView: true}) + .addView('view.104', {partId: 'bottom', activateView: true}), + ); + + await testView.view.tab.activateDropZones({grid: 'workbench'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'south'})).toBe(false); @@ -477,32 +526,36 @@ test.describe('View Drag Workbench Grid', () => { }); /** - * +----------+-----------------+----------+ - * | | MAIN-AREA | | - * | LEFT | [view.1,view.2] | | - * | [view.4] +-----------------| | - * | | MIDDLE | RIGHT | - * | | [view.3] | [view.6] | - * +----------+-----------------+ | - * | BOTTOM | | - * | [view.5] | | - * +----------------------------+----------+ + * +------------+-----------------+------------+ + * | | MAIN-AREA | | + * | LEFT | [view.1,view.2] | | + * | [view.103] +-----------------| | + * | | MIDDLE | RIGHT | + * | | [view.102] | [view.105] | + * +------------+-----------------+ | + * | BOTTOM | | + * | [view.104] | | + * +------------------------------+------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the workbench grid (6)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('right', {relativeTo: MAIN_AREA, align: 'right'}); - await layoutPage.addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom'}); - await layoutPage.addPart('left', {relativeTo: MAIN_AREA, align: 'left'}); - await layoutPage.addPart('middle', {relativeTo: MAIN_AREA, align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'middle', activateView: true}); - await layoutPage.addView('view.4', {partId: 'left', activateView: true}); - await layoutPage.addView('view.5', {partId: 'bottom', activateView: true}); - await layoutPage.addView('view.6', {partId: 'right', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: MAIN_AREA, align: 'right'}) + .addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom'}) + .addPart('left', {relativeTo: MAIN_AREA, align: 'left'}) + .addPart('middle', {relativeTo: MAIN_AREA, align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'middle', activateView: true}) + .addView('view.103', {partId: 'left', activateView: true}) + .addView('view.104', {partId: 'bottom', activateView: true}) + .addView('view.105', {partId: 'right', activateView: true}), + ); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'workbench'}); + await testView.view.tab.activateDropZones({grid: 'workbench'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'south'})).toBe(true); @@ -511,25 +564,29 @@ test.describe('View Drag Workbench Grid', () => { }); /** - * +------------+-----------------+----------+ - * | | MAIN-AREA | | - * | LEFT | [view.1,view.2] | RIGHT | - * | +-----------------|| - * | | BOTTOM-MIDDLE | | - * | | [view.3] | | - * +------------+-----------------+----------+ + * +------------+-------------------+----------+ + * | | MAIN-AREA | | + * | LEFT | [view.1,view.101] | RIGHT | + * | +-------------------|| + * | | BOTTOM-MIDDLE | | + * | | [view.102] | | + * +------------+-------------------+----------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the workbench grid (7)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {relativeTo: MAIN_AREA, align: 'left'}); - await layoutPage.addPart('right', {relativeTo: MAIN_AREA, align: 'right'}); - await layoutPage.addPart('bottom-middle', {relativeTo: MAIN_AREA, align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'bottom-middle', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {relativeTo: MAIN_AREA, align: 'left'}) + .addPart('right', {relativeTo: MAIN_AREA, align: 'right'}) + .addPart('bottom-middle', {relativeTo: MAIN_AREA, align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom-middle', activateView: true}), + ); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'workbench'}); + await testView.view.tab.activateDropZones({grid: 'workbench'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'south'})).toBe(false); @@ -538,26 +595,30 @@ test.describe('View Drag Workbench Grid', () => { }); /** - * +-------------+-----------------+--------------+ - * | LEFT-TOP | | RIGHT-TOP | - * | | MAIN-AREA | | - * +-------------+ [view.1,view.2] +--------------+ - * | LEFT-BOTTOM | | RIGHT-BOTTOM | - * | | | [view.3] | - * +-------------+-----------------+--------------+ + * +-------------+-------------------+--------------+ + * | LEFT-TOP | | RIGHT-TOP | + * | | MAIN-AREA | | + * +-------------+ [view.1,view.101] +--------------+ + * | LEFT-BOTTOM | | RIGHT-BOTTOM | + * | | | [view.102] | + * +-------------+-------------------+--------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the workbench grid (8)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left-top', {relativeTo: MAIN_AREA, align: 'left'}); - await layoutPage.addPart('right-top', {relativeTo: MAIN_AREA, align: 'right'}); - await layoutPage.addPart('left-bottom', {relativeTo: 'left-top', align: 'bottom'}); - await layoutPage.addPart('right-bottom', {relativeTo: 'right-top', align: 'bottom'}); - await layoutPage.addView('view.3', {partId: 'right-bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'workbench'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left-top', {relativeTo: MAIN_AREA, align: 'left'}) + .addPart('right-top', {relativeTo: MAIN_AREA, align: 'right'}) + .addPart('left-bottom', {relativeTo: 'left-top', align: 'bottom'}) + .addPart('right-bottom', {relativeTo: 'right-top', align: 'bottom'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'right-bottom', activateView: true}), + ); + + await testView.view.tab.activateDropZones({grid: 'workbench'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'south'})).toBe(true); @@ -566,24 +627,28 @@ test.describe('View Drag Workbench Grid', () => { }); /** - * +------------------+----------+ - * | MAIN-AREA | RIGHT | - * | [view.1, view.2] | [view.3] | - * +------------------+----------+ - * | BOTTOM | - * | | - * +-----------------------------+ + * +--------------------+------------+ + * | MAIN-AREA | RIGHT | + * | [view.1, view.101] | [view.102] | + * +--------------------+------------+ + * | BOTTOM | + * | | + * +---------------------------------+ */ test('should NOT allow dragging a view to the north or a fully adjacent side of the workbench grid (9)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom'}); - await layoutPage.addPart('right', {relativeTo: MAIN_AREA, align: 'right'}); - await layoutPage.addView('view.3', {partId: 'right', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom'}) + .addPart('right', {relativeTo: MAIN_AREA, align: 'right'}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'right', activateView: true}), + ); - const view2 = (await workbenchNavigator.openInNewTab(ViewPagePO)).view; - await view2.tab.activateDropZones({grid: 'workbench'}); + await testView.view.tab.activateDropZones({grid: 'workbench'}); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'north'})).toBe(false); await expect.poll(() => appPO.isDropZoneActive({grid: 'workbench', region: 'south'})).toBe(true); @@ -592,30 +657,32 @@ test.describe('View Drag Workbench Grid', () => { }); /** - * +------------------+ - * | MAIN-AREA | - * | [view.1, view.2] | - * +------------------+ - * | BOTTOM | - * | [view.3] | - * +------------------+ + * +--------------------+ + * | MAIN-AREA | + * | [view.1, view.101] | + * +--------------------+ + * | BOTTOM | + * | [view.102] | + * +--------------------+ */ test('should disable drop zone when dragging a view into the tabbar', async ({page, appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom', ratio: .25}); - await layoutPage.addView('view.3', {partId: 'bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom', ratio: .25}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom', activateView: true}), + ); // Get bounding box of the tabbar of the 'bottom' part. const bottomPart = appPO.part({partId: 'bottom'}); const bottomTabbarBounds = fromRect(await bottomPart.getPartBarBoundingBox()); - // Open view in the initial part. - const viewPage2 = await workbenchNavigator.openInNewTab(ViewPagePO); - // Press mouse button on the view tab. - await viewPage2.view.tab.mousedown(); + await testView.view.tab.mousedown(); // Move tab to the center of the tabbar of the 'bottom' part. await page.mouse.move(bottomTabbarBounds.hcenter, bottomTabbarBounds.vcenter, {steps: 100}); @@ -628,30 +695,32 @@ test.describe('View Drag Workbench Grid', () => { }); /** - * +------------------+ - * | MAIN-AREA | - * | [view.1, view.2] | - * +------------------+ - * | BOTTOM | - * | [view.3] | - * +------------------+ + * +--------------------+ + * | MAIN-AREA | + * | [view.1, view.101] | + * +--------------------+ + * | BOTTOM | + * | [view.102] | + * +--------------------+ */ test('should not disable drop zone when entering tabbar while dragging a view over the drop zone', async ({page, appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom', ratio: .25}); - await layoutPage.addView('view.3', {partId: 'bottom', activateView: true}); + const testView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await testView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('bottom', {relativeTo: MAIN_AREA, align: 'bottom', ratio: .25}) + .addView('view.101', {partId: initialPartId}) + .addView('view.102', {partId: 'bottom', activateView: true}), + ); // Get bounding box of the tabbar of the 'bottom' part. const bottomPart = appPO.part({partId: 'bottom'}); const bottomTabbarBounds = fromRect(await bottomPart.getPartBarBoundingBox()); - // Open view in the initial part. - const viewPage2 = await workbenchNavigator.openInNewTab(ViewPagePO); - // Press mouse button on the view tab. - await viewPage2.view.tab.mousedown(); + await testView.view.tab.mousedown(); // Move tab into drop zone. await page.mouse.move(0, 0, {steps: 1}); diff --git a/projects/scion/e2e-testing/src/workbench/view-drag.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/view-drag.e2e-spec.ts index d6a84d94c..dda28b0d3 100644 --- a/projects/scion/e2e-testing/src/workbench/view-drag.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/view-drag.e2e-spec.ts @@ -11,12 +11,9 @@ import {expect} from '@playwright/test'; import {test} from '../fixtures'; import {ViewPagePO} from './page-object/view-page.po'; -import {LayoutPagePO} from './page-object/layout-page.po'; import {MPart, MTreeNode} from '../matcher/to-equal-workbench-layout.matcher'; import {expectView} from '../matcher/view-matcher'; -import {PerspectivePagePO} from './page-object/perspective-page.po'; import {MAIN_AREA} from '../workbench.model'; -import {RouterPagePO} from './page-object/router-page.po'; test.describe('View Drag', () => { @@ -330,72 +327,75 @@ test.describe('View Drag', () => { test.describe('drag to another part', () => { /** - * +----------+------------------+ +------------------+----------+ - * | INITIAL | XYZ | => | INITIAL | XYZ | - * | [view.1] | [view.2, view.3] | | [view.1, view.2] | [view.3] | - * +----------+------------------+ +------------------+----------+ + * +-----------+----------------------+ +--------------------+------------+ + * | INITIAL | RIGHT | => | INITIAL | RIGHT | + * | [view.1] | [view.101, view.102] | | [view.1, view.101] | [view.102] | + * +-----------+----------------------+ +--------------------+------------+ */ test('should allow dragging a view to the center of another part', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Open view in the initial part. - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - const view2 = appPO.view({viewId: 'view.2'}); - const view3 = appPO.view({viewId: 'view.3'}); + const initialPartView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await initialPartView.view.part.getPartId(); // Open two views in another part. - await layoutPage.addPart('xyz', {relativeTo: await layoutPage.view.part.getPartId(), align: 'right', ratio: .5}); - await layoutPage.addView('view.2', {partId: 'xyz', activateView: true}); - await layoutPage.addView('view.3', {partId: 'xyz'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: initialPartId, align: 'right', ratio: .5}) + .addView('view.101', {partId: 'right', activateView: true}) + .addView('view.102', {partId: 'right'}), + ); - // Move view 2 to the center of the initial part. - await view2.tab.dragTo({partId: await layoutPage.view.part.getPartId(), region: 'center'}); + // Move view to the center of the initial part. + const testView = appPO.view({viewId: 'view.101'}); + await testView.tab.dragTo({partId: initialPartId, region: 'center'}); - // Expect view 2 to be moved to the initial part. + // Expect view to be moved to the initial part. await expect(appPO.workbench).toEqualWorkbenchLayout({ mainAreaGrid: { root: new MTreeNode({ direction: 'row', ratio: .5, child1: new MPart({ - id: await layoutPage.view.part.getPartId(), - views: [{id: 'view.1'}, {id: 'view.2'}], - activeViewId: 'view.2', + id: initialPartId, + views: [{id: 'view.1'}, {id: 'view.101'}], + activeViewId: 'view.101', }), child2: new MPart({ - id: await view3.part.getPartId(), - views: [{id: 'view.3'}], - activeViewId: 'view.3', + id: 'right', + views: [{id: 'view.102'}], + activeViewId: 'view.102', }), }), - activePartId: await layoutPage.view.part.getPartId(), + activePartId: initialPartId, }, }); }); /** - * +----------+------------------+ +----------+----------+----------+ - * | INITIAL | XYZ | => | WEST | INITIAL | XYZ | - * | [view.1] | [view.2, view.3] | | [view.2] | [view.1] | [view.3] | - * +----------+------------------+ +----------+----------+----------+ + * +-----------+----------------------+ +------------+------------+------------+ + * | INITIAL | RIGHT | => | WEST | INITIAL | RIGHT | + * | [view.1] | [view.101, view.102] | | [view.101] | [view.1] | [view.102] | + * +-----------+----------------------+ +------------+------------+------------+ */ test('should allow dragging a view to the west of another part', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Open view in the initial part. - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - const view2 = appPO.view({viewId: 'view.2'}); - const view3 = appPO.view({viewId: 'view.3'}); + const initialPartView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await initialPartView.view.part.getPartId(); // Open two views in another part. - await layoutPage.addPart('xyz', {relativeTo: await layoutPage.view.part.getPartId(), align: 'right', ratio: .5}); - await layoutPage.addView('view.2', {partId: 'xyz', activateView: true}); - await layoutPage.addView('view.3', {partId: 'xyz'}); - - // Move view 2 to a new part in the west of the initial part. - await view2.tab.dragTo({partId: await layoutPage.view.part.getPartId(), region: 'west'}); - - // Expect view 2 to be moved to a new part in the west of the initial part. + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: initialPartId, align: 'right', ratio: .5}) + .addView('view.101', {partId: 'right', activateView: true}) + .addView('view.102', {partId: 'right'}), + ); + + // Move view to a new part in the west of the initial part. + const testView = appPO.view({viewId: 'view.101'}); + await testView.tab.dragTo({partId: initialPartId, region: 'west'}); + const testViewInfo = await testView.getInfo(); + + // Expect view to be moved to a new part in the west of the initial part. await expect(appPO.workbench).toEqualWorkbenchLayout({ mainAreaGrid: { root: new MTreeNode({ @@ -405,50 +405,52 @@ test.describe('View Drag', () => { direction: 'row', ratio: .5, child1: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), child2: new MPart({ - id: await layoutPage.view.part.getPartId(), + id: initialPartId, views: [{id: 'view.1'}], activeViewId: 'view.1', }), }), child2: new MPart({ - id: await view3.part.getPartId(), - views: [{id: 'view.3'}], - activeViewId: 'view.3', + id: 'right', + views: [{id: 'view.102'}], + activeViewId: 'view.102', }), }), - activePartId: await view2.part.getPartId(), + activePartId: testViewInfo.partId, }, }); }); /** - * +----------+------------------+ +----------+----------+----------+ - * | INITIAL | XYZ | => | INITIAL | EAST | XYZ | - * | [view.1] | [view.2, view.3] | | [view.1] | [view.2] | [view.3] | - * +----------+------------------+ +----------+----------+----------+ + * +----------+----------------------+ +----------+------------+------------+ + * | INITIAL | RIGHT | => | INITIAL | EAST | RIGHT | + * | [view.1] | [view.101, view.102] | | [view.1] | [view.101] | [view.102] | + * +----------+----------------------+ +----------+------------+------------+ */ test('should allow dragging a view to the east of another part', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Open view in the initial part. - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - const view2 = appPO.view({viewId: 'view.2'}); - const view3 = appPO.view({viewId: 'view.3'}); + const initialPartView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await initialPartView.view.part.getPartId(); // Open two views in another part. - await layoutPage.addPart('xyz', {relativeTo: await layoutPage.view.part.getPartId(), align: 'right', ratio: .5}); - await layoutPage.addView('view.2', {partId: 'xyz', activateView: true}); - await layoutPage.addView('view.3', {partId: 'xyz'}); - - // Move view 2 to a new part in the east of the initial part. - await view2.tab.dragTo({partId: await layoutPage.view.part.getPartId(), region: 'east'}); - - // Expect view 2 to be moved to a new part in the east of the initial part. + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: initialPartId, align: 'right', ratio: .5}) + .addView('view.101', {partId: 'right', activateView: true}) + .addView('view.102', {partId: 'right'}), + ); + + // Move view to a new part in the east of the initial part. + const testView = appPO.view({viewId: 'view.101'}); + await testView.tab.dragTo({partId: initialPartId, region: 'east'}); + const testViewInfo = await testView.getInfo(); + + // Expect view to be moved to a new part in the east of the initial part. await expect(appPO.workbench).toEqualWorkbenchLayout({ mainAreaGrid: { root: new MTreeNode({ @@ -458,53 +460,55 @@ test.describe('View Drag', () => { direction: 'row', ratio: .5, child1: new MPart({ - id: await layoutPage.view.part.getPartId(), + id: initialPartId, views: [{id: 'view.1'}], activeViewId: 'view.1', }), child2: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), }), child2: new MPart({ - id: await view3.part.getPartId(), - views: [{id: 'view.3'}], - activeViewId: 'view.3', + id: 'right', + views: [{id: 'view.102'}], + activeViewId: 'view.102', }), }), - activePartId: await view2.part.getPartId(), + activePartId: testViewInfo.partId, }, }); }); /** - * +----------+------------------+ +----------+----------+ - * | | | | NORTH | | - * | INITIAL | XYZ | | [view.2] | XYZ | - * | [view.1] | [view.2, view.3] | => +----------+ [view.3] | - * | | | | INITIAL | | - * | | | | [view.1] | | - * +----------+------------------+ +----------+----------+ + * +----------+----------------------+ +------------+------------+ + * | | | | NORTH | | + * | INITIAL | RIGHT | | [view.101] | RIGHT | + * | [view.1] | [view.101, view.102] | => +------------+ [view.102] | + * | | | | INITIAL | | + * | | | | [view.1] | | + * +----------+----------------------+ +------------+------------+ */ test('should allow dragging a view to the north of another part', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Open view in the initial part. - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - const view2 = appPO.view({viewId: 'view.2'}); - const view3 = appPO.view({viewId: 'view.3'}); + const initialPartView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await initialPartView.view.part.getPartId(); // Open two views in another part. - await layoutPage.addPart('xyz', {relativeTo: await layoutPage.view.part.getPartId(), align: 'right', ratio: .5}); - await layoutPage.addView('view.2', {partId: 'xyz', activateView: true}); - await layoutPage.addView('view.3', {partId: 'xyz'}); - - // Move view 2 to a new part in the north of the initial part. - await view2.tab.dragTo({partId: await layoutPage.view.part.getPartId(), region: 'north'}); - - // Expect view 2 to be moved to a new part in the north of the initial part. + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: initialPartId, align: 'right', ratio: .5}) + .addView('view.101', {partId: 'right', activateView: true}) + .addView('view.102', {partId: 'right'}), + ); + + // Move view to a new part in the north of the initial part. + const testView = appPO.view({viewId: 'view.101'}); + await testView.tab.dragTo({partId: initialPartId, region: 'north'}); + const testViewInfo = await testView.getInfo(); + + // Expect view to be moved to a new part in the north of the initial part. await expect(appPO.workbench).toEqualWorkbenchLayout({ mainAreaGrid: { root: new MTreeNode({ @@ -514,53 +518,55 @@ test.describe('View Drag', () => { direction: 'column', ratio: .5, child1: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), child2: new MPart({ - id: await layoutPage.view.part.getPartId(), + id: initialPartId, views: [{id: 'view.1'}], activeViewId: 'view.1', }), }), child2: new MPart({ - id: await view3.part.getPartId(), - views: [{id: 'view.3'}], - activeViewId: 'view.3', + id: 'right', + views: [{id: 'view.102'}], + activeViewId: 'view.102', }), }), - activePartId: await view2.part.getPartId(), + activePartId: testViewInfo.partId, }, }); }); /** - * +----------+------------------+ +----------+----------+ - * | | | | INITIAL | | - * | INITIAL | XYZ | | [view.1] | XYZ | - * | [view.1] | [view.2, view.3] | => +----------+ [view.3] | - * | | | | SOUTH | | - * | | | | [view.2] | | - * +----------+------------------+ +----------+----------+ + * +----------+----------------------+ +------------+------------+ + * | | | | INITIAL | | + * | INITIAL | RIGHT | | [view.1] | RIGHT | + * | [view.1] | [view.101, view.102] | => +------------+ [view.102] | + * | | | | SOUTH | | + * | | | | [view.101] | | + * +----------+----------------------+ +------------+------------+ */ test('should allow dragging a view to the south of another part', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Open view in the initial part. - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - const view2 = appPO.view({viewId: 'view.2'}); - const view3 = appPO.view({viewId: 'view.3'}); + const initialPartView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await initialPartView.view.part.getPartId(); // Open two views in another part. - await layoutPage.addPart('another', {relativeTo: await layoutPage.view.part.getPartId(), align: 'right', ratio: .5}); - await layoutPage.addView('view.2', {partId: 'another', activateView: true}); - await layoutPage.addView('view.3', {partId: 'another'}); - - // Move view 2 to a new part in the south of the initial part. - await view2.tab.dragTo({partId: await layoutPage.view.part.getPartId(), region: 'south'}); - - // Expect view 2 to be moved to a new part in the south of the initial part. + await workbenchNavigator.modifyLayout(layout => layout + .addPart('right', {relativeTo: initialPartId, align: 'right', ratio: .5}) + .addView('view.101', {partId: 'right', activateView: true}) + .addView('view.102', {partId: 'right'}), + ); + + // Move view to a new part in the south of the initial part. + const testView = appPO.view({viewId: 'view.101'}); + await testView.tab.dragTo({partId: initialPartId, region: 'south'}); + const testViewInfo = await testView.getInfo(); + + // Expect view to be moved to a new part in the south of the initial part. await expect(appPO.workbench).toEqualWorkbenchLayout({ mainAreaGrid: { root: new MTreeNode({ @@ -570,23 +576,23 @@ test.describe('View Drag', () => { direction: 'column', ratio: .5, child1: new MPart({ - id: await layoutPage.view.part.getPartId(), + id: initialPartId, views: [{id: 'view.1'}], activeViewId: 'view.1', }), child2: new MPart({ - id: await view2.part.getPartId(), - views: [{id: 'view.2'}], - activeViewId: 'view.2', + id: testViewInfo.partId, + views: [{id: 'view.101'}], + activeViewId: 'view.101', }), }), child2: new MPart({ - id: await view3.part.getPartId(), - views: [{id: 'view.3'}], - activeViewId: 'view.3', + id: 'right', + views: [{id: 'view.102'}], + activeViewId: 'view.102', }), }), - activePartId: await view2.part.getPartId(), + activePartId: testViewInfo.partId, }, }); }); @@ -597,26 +603,12 @@ test.describe('View Drag', () => { test('should drop view on start page of the main area (grid root is MPart)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Create perspective with a left and right part. - const perspectivePage = await workbenchNavigator.openInNewTab(PerspectivePagePO); - await perspectivePage.registerPerspective({ - id: 'perspective', - parts: [ - {id: MAIN_AREA}, - {id: 'left', align: 'left'}, - ], - }); - await perspectivePage.view.tab.close(); - await appPO.switchPerspective('perspective'); - - // TODO [WB-LAYOUT] Open test view via perspective definition. - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.enterTarget('blank'); - await routerPage.enterBlankPartId('left'); - await routerPage.clickNavigate(); - await routerPage.view.tab.close(); + await workbenchNavigator.createPerspective(factory => factory + .addPart(MAIN_AREA) + .addPart('left', {align: 'left'}) + .addView('testee', {partId: 'left', cssClass: 'testee'}) + .navigateView('testee', ['test-view']), + ); // Drop view on the start page of the main area. const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); @@ -630,33 +622,18 @@ test.describe('View Drag', () => { test('should drop view on start page of the main area (grid root is MTreeNode)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Create perspective with a part left to the main area. - const perspectivePage = await workbenchNavigator.openInNewTab(PerspectivePagePO); - await perspectivePage.registerPerspective({ - id: 'perspective', - parts: [ - {id: MAIN_AREA}, - {id: 'left', align: 'left'}, - ], - }); - await perspectivePage.view.tab.close(); - await appPO.switchPerspective('perspective'); + await workbenchNavigator.createPerspective(factory => factory + .addPart(MAIN_AREA) + .addPart('left', {align: 'left'}) + .addView('testee', {partId: 'left', cssClass: 'testee'}) + .navigateView('testee', ['test-view']), + ); // Change the grid root of the main area to a `MTreeNode`. - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - const mainAreaActivePartId = await layoutPage.view.part.getPartId(); - await layoutPage.addPart('main-left', {relativeTo: mainAreaActivePartId, align: 'left'}); - await layoutPage.addPart('main-right', {relativeTo: mainAreaActivePartId, align: 'right'}); - await layoutPage.view.tab.close(); - - // TODO [WB-LAYOUT] Open test view via perspective definition. - const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.enterTarget('blank'); - await routerPage.enterBlankPartId('left'); - await routerPage.clickNavigate(); - await routerPage.view.tab.close(); + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addPart('main-left', {relativeTo: activePartId, align: 'left'}) + .addPart('main-right', {relativeTo: activePartId, align: 'right'}), + ); // Drop view on the start page of the main area. const testeeViewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); diff --git a/projects/scion/e2e-testing/src/workbench/view-not-found.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/view-not-found.e2e-spec.ts new file mode 100644 index 000000000..54044d562 --- /dev/null +++ b/projects/scion/e2e-testing/src/workbench/view-not-found.e2e-spec.ts @@ -0,0 +1,185 @@ +/* + * 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 + */ + +import {test} from '../fixtures'; +import {expect} from '@playwright/test'; +import {PageNotFoundPagePO} from './page-object/page-not-found-page.po'; +import {expectView} from '../matcher/view-matcher'; +import {BlankViewPagePO} from './page-object/blank-view-page.po'; +import {MAIN_AREA} from '../workbench.model'; +import {ConsoleLogs} from '../helper/console-logs'; +import {ViewPagePO} from './page-object/view-page.po'; + +test.describe('Workbench Page Not Found', () => { + + test('should display blank page when adding a view but not navigating it', async ({appPO, workbenchNavigator, consoleLogs}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Add view.101 in peripheral area + // Add view.102 in main area + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addPart('left', {align: 'left'}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: activePartId}), + ); + + const viewPage1 = new BlankViewPagePO(appPO, {viewId: 'view.101'}); + const viewPage2 = new BlankViewPagePO(appPO, {viewId: 'view.102'}); + + await expectView(viewPage1).toBeActive(); + await expectView(viewPage2).toBeActive(); + + // Reload the application and expect the blank page to still be displayed. + await test.step('Reloading the application', async () => { + await appPO.reload(); + await expectView(viewPage1).toBeActive(); + await expectView(viewPage2).toBeActive(); + }); + + // Expect Angular router not to error. + await expect.poll(() => consoleLogs.get({severity: 'error'})).toHaveLength(0); + }); + + test('should display "Not Found Page" when navigating to an unknown path', async ({appPO, workbenchNavigator, consoleLogs}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Add view.101 in peripheral area + // Add view.102 in main area + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addPart('left', {align: 'left'}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: activePartId}) + .navigateView('view.101', ['does/not/exist']) + .navigateView('view.102', ['does/not/exist']), + ); + + const viewPage1 = new PageNotFoundPagePO(appPO, {viewId: 'view.101'}); + const viewPage2 = new PageNotFoundPagePO(appPO, {viewId: 'view.102'}); + + await expectView(viewPage1).toBeActive(); + await expectView(viewPage2).toBeActive(); + + // Reload the application and expect the "Not Found Page" to still be displayed. + await test.step('Reloading the application', async () => { + await appPO.reload(); + await expectView(viewPage1).toBeActive(); + await expectView(viewPage2).toBeActive(); + }); + + // Expect Angular router not to error. + await expect.poll(() => consoleLogs.get({severity: 'error'})).toHaveLength(0); + }); + + test('should display "Not Found Page" when navigating with a hint that matches no route', async ({appPO, workbenchNavigator, consoleLogs}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Add view.101 in peripheral area + // Add view.102 in main area + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addPart('left', {align: 'left'}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: activePartId}) + .navigateView('view.101', [], {hint: 'does-not-match'}) + .navigateView('view.102', [], {hint: 'does-not-match'}), + ); + + const viewPage1 = new PageNotFoundPagePO(appPO, {viewId: 'view.101'}); + const viewPage2 = new PageNotFoundPagePO(appPO, {viewId: 'view.102'}); + + await expectView(viewPage1).toBeActive(); + await expectView(viewPage2).toBeActive(); + + // Reload the application and expect the "Not Found Page" to still be displayed. + await test.step('Reloading the application', async () => { + await appPO.reload(); + await expectView(viewPage1).toBeActive(); + await expectView(viewPage2).toBeActive(); + }); + + // Expect Angular router not to error. + await expect.poll(() => consoleLogs.get({severity: 'error'})).toHaveLength(0); + }); + + test('should drag "Not Found Page" to another part', async ({appPO, workbenchNavigator, consoleLogs}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + const initialPartView = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await initialPartView.view.part.getPartId(); + + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left'}) + .addPart('right', {align: 'right'}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: 'right', activateView: true}) + .navigateView('view.101', ['does/not/exist']), + ); + + const viewPage = new PageNotFoundPagePO(appPO, {viewId: 'view.101'}); + + // Drag view to right part. + await viewPage.view.tab.dragTo({partId: 'right', region: 'center'}); + + // Except view to be moved to right part. + await expectView(viewPage).toBeActive(); + await expect.poll(() => viewPage.view.part.getPartId()).toEqual('right'); + + // Drag view to main area part + await viewPage.view.tab.dragTo({partId: initialPartId, region: 'center'}); + await expectView(viewPage).toBeActive(); + await expect.poll(() => viewPage.view.part.getPartId()).toEqual(initialPartId); + + // Expect Angular router not to error. + await expect.poll(() => consoleLogs.get({severity: 'error'})).toHaveLength(0); + }); + + test('should drag "Not Found Page" to a new window', async ({appPO, workbenchNavigator, consoleLogs}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addPart('left', {align: 'left'}) + .addPart('right', {align: 'right'}) + .addView('testee-1', {partId: 'left', activateView: true, cssClass: 'testee-1'}) + .addView('testee-2', {partId: activePartId, cssClass: 'testee-2'}) + .addView('testee-3', {partId: 'right', cssClass: 'testee-3'}) + .navigateView('testee-1', ['does/not/exist']) + .navigateView('testee-2', ['does/not/exist']) + .navigateView('testee-3', [], {hint: 'does-not-match'}), + ); + + const viewPage1 = new PageNotFoundPagePO(appPO, {cssClass: 'testee-1'}); + const viewPage2 = new PageNotFoundPagePO(appPO, {cssClass: 'testee-2'}); + const viewPage3 = new PageNotFoundPagePO(appPO, {cssClass: 'testee-3'}); + + // Move testee-1 view to new window (into main area). + const newAppPO = await viewPage1.view.tab.moveToNewWindow(); + + // Expect testee-1 view to be moved to new window. + const newWindowViewPage1 = new PageNotFoundPagePO(newAppPO, {cssClass: 'testee-1'}); + await expectView(newWindowViewPage1).toBeActive(); + + // Move testee-2 view to existing window (into peripheral area). + await viewPage2.view.tab.moveTo(MAIN_AREA, {region: 'west', workbenchId: await newAppPO.getWorkbenchId()}); + + // Expect testee-2 view to be moved to existing window. + const newWindowViewPage2 = new PageNotFoundPagePO(newAppPO, {cssClass: 'testee-2'}); + await expectView(newWindowViewPage2).toBeActive(); + + // Move testee-3 view to existing window (into peripheral area). + await viewPage3.view.tab.moveTo(MAIN_AREA, {region: 'east', workbenchId: await newAppPO.getWorkbenchId()}); + + // Expect testee-3 view to be moved to existing window. + const newWindowViewPage3 = new PageNotFoundPagePO(newAppPO, {cssClass: 'testee-3'}); + await expectView(newWindowViewPage3).toBeActive(); + + // Expect Angular router not to error. + await expect.poll(() => consoleLogs.get({severity: 'error'})).toHaveLength(0); + await expect.poll(() => new ConsoleLogs(newAppPO.page).get({severity: 'error'})).toHaveLength(0); + }); +}); diff --git a/projects/scion/e2e-testing/src/workbench/view-route-data.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/view-route-data.e2e-spec.ts index 793a9fb99..86395e24c 100644 --- a/projects/scion/e2e-testing/src/workbench/view-route-data.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/view-route-data.e2e-spec.ts @@ -26,10 +26,10 @@ test.describe('View Route Data', () => { await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath(`${basePath}/feature-a`); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate([`${basePath}/feature-a`], { + target: 'blank', + cssClass: 'testee' + }); const view = appPO.view({cssClass: 'testee'}); await expect(view.tab.title).toHaveText('Features Title'); @@ -42,10 +42,10 @@ test.describe('View Route Data', () => { await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath(`${basePath}/feature-b`); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate([`${basePath}/feature-b`], { + target: 'blank', + cssClass: 'testee', + }); const view = appPO.view({cssClass: 'testee'}); await expect(view.tab.title).toHaveText('Feature B Title'); @@ -59,10 +59,10 @@ test.describe('View Route Data', () => { await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath(`${basePath}/feature-c`); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate([`${basePath}/feature-c`], { + target: 'blank', + cssClass: 'testee', + }); const view = appPO.view({cssClass: 'testee'}); await expect(view.tab.title).toHaveText('Features Title'); @@ -75,10 +75,10 @@ test.describe('View Route Data', () => { await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath(`${basePath}/feature-d`); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate([`${basePath}/feature-d`], { + target: 'blank', + cssClass: 'testee', + }); const view = appPO.view({cssClass: 'testee'}); await expect(view.tab.title).toHaveText('Feature D Title'); @@ -97,10 +97,10 @@ test.describe('View Route Data', () => { await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath(`${basePath}/feature-a`); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate([`${basePath}/feature-a`], { + target: 'blank', + cssClass: 'testee', + }); const view = appPO.view({cssClass: 'testee'}); await expect(view.tab.title).toHaveText('Features Lazy Title'); @@ -113,10 +113,10 @@ test.describe('View Route Data', () => { await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath(`${basePath}/feature-b`); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate([`${basePath}/feature-b`], { + target: 'blank', + cssClass: 'testee', + }); const view = appPO.view({cssClass: 'testee'}); await expect(view.tab.title).toHaveText('Feature B Title'); @@ -130,10 +130,10 @@ test.describe('View Route Data', () => { await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath(`${basePath}/feature-c`); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate([`${basePath}/feature-c`], { + target: 'blank', + cssClass: 'testee', + }); const view = appPO.view({cssClass: 'testee'}); await expect(view.tab.title).toHaveText('Features Lazy Title'); @@ -146,10 +146,10 @@ test.describe('View Route Data', () => { await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath(`${basePath}/feature-d`); - await routerPage.enterTarget('blank'); - await routerPage.enterCssClass('testee'); - await routerPage.clickNavigate(); + await routerPage.navigate([`${basePath}/feature-d`], { + target: 'blank', + cssClass: 'testee', + }); const view = appPO.view({cssClass: 'testee'}); await expect(view.tab.title).toHaveText('Feature D Title'); diff --git a/projects/scion/e2e-testing/src/workbench/view-tab-bar.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/view-tab-bar.e2e-spec.ts index 9434cc108..13b914125 100644 --- a/projects/scion/e2e-testing/src/workbench/view-tab-bar.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/view-tab-bar.e2e-spec.ts @@ -14,9 +14,7 @@ import {StartPagePO} from '../start-page.po'; import {RouterPagePO} from './page-object/router-page.po'; import {ViewPagePO} from './page-object/view-page.po'; import {expectView} from '../matcher/view-matcher'; -import {PerspectivePagePO} from './page-object/perspective-page.po'; import {MPart, MTreeNode} from '../matcher/to-equal-workbench-layout.matcher'; -import {LayoutPagePO} from './page-object/layout-page.po'; test.describe('View Tabbar', () => { @@ -25,10 +23,10 @@ test.describe('View Tabbar', () => { const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); // open view-1 - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee-1'); - await routerPage.enterTarget('blank'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + cssClass: 'testee-1', + }); const testee1ViewPage = new ViewPagePO(appPO, {cssClass: 'testee-1'}); @@ -38,10 +36,10 @@ test.describe('View Tabbar', () => { // open view-2 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee-2'); - await routerPage.enterTarget('blank'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + cssClass: 'testee-2', + }); const testee2ViewPage = new ViewPagePO(appPO, {cssClass: 'testee-2'}); @@ -52,10 +50,10 @@ test.describe('View Tabbar', () => { // open view-3 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterCssClass('testee-3'); - await routerPage.enterTarget('blank'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + cssClass: 'testee-3', + }); const testee3ViewPage = new ViewPagePO(appPO, {cssClass: 'testee-3'}); @@ -104,44 +102,32 @@ test.describe('View Tabbar', () => { test('should open new view to the right of the active view', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - // Register Angular routes. - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.registerRoute({path: '', component: 'router-page', outlet: 'router'}); - - const perspectivePage = await workbenchNavigator.openInNewTab(PerspectivePagePO); - await perspectivePage.registerPerspective({ - id: 'perspective', - parts: [ - {id: 'left'}, - {id: 'right', align: 'right', activate: true}, - ], - views: [ - // Add views to the left part. - {id: 'view.1', partId: 'left'}, - {id: 'router', partId: 'left', activateView: true}, // TODO [WB-LAYOUT] Change to view.2 and navigate to router page - {id: 'view.3', partId: 'left'}, - {id: 'view.4', partId: 'left'}, - // Add views to the right part. - {id: 'view.5', partId: 'right', activateView: true}, - {id: 'view.6', partId: 'right'}, - ], - }); - await appPO.switchPerspective('perspective'); + await workbenchNavigator.createPerspective(factory => factory + .addPart('left') + .addPart('right', {align: 'right'}, {activate: true}) + .addView('view.1', {partId: 'left'}) + .addView('view.2', {partId: 'left', activateView: true}) + .addView('view.3', {partId: 'left'}) + .addView('view.4', {partId: 'left'}) + .addView('view.5', {partId: 'right', activateView: true}) + .addView('view.6', {partId: 'right'}) + .navigateView('view.2', ['test-router']), + ); // Open view in the active part (left part). - const routerPage = new RouterPagePO(appPO, {viewId: 'router'}); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.clickNavigate(); + const routerPage = new RouterPagePO(appPO, {viewId: 'view.2'}); + await routerPage.navigate(['test-view'], { + target: 'blank', + }); - // Expect view.2 to be opened to the right of the active view. + // Expect view.7 (new view) to be opened to the right of the active view. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ child1: new MPart({ id: 'left', - views: [{id: 'view.1'}, {id: 'router'}, {id: 'view.2'}, {id: 'view.3'}, {id: 'view.4'}], - activeViewId: 'view.2', + views: [{id: 'view.1'}, {id: 'view.2'}, {id: 'view.7'}, {id: 'view.3'}, {id: 'view.4'}], + activeViewId: 'view.7', }), child2: new MPart({ id: 'right', @@ -157,24 +143,24 @@ test.describe('View Tabbar', () => { // Open view in the right part. await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterBlankPartId('right'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + partId: 'right', + }); - // Expect view.7 to be opened to the right of the active view. + // Expect view.8 (new view) to be opened to the right of the active view. await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ child1: new MPart({ id: 'left', - views: [{id: 'view.1'}, {id: 'router'}, {id: 'view.2'}, {id: 'view.3'}, {id: 'view.4'}], - activeViewId: 'router', + views: [{id: 'view.1'}, {id: 'view.2'}, {id: 'view.7'}, {id: 'view.3'}, {id: 'view.4'}], + activeViewId: 'view.2', }), child2: new MPart({ id: 'right', - views: [{id: 'view.5'}, {id: 'view.7'}, {id: 'view.6'}], - activeViewId: 'view.7', + views: [{id: 'view.5'}, {id: 'view.8'}, {id: 'view.6'}], + activeViewId: 'view.8', }), direction: 'row', ratio: .5, @@ -187,25 +173,16 @@ test.describe('View Tabbar', () => { test('should open view moved via drag & drop after the active view', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const perspectivePage = await workbenchNavigator.openInNewTab(PerspectivePagePO); - await perspectivePage.registerPerspective({ - id: 'perspective', - parts: [ - {id: 'left'}, - {id: 'right', align: 'right', activate: true}, - ], - views: [ - // Add views to the left part. - {id: 'view.1', partId: 'left'}, - {id: 'view.2', partId: 'left'}, - {id: 'view.3', partId: 'left', activateView: true}, - {id: 'view.4', partId: 'left'}, - // Add views to the right part. - {id: 'view.5', partId: 'right', activateView: true}, - {id: 'view.6', partId: 'right'}, - ], - }); - await appPO.switchPerspective('perspective'); + await workbenchNavigator.createPerspective(factory => factory + .addPart('left') + .addPart('right', {align: 'right'}, {activate: true}) + .addView('view.1', {partId: 'left'}) + .addView('view.2', {partId: 'left'}) + .addView('view.3', {partId: 'left', activateView: true}) + .addView('view.4', {partId: 'left'}) + .addView('view.5', {partId: 'right', activateView: true}) + .addView('view.6', {partId: 'right'}), + ); // Move view.5 to the left part const view5 = appPO.view({viewId: 'view.5'}); @@ -236,20 +213,13 @@ test.describe('View Tabbar', () => { test('should activate the view to the left of the view that is dragged out of the tab bar', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const perspectivePage = await workbenchNavigator.openInNewTab(PerspectivePagePO); - await perspectivePage.registerPerspective({ - id: 'perspective', - parts: [ - {id: 'part'}, - ], - views: [ - {id: 'view.1', partId: 'part'}, - {id: 'view.2', partId: 'part'}, - {id: 'view.3', partId: 'part', activateView: true}, - {id: 'view.4', partId: 'part'}, - ], - }); - await appPO.switchPerspective('perspective'); + await workbenchNavigator.createPerspective(factory => factory + .addPart('part') + .addView('view.1', {partId: 'part'}) + .addView('view.2', {partId: 'part'}) + .addView('view.3', {partId: 'part', activateView: true}) + .addView('view.4', {partId: 'part'}), + ); // Drag view.3 out of the tabbar. const view3 = appPO.view({viewId: 'view.3'}); @@ -271,20 +241,13 @@ test.describe('View Tabbar', () => { test('should not change the view order when dragging a view to its own part (noop)', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const perspectivePage = await workbenchNavigator.openInNewTab(PerspectivePagePO); - await perspectivePage.registerPerspective({ - id: 'perspective', - parts: [ - {id: 'part'}, - ], - views: [ - {id: 'view.1', partId: 'part'}, - {id: 'view.2', partId: 'part'}, - {id: 'view.3', partId: 'part', activateView: true}, - {id: 'view.4', partId: 'part'}, - ], - }); - await appPO.switchPerspective('perspective'); + await workbenchNavigator.createPerspective(factory => factory + .addPart('part') + .addView('view.1', {partId: 'part'}) + .addView('view.2', {partId: 'part'}) + .addView('view.3', {partId: 'part', activateView: true}) + .addView('view.4', {partId: 'part'}), + ); // Drag view.3 to its own part. const view3 = appPO.view({viewId: 'view.3'}); @@ -306,20 +269,13 @@ test.describe('View Tabbar', () => { test('should cancel drag operation if pressing escape', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); - const perspectivePage = await workbenchNavigator.openInNewTab(PerspectivePagePO); - await perspectivePage.registerPerspective({ - id: 'perspective', - parts: [ - {id: 'part'}, - ], - views: [ - {id: 'view.1', partId: 'part'}, - {id: 'view.2', partId: 'part'}, - {id: 'view.3', partId: 'part', activateView: true}, - {id: 'view.4', partId: 'part'}, - ], - }); - await appPO.switchPerspective('perspective'); + await workbenchNavigator.createPerspective(factory => factory + .addPart('part') + .addView('view.1', {partId: 'part'}) + .addView('view.2', {partId: 'part'}) + .addView('view.3', {partId: 'part', activateView: true}) + .addView('view.4', {partId: 'part'}), + ); // Drag view.3 out of the tabbar. const view3 = appPO.view({viewId: 'view.3'}); @@ -346,10 +302,10 @@ test.describe('View Tabbar', () => { const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); // open view.2 - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterInsertionIndex('end'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + position: 'end', + }); const testee2ViewPage = new ViewPagePO(appPO, {viewId: 'view.2'}); @@ -359,10 +315,10 @@ test.describe('View Tabbar', () => { // open view.3 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterInsertionIndex('end'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + position: 'end', + }); const testee3ViewPage = new ViewPagePO(appPO, {viewId: 'view.3'}); @@ -373,10 +329,10 @@ test.describe('View Tabbar', () => { // open view.4 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterInsertionIndex('end'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + position: 'end', + }); const testee4ViewPage = new ViewPagePO(appPO, {viewId: 'view.4'}); @@ -392,10 +348,10 @@ test.describe('View Tabbar', () => { const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); // open view.2 - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterInsertionIndex('start'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + position: 'start', + }); const testee2ViewPage = new ViewPagePO(appPO, {viewId: 'view.2'}); @@ -405,10 +361,10 @@ test.describe('View Tabbar', () => { // open view.3 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterInsertionIndex('start'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + position: 'start', + }); const testee3ViewPage = new ViewPagePO(appPO, {viewId: 'view.3'}); @@ -419,10 +375,10 @@ test.describe('View Tabbar', () => { // open view.4 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterInsertionIndex('start'); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + position: 'start', + }); const testee4ViewPage = new ViewPagePO(appPO, {viewId: 'view.4'}); @@ -438,10 +394,10 @@ test.describe('View Tabbar', () => { const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); // open view.2 - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterInsertionIndex(1); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + position: 1, + }); const testee2ViewPage = new ViewPagePO(appPO, {viewId: 'view.2'}); @@ -451,10 +407,10 @@ test.describe('View Tabbar', () => { // open view.3 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterInsertionIndex(1); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + position: 1, + }); const testee3ViewPage = new ViewPagePO(appPO, {viewId: 'view.3'}); @@ -465,10 +421,10 @@ test.describe('View Tabbar', () => { // open view.4 await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget('blank'); - await routerPage.enterInsertionIndex(1); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view'], { + target: 'blank', + position: 1, + }); const testee4ViewPage = new ViewPagePO(appPO, {viewId: 'view.4'}); diff --git a/projects/scion/e2e-testing/src/workbench/view.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/view.e2e-spec.ts index 9e7234eae..0a5b9f40b 100644 --- a/projects/scion/e2e-testing/src/workbench/view.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/view.e2e-spec.ts @@ -13,6 +13,11 @@ import {test} from '../fixtures'; import {RouterPagePO} from './page-object/router-page.po'; import {ViewPagePO} from './page-object/view-page.po'; import {expectView} from '../matcher/view-matcher'; +import {NavigationTestPagePO} from './page-object/test-pages/navigation-test-page.po'; +import {MPart, MTreeNode} from '../matcher/to-equal-workbench-layout.matcher'; +import {MAIN_AREA} from '../workbench.model'; +import {WorkbenchNavigator} from './workbench-navigator'; +import {StartPagePO} from '../start-page.po'; test.describe('Workbench View', () => { @@ -20,14 +25,14 @@ test.describe('Workbench View', () => { await appPO.navigateTo({microfrontendSupport: false}); const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); - await routerPage.enterPath('/test-view'); - await routerPage.enterCssClass('testee'); - await routerPage.enterTarget('view.99'); - await routerPage.clickNavigate(); + await routerPage.navigate(['/test-view'], { + target: 'view.100', + cssClass: 'testee', + }); const viewPage = new ViewPagePO(appPO, {cssClass: 'testee'}); - await expect(viewPage.viewId).toHaveText('view.99'); + await expect(viewPage.viewId).toHaveText('view.100'); }); test('should allow updating the view tab title', async ({appPO, workbenchNavigator}) => { @@ -41,6 +46,34 @@ test.describe('Workbench View', () => { await expect(viewPage.view.tab.title).toHaveText('title'); }); + test('should show title of inactive views when reloading the application', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // open test view 1 + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate(['test-pages/navigation-test-page', {title: 'view-1-title'}], { + target: 'view.101', + }); + + // open test view 2 + await routerPage.view.tab.click(); + await routerPage.navigate(['test-pages/navigation-test-page', {title: 'view-2-title'}], { + target: 'view.102', + }); + + const testee1ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.101'}); + const testee2ViewPage = new NavigationTestPagePO(appPO, {viewId: 'view.102'}); + + // reload the application + await appPO.reload(); + + await expectView(testee1ViewPage).toBeInactive(); + await expect(testee1ViewPage.view.tab.title).toHaveText('view-1-title'); + + await expectView(testee2ViewPage).toBeActive(); + await expect(testee2ViewPage.view.tab.title).toHaveText('view-2-title'); + }); + test('should allow updating the view tab heading', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); await appPO.setDesignToken('--sci-workbench-tab-height', '3.5rem'); @@ -109,9 +142,9 @@ test.describe('Workbench View', () => { // Navigate to a different route in the same view await routerPage.view.tab.click(); - await routerPage.enterPath('test-router'); - await routerPage.enterTarget(viewId); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-router'], { + target: viewId, + }); // Expect the view to be pristine const testeeView = appPO.view({viewId}); @@ -129,10 +162,9 @@ test.describe('Workbench View', () => { // Update matrix params (does not affect routing) await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget(await viewPage.view.getViewId()); - await routerPage.enterMatrixParams({matrixParam: 'value'}); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {matrixParam: 'value'}], { + target: await viewPage.view.getViewId(), + }); // Expect the view to still be dirty await expect.poll(() => viewPage.view.tab.isDirty()).toBe(true); @@ -153,10 +185,9 @@ test.describe('Workbench View', () => { // Update matrix params (does not affect routing) await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterTarget(await viewPage.view.getViewId()); - await routerPage.enterMatrixParams({matrixParam: 'value'}); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {matrixParam: 'value'}], { + target: await viewPage.view.getViewId(), + }); // Expect the title has not changed await expect(viewPage.view.tab.title).toHaveText('TITLE'); @@ -178,9 +209,7 @@ test.describe('Workbench View', () => { // Update matrix params (does not affect routing) await routerPage.view.tab.click(); - await routerPage.enterPath('test-view'); - await routerPage.enterMatrixParams({matrixParam: 'value'}); - await routerPage.clickNavigate(); + await routerPage.navigate(['test-view', {matrixParam: 'value'}]); // Expect the heading has not changed await expect(viewPage.view.tab.heading).toHaveText('HEADING'); @@ -271,7 +300,7 @@ test.describe('Workbench View', () => { consoleLogs.clear(); }); - test('should allow to close the view', async ({appPO, workbenchNavigator}) => { + test('should close a view', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); await viewPage.clickClose(); @@ -280,6 +309,128 @@ test.describe('Workbench View', () => { await expectView(viewPage).not.toBeAttached(); }); + test('should prevent closing a view', async ({appPO, page, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + const testeeViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + + // Prevent the view from closing. + await testeeViewPage.checkConfirmClosing(true); + + // Close view via view tab (prevent). + await testeeViewPage.view.tab.close(); + const canCloseMessageBox = appPO.messagebox({cssClass: ['e2e-close-view', await testeeViewPage.view.getViewId()]}); + await canCloseMessageBox.clickActionButton('no'); + await expectView(testeeViewPage).toBeActive(); + + // Close view via WorkbenchView handle (prevent). + await testeeViewPage.clickClose(); + await canCloseMessageBox.clickActionButton('no'); + await expectView(testeeViewPage).toBeActive(); + + // Close view via close keystroke (prevent). + await testeeViewPage.view.tab.click(); + await page.keyboard.press('Control+K'); + await canCloseMessageBox.clickActionButton('no'); + await expectView(testeeViewPage).toBeActive(); + + // Close all views via close keystroke (prevent). + await testeeViewPage.view.tab.click(); + await page.keyboard.press('Control+Shift+Alt+K'); + await canCloseMessageBox.clickActionButton('no'); + await expectView(testeeViewPage).toBeActive(); + + // Close view via router (prevent). + // Do not wait for the navigation to complete because the message box blocks navigation. + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + await routerPage.navigate([], {target: await testeeViewPage.view.getViewId(), close: true, waitForNavigation: false}); + await canCloseMessageBox.clickActionButton('no'); + await expectView(testeeViewPage).toBeInactive(); + + // Close all views via router (prevent). + // Do not wait for the navigation to complete because the message box blocks navigation. + await routerPage.navigate(['test-view'], {close: true, waitForNavigation: false}); + await canCloseMessageBox.clickActionButton('no'); + await expectView(testeeViewPage).toBeInactive(); + + // Close view. + await testeeViewPage.view.tab.close(); + await canCloseMessageBox.clickActionButton('yes'); + await expectView(testeeViewPage).not.toBeAttached(); + }); + + test('should close confirmed views, leaving other views open', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open test view 1. + const testee1ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + await testee1ViewPage.checkConfirmClosing(true); // prevent the view from closing + + // Open test view 2. + const testee2ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + await testee2ViewPage.checkConfirmClosing(true); // prevent the view from closing + + // Open test view 3. + const testee3ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + + // Close all views. + const contextMenu = await testee3ViewPage.view.tab.openContextMenu(); + await contextMenu.menuItems.closeAll.click(); + + // Expect all views to still be opened. + await expect(appPO.views()).toHaveCount(3); + + // Confirm closing view 1. + const canCloseMessageBox1 = appPO.messagebox({cssClass: ['e2e-close-view', await testee1ViewPage.view.getViewId()]}); + await canCloseMessageBox1.clickActionButton('yes'); + + // Prevent closing view 2. + const canCloseMessageBox2 = appPO.messagebox({cssClass: ['e2e-close-view', await testee2ViewPage.view.getViewId()]}); + await canCloseMessageBox2.clickActionButton('no'); + + // Expect view 1 and view 3 to be closed. + await expectView(testee1ViewPage).not.toBeAttached(); + await expectView(testee2ViewPage).toBeActive(); + await expectView(testee3ViewPage).not.toBeAttached(); + }); + + test('should close view and log error if `CanClose` guard errors', async ({appPO, workbenchNavigator, consoleLogs}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open test view 1. + const testee1ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + await testee1ViewPage.checkConfirmClosing(true); // prevent the view from closing + + // Open test view 2. + const testee2ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + await testee2ViewPage.checkConfirmClosing(true); // prevent the view from closing + + // Open test view 3. + const testee3ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + + // Close all views. + const contextMenu = await testee3ViewPage.view.tab.openContextMenu(); + await contextMenu.menuItems.closeAll.click(); + + // Expect all views to still be opened. + await expect(appPO.views()).toHaveCount(3); + + // Simulate `CanClose` guard of view 1 to error. + const canCloseMessageBox1 = appPO.messagebox({cssClass: ['e2e-close-view', await testee1ViewPage.view.getViewId()]}); + await canCloseMessageBox1.clickActionButton('error'); + + // Prevent closing view 2. + const canCloseMessageBox2 = appPO.messagebox({cssClass: ['e2e-close-view', await testee2ViewPage.view.getViewId()]}); + await canCloseMessageBox2.clickActionButton('no'); + + // Expect view 1 and view 3 to be closed. + await expectView(testee1ViewPage).not.toBeAttached(); + await expectView(testee2ViewPage).toBeActive(); + await expectView(testee3ViewPage).not.toBeAttached(); + + await expect.poll(() => consoleLogs.contains({severity: 'error', message: /\[CanCloseSpecError] Error in CanLoad of view 'view\.1'\./})).toBe(true); + }); + test(`should disable context menu 'Close tab' for 'non-closable' view`, async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); @@ -320,6 +471,241 @@ test.describe('Workbench View', () => { await expectView(viewPage2).toBeActive(); }); + /** + * Tests to unset the "markedForRemoval" flag after navigation, i.e., that a subsequent layout operation does not invoke the `CanClose` guard again. + */ + test('should unset `markedForRemoval` flag after navigation', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open test view. + const testeeViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + + // Prevent the view from closing. + await testeeViewPage.checkConfirmClosing(true); + + // Try closing the view. + await testeeViewPage.view.tab.close(); + + // Prevent closing the view. + const canCloseMessageBox = appPO.messagebox({cssClass: ['e2e-close-view', await testeeViewPage.view.getViewId()]}); + await canCloseMessageBox.clickActionButton('no'); + + // Expect view not to be closed. + await expectView(testeeViewPage).toBeActive(); + + // Perform navigation after prevented closing. + await workbenchNavigator.openInNewTab(ViewPagePO); + + // Expect `CanClose` guard not to be invoked. + await expect(appPO.messagebox({cssClass: ['e2e-close-view', await testeeViewPage.view.getViewId()]}).locator).not.toBeAttached(); + }); + + test('should not invoke `CanClose` guard when dragging view in the same layout in the main area', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open test view 1. + const testee1ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + const testee1ViewId = await testee1ViewPage.view.getViewId(); + + // Open test view 2. + const testee2ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + const testee2ViewId = await testee2ViewPage.view.getViewId(); + + // Prevent the view from closing. + await testee2ViewPage.checkConfirmClosing(true); + + // Test `CanClose` guard to be installed. + await testee2ViewPage.view.tab.close(); + const canCloseMessageBox = appPO.messagebox({cssClass: ['e2e-close-view', testee2ViewId]}); + await canCloseMessageBox.clickActionButton('no'); + await expectView(testee2ViewPage).toBeActive(); + + // Drag view in the layout. + await testee2ViewPage.view.tab.dragTo({partId: await testee2ViewPage.view.part.getPartId(), region: 'east'}); + + // Expect `CanClose` guard not to be invoked. + await expect(appPO.messagebox({cssClass: ['e2e-close-view', testee2ViewId]}).locator).not.toBeAttached(); + + // Expect view to be dragged. + await expectView(testee2ViewPage).toBeActive(); + await expect(appPO.workbench).toEqualWorkbenchLayout({ + mainAreaGrid: { + root: new MTreeNode({ + direction: 'row', + ratio: .5, + child1: new MPart({ + views: [{id: testee1ViewId}], + activeViewId: testee1ViewId, + }), + child2: new MPart({ + views: [{id: testee2ViewId}], + activeViewId: testee2ViewId, + }), + }), + }, + }); + }); + + test('should not invoke `CanClose` guard when dragging view in the same layout into the peripheral area', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open test view 1. + const testee1ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + const testee1ViewId = await testee1ViewPage.view.getViewId(); + + // Open test view 2. + const testee2ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + const testee2ViewId = await testee2ViewPage.view.getViewId(); + + // Prevent the view from closing. + await testee2ViewPage.checkConfirmClosing(true); + + // Test `CanClose` guard to be installed. + await testee2ViewPage.view.tab.close(); + const canCloseMessageBox = appPO.messagebox({cssClass: ['e2e-close-view', testee2ViewId]}); + await canCloseMessageBox.clickActionButton('no'); + await expectView(testee2ViewPage).toBeActive(); + + // Drag view in the layout. + await testee2ViewPage.view.tab.dragTo({grid: 'workbench', region: 'east'}); + + // Expect `CanClose` guard not to be invoked. + await expect(appPO.messagebox({cssClass: ['e2e-close-view', testee2ViewId]}).locator).not.toBeAttached(); + + // Expect view to be dragged. + await expectView(testee2ViewPage).toBeActive(); + await expect(appPO.workbench).toEqualWorkbenchLayout({ + workbenchGrid: { + root: new MTreeNode({ + direction: 'row', + ratio: .8, + child1: new MPart({id: MAIN_AREA}), + child2: new MPart({ + views: [{id: testee2ViewId}], + activeViewId: testee2ViewId, + }), + }), + }, + mainAreaGrid: { + root: new MPart({ + views: [{id: testee1ViewId}], + activeViewId: testee1ViewId, + }), + }, + }); + }); + + test('should not invoke `CanClose` guard when dragging view to a new window', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open test view 1. + const testee1ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + const testee1ViewId = await testee1ViewPage.view.getViewId(); + + // Open test view 2. + const testee2ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + const testee2ViewId = await testee2ViewPage.view.getViewId(); + + // Prevent the view from closing. + await testee2ViewPage.checkConfirmClosing(true); + + // Test `CanClose` guard to be installed. + await testee2ViewPage.view.tab.close(); + const canCloseMessageBox = appPO.messagebox({cssClass: ['e2e-close-view', testee2ViewId]}); + await canCloseMessageBox.clickActionButton('no'); + await expectView(testee2ViewPage).toBeActive(); + + // Move view to new window + const newAppPO = await testee2ViewPage.view.tab.moveToNewWindow(); + const newWindow = { + appPO: newAppPO, + workbenchNavigator: new WorkbenchNavigator(newAppPO), + }; + + // Expect `CanClose` guard not to be invoked. + await expect(appPO.messagebox({cssClass: ['e2e-close-view', testee2ViewId]}).locator).not.toBeAttached(); + + // Expect view to be moved to the new window. + await expect(newAppPO.workbench).toEqualWorkbenchLayout({ + mainAreaGrid: { + root: new MPart({ + views: [{id: 'view.1'}], + activeViewId: 'view.1', + }), + }, + }); + await expectView(new ViewPagePO(newWindow.appPO, {viewId: 'view.1'})).toBeActive(); + + // Expect view to be removed from the origin window. + await expect(appPO.workbench).toEqualWorkbenchLayout({ + mainAreaGrid: { + root: new MPart({ + views: [{id: testee1ViewId}], + activeViewId: testee1ViewId, + }), + }, + }); + await expectView(testee1ViewPage).toBeActive(); + await expectView(testee2ViewPage).not.toBeAttached(); + }); + + test('should not invoke `CanClose` guard when dragging view to another window', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + // Open test view 1. + const testee1ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + const testee1ViewId = await testee1ViewPage.view.getViewId(); + + // Open test view 2. + const testee2ViewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + const testee2ViewId = await testee2ViewPage.view.getViewId(); + + // Prevent the view from closing. + await testee2ViewPage.checkConfirmClosing(true); + + // Test `CanClose` guard to be installed. + await testee2ViewPage.view.tab.close(); + const canCloseMessageBox = appPO.messagebox({cssClass: ['e2e-close-view', testee2ViewId]}); + await canCloseMessageBox.clickActionButton('no'); + await expectView(testee2ViewPage).toBeActive(); + + // Open new browser window. + const newAppPO = await appPO.openNewWindow(); + await newAppPO.navigateTo({microfrontendSupport: false}); + + // Move view to new browser window. + const startPagePartId = (await new StartPagePO(newAppPO).getPartId())!; + await testee2ViewPage.view.tab.moveTo(startPagePartId, { + workbenchId: await newAppPO.getWorkbenchId(), + }); + + // Expect `CanClose` guard not to be invoked. + await expect(appPO.messagebox({cssClass: ['e2e-close-view', testee2ViewId]}).locator).not.toBeAttached(); + + // Expect view to be moved to the new window. + await expect(newAppPO.workbench).toEqualWorkbenchLayout({ + mainAreaGrid: { + root: new MPart({ + views: [{id: 'view.1'}], + activeViewId: 'view.1', + }), + }, + }); + await expectView(new ViewPagePO(newAppPO, {viewId: 'view.1'})).toBeActive(); + + // Expect view to be removed from the origin window. + await expect(appPO.workbench).toEqualWorkbenchLayout({ + mainAreaGrid: { + root: new MPart({ + views: [{id: testee1ViewId}], + activeViewId: testee1ViewId, + }), + }, + }); + await expectView(testee1ViewPage).toBeActive(); + await expectView(testee2ViewPage).not.toBeAttached(); + }); + test('should detach view if not active', async ({appPO, workbenchNavigator}) => { await appPO.navigateTo({microfrontendSupport: false}); @@ -368,4 +754,26 @@ test.describe('Workbench View', () => { // Expect view 2 not to be instantiated anew await expect.poll(() => viewPage2.getComponentInstanceId()).toEqual(view2ComponentId); }); + + test('should not destroy the component of the view when it is inactivated', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + const routerPage = await workbenchNavigator.openInNewTab(RouterPagePO); + const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); + + const componentInstanceId = await viewPage.getComponentInstanceId(); + + // activate the router test view + await routerPage.view.tab.click(); + await expectView(routerPage).toBeActive(); + await expectView(viewPage).toBeInactive(); + + // activate the test view + await viewPage.view.tab.click(); + await expectView(viewPage).toBeActive(); + await expectView(routerPage).toBeInactive(); + + // expect the component not to be constructed anew + await expect.poll(() => viewPage.getComponentInstanceId()).toEqual(componentInstanceId); + }); }); diff --git a/projects/scion/e2e-testing/src/workbench/workbench-layout-migration.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/workbench-layout-migration.e2e-spec.ts index da8893159..4b97c47e0 100644 --- a/projects/scion/e2e-testing/src/workbench/workbench-layout-migration.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/workbench-layout-migration.e2e-spec.ts @@ -10,9 +10,12 @@ import {expect} from '@playwright/test'; import {test} from '../fixtures'; -import {WorkenchStartupQueryParams} from '../app.po'; import {MPart, MTreeNode} from '../matcher/to-equal-workbench-layout.matcher'; import {MAIN_AREA} from '../workbench.model'; +import {expectView} from '../matcher/view-matcher'; +import {ViewPagePO} from './page-object/view-page.po'; +import {RouterPagePO} from './page-object/router-page.po'; +import {ViewInfo} from './page-object/view-info-dialog.po'; test.describe('Workbench Layout Migration', () => { @@ -23,9 +26,11 @@ test.describe('Workbench Layout Migration', () => { * | Active View: view.1 | Active View: view.3 | * +--------------------------------------------+--------------------------------------------+ */ - test('should migrate workbench layout v1 to the latest version', async ({page, appPO}) => { - await page.goto(`/?${WorkenchStartupQueryParams.STANDALONE}=true/#/(view.3:test-view//view.2:test-view//view.1:test-view)?parts=eyJyb290Ijp7Im5vZGVJZCI6IjhkMWQ4MzA1LTgxYzItNDllOC05NWE3LWFlYjNlODM1ODFhMSIsImNoaWxkMSI6eyJ2aWV3SWRzIjpbInZpZXcuMSJdLCJwYXJ0SWQiOiIzOGY5MTU0MS03ZmRjLTRjNzEtYmVjMi0xZDVhZDc1MjNiZWUiLCJhY3RpdmVWaWV3SWQiOiJ2aWV3LjEifSwiY2hpbGQyIjp7InZpZXdJZHMiOlsidmlldy4yIiwidmlldy4zIl0sInBhcnRJZCI6ImZlZDM4MDExLTY2YjctNDZjZC1iYjQyLTMwY2U2ZjBmODA3MSIsImFjdGl2ZVZpZXdJZCI6InZpZXcuMyJ9LCJkaXJlY3Rpb24iOiJyb3ciLCJyYXRpbyI6MC41fSwiYWN0aXZlUGFydElkIjoiMzhmOTE1NDEtN2ZkYy00YzcxLWJlYzItMWQ1YWQ3NTIzYmVlIiwidXVpZCI6IjFlMjIzN2U1LWE3MzAtNDk1NC1iYWJmLWNkMzRjMjM3OWI1ZSJ9`); - await appPO.waitUntilWorkbenchStarted(); + test('should migrate workbench layout v1 to the latest version', async ({appPO}) => { + await appPO.navigateTo({ + url: '#/(view.3:test-view//view.2:test-view//view.1:test-view)?parts=eyJyb290Ijp7Im5vZGVJZCI6IjhkMWQ4MzA1LTgxYzItNDllOC05NWE3LWFlYjNlODM1ODFhMSIsImNoaWxkMSI6eyJ2aWV3SWRzIjpbInZpZXcuMSJdLCJwYXJ0SWQiOiIzOGY5MTU0MS03ZmRjLTRjNzEtYmVjMi0xZDVhZDc1MjNiZWUiLCJhY3RpdmVWaWV3SWQiOiJ2aWV3LjEifSwiY2hpbGQyIjp7InZpZXdJZHMiOlsidmlldy4yIiwidmlldy4zIl0sInBhcnRJZCI6ImZlZDM4MDExLTY2YjctNDZjZC1iYjQyLTMwY2U2ZjBmODA3MSIsImFjdGl2ZVZpZXdJZCI6InZpZXcuMyJ9LCJkaXJlY3Rpb24iOiJyb3ciLCJyYXRpbyI6MC41fSwiYWN0aXZlUGFydElkIjoiMzhmOTE1NDEtN2ZkYy00YzcxLWJlYzItMWQ1YWQ3NTIzYmVlIiwidXVpZCI6IjFlMjIzN2U1LWE3MzAtNDk1NC1iYWJmLWNkMzRjMjM3OWI1ZSJ9', + microfrontendSupport: false, + }); await expect(appPO.workbench).toEqualWorkbenchLayout({ workbenchGrid: { @@ -54,5 +59,174 @@ test.describe('Workbench Layout Migration', () => { activePartId: '38f91541-7fdc-4c71-bec2-1d5ad7523bee', }, }); + + const viewPage1 = new ViewPagePO(appPO, {viewId: 'view.1'}); + await expectView(viewPage1).toBeActive(); + + const viewPage2 = new ViewPagePO(appPO, {viewId: 'view.2'}); + await expectView(viewPage2).toBeInactive(); + + const viewPage3 = new ViewPagePO(appPO, {viewId: 'view.3'}); + await expectView(viewPage3).toBeActive(); + + await expect.poll(() => viewPage1.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: ''}, + } satisfies Partial, + ); + + await expect.poll(() => viewPage2.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: ''}, + } satisfies Partial, + ); + + await expect.poll(() => viewPage3.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: ''}, + } satisfies Partial, + ); + }); + + /** + * ## Given layout in version 2: + * + * PERIPHERAL AREA MAIN AREA PERIPHERAL AREA + * +--------------------------------------------+ +--------------------------------------------+ +--------------------------------------------+ + * | Part: 33b22f60-bf34-4704-885d-7de0d707430f | | Part: a25eb4cf-9da7-43e7-8db2-302fd38e59a1 | | Part: 9bc4c09f-67a7-4c69-a28b-532781a1c98f | + * | Views: [view.3] | | Views: [view.1, test-view] | | Views: [test-router] | + * | Active View: view.3 | | Active View: test-view | | Active View: test-router | + * | | +--------------------------------------------+ | | + * | | | Part: 2b534d97-ed7d-43b3-bb2c-0e59d9766e86 | | | + * | | | Views: [view.2] | | | + * | | | Active View: view.2 | | | + * +--------------------------------------------+ +--------------------------------------------+ +--------------------------------------------+ + * view.1: [path='test-view'] + * view.2: [path='test-view'] + * test-view: [path='', outlet='test-view'] + * test-router: [path='', outlet='test-router'] + * view.3: [path='test-view'] + * + * ## Migrated layout: + * + * PERIPHERAL AREA MAIN AREA PERIPHERAL AREA + * +--------------------------------------------+ +--------------------------------------------+ +--------------------------------------------+ + * | Part: 33b22f60-bf34-4704-885d-7de0d707430f | | Part: a25eb4cf-9da7-43e7-8db2-302fd38e59a1 | | Part: 9bc4c09f-67a7-4c69-a28b-532781a1c98f | + * | Views: [view.3] | | Views: [view.1, view.4] | | Views: [view.5] | + * | Active View: view.3 | | Active View: view.4 | | Active View: view.5 | + * | | +--------------------------------------------+ | | + * | | | Part: 2b534d97-ed7d-43b3-bb2c-0e59d9766e86 | | | + * | | | Views: [view.2] | | | + * | | | Active View: view.2 | | | + * +--------------------------------------------+ +--------------------------------------------+ +--------------------------------------------+ + * view.1: [path='test-view'] + * view.2: [path='test-view'] + * view.3: [path='test-view'] + * view.4: [path='', navigationHint='test-view'] + * view.5: [path='', navigationHint='test-router'] + */ + test('should migrate workbench layout v2 to the latest version', async ({appPO}) => { + await appPO.navigateTo({ + url: '#/(view.1:test-view//view.2:test-view//view.3:test-view)?main_area=eyJyb290Ijp7InR5cGUiOiJNVHJlZU5vZGUiLCJjaGlsZDEiOnsidHlwZSI6Ik1QYXJ0Iiwidmlld3MiOlt7ImlkIjoidmlldy4xIn0seyJpZCI6InRlc3QtdmlldyJ9XSwiaWQiOiJhMjVlYjRjZi05ZGE3LTQzZTctOGRiMi0zMDJmZDM4ZTU5YTEiLCJzdHJ1Y3R1cmFsIjpmYWxzZSwiYWN0aXZlVmlld0lkIjoidGVzdC12aWV3In0sImNoaWxkMiI6eyJ0eXBlIjoiTVBhcnQiLCJ2aWV3cyI6W3siaWQiOiJ2aWV3LjIifV0sImlkIjoiMmI1MzRkOTctZWQ3ZC00M2IzLWJiMmMtMGU1OWQ5NzY2ZTg2Iiwic3RydWN0dXJhbCI6ZmFsc2UsImFjdGl2ZVZpZXdJZCI6InZpZXcuMiJ9LCJkaXJlY3Rpb24iOiJjb2x1bW4iLCJyYXRpbyI6MC41fSwiYWN0aXZlUGFydElkIjoiYTI1ZWI0Y2YtOWRhNy00M2U3LThkYjItMzAyZmQzOGU1OWExIn0vLzI%3D', + microfrontendSupport: false, + localStorage: { + 'scion.workbench.perspective': 'empty', + 'scion.workbench.perspectives.empty': 'eyJpbml0aWFsV29ya2JlbmNoR3JpZCI6ImV5SnliMjkwSWpwN0luUjVjR1VpT2lKTlVHRnlkQ0lzSW5acFpYZHpJanBiWFN3aWFXUWlPaUp0WVdsdUxXRnlaV0VpTENKemRISjFZM1IxY21Gc0lqcDBjblZsZlN3aVlXTjBhWFpsVUdGeWRFbGtJam9pYldGcGJpMWhjbVZoSW4wdkx6ST0iLCJ3b3JrYmVuY2hHcmlkIjoiZXlKeWIyOTBJanA3SW5SNWNHVWlPaUpOVkhKbFpVNXZaR1VpTENKamFHbHNaREVpT25zaWRIbHdaU0k2SWsxVWNtVmxUbTlrWlNJc0ltTm9hV3hrTVNJNmV5SjBlWEJsSWpvaVRWQmhjblFpTENKMmFXVjNjeUk2VzNzaWFXUWlPaUoyYVdWM0xqTWlmVjBzSW1sa0lqb2lNek5pTWpKbU5qQXRZbVl6TkMwME56QTBMVGc0TldRdE4yUmxNR1EzTURjME16Qm1JaXdpYzNSeWRXTjBkWEpoYkNJNlptRnNjMlVzSW1GamRHbDJaVlpwWlhkSlpDSTZJblpwWlhjdU15SjlMQ0pqYUdsc1pESWlPbnNpZEhsd1pTSTZJazFRWVhKMElpd2lkbWxsZDNNaU9sdGRMQ0pwWkNJNkltMWhhVzR0WVhKbFlTSXNJbk4wY25WamRIVnlZV3dpT25SeWRXVjlMQ0prYVhKbFkzUnBiMjRpT2lKeWIzY2lMQ0p5WVhScGJ5STZNQzR5ZlN3aVkyaHBiR1F5SWpwN0luUjVjR1VpT2lKTlVHRnlkQ0lzSW5acFpYZHpJanBiZXlKcFpDSTZJblJsYzNRdGNtOTFkR1Z5SW4xZExDSnBaQ0k2SWpsaVl6UmpNRGxtTFRZM1lUY3ROR00yT1MxaE1qaGlMVFV6TWpjNE1XRXhZems0WmlJc0luTjBjblZqZEhWeVlXd2lPbVpoYkhObExDSmhZM1JwZG1WV2FXVjNTV1FpT2lKMFpYTjBMWEp2ZFhSbGNpSjlMQ0prYVhKbFkzUnBiMjRpT2lKeWIzY2lMQ0p5WVhScGJ5STZNQzQ0ZlN3aVlXTjBhWFpsVUdGeWRFbGtJam9pTXpOaU1qSm1OakF0WW1Zek5DMDBOekEwTFRnNE5XUXROMlJsTUdRM01EYzBNekJtSW4wdkx6ST0iLCJ2aWV3T3V0bGV0cyI6eyJ2aWV3LjMiOlsidGVzdC12aWV3Il19fQ==', + }, + }); + + await expect(appPO.workbench).toEqualWorkbenchLayout({ + workbenchGrid: { + root: new MTreeNode({ + direction: 'row', + ratio: .8, + child1: new MTreeNode({ + direction: 'row', + ratio: .2, + child1: new MPart({ + id: '33b22f60-bf34-4704-885d-7de0d707430f', + views: [{id: 'view.3'}], + activeViewId: 'view.3', + }), + child2: new MPart({ + id: MAIN_AREA, + }), + }), + child2: new MPart({ + id: '9bc4c09f-67a7-4c69-a28b-532781a1c98f', + views: [{id: 'view.5'}], + activeViewId: 'view.5', + }), + }), + activePartId: '33b22f60-bf34-4704-885d-7de0d707430f', + }, + mainAreaGrid: { + root: new MTreeNode({ + direction: 'column', + ratio: .5, + child1: new MPart({ + id: 'a25eb4cf-9da7-43e7-8db2-302fd38e59a1', + views: [{id: 'view.1'}, {id: 'view.4'}], + activeViewId: 'view.4', + }), + child2: new MPart({ + id: '2b534d97-ed7d-43b3-bb2c-0e59d9766e86', + views: [{id: 'view.2'}], + activeViewId: 'view.2', + }), + }), + activePartId: 'a25eb4cf-9da7-43e7-8db2-302fd38e59a1', + }, + }); + + const viewPage1 = new ViewPagePO(appPO, {viewId: 'view.1'}); + await viewPage1.view.tab.click(); + await expectView(viewPage1).toBeActive(); + await expect.poll(() => viewPage1.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: ''}, + urlSegments: 'test-view', + } satisfies Partial, + ); + + const viewPage2 = new ViewPagePO(appPO, {viewId: 'view.2'}); + await viewPage2.view.tab.click(); + await expectView(viewPage2).toBeActive(); + await expect.poll(() => viewPage2.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: ''}, + urlSegments: 'test-view', + } satisfies Partial, + ); + + const viewPage3 = new ViewPagePO(appPO, {viewId: 'view.3'}); + await viewPage3.view.tab.click(); + await expectView(viewPage3).toBeActive(); + await expect.poll(() => viewPage3.view.getInfo()).toMatchObject( + { + routeData: {path: 'test-view', navigationHint: ''}, + urlSegments: 'test-view', + } satisfies Partial, + ); + + const viewPage4 = new ViewPagePO(appPO, {viewId: 'view.4'}); + await viewPage4.view.tab.click(); + await expectView(viewPage4).toBeActive(); + await expect.poll(() => viewPage4.view.getInfo()).toMatchObject( + { + routeData: {path: '', navigationHint: 'test-view'}, + urlSegments: '', + } satisfies Partial, + ); + + const viewPage5 = new RouterPagePO(appPO, {viewId: 'view.5'}); + await viewPage5.view.tab.click(); + await expectView(viewPage5).toBeActive(); + await expect.poll(() => viewPage5.view.getInfo()).toMatchObject( + { + routeData: {path: '', navigationHint: 'test-router'}, + urlSegments: '', + } satisfies Partial, + ); }); }); diff --git a/projects/scion/e2e-testing/src/workbench/workbench-navigator.ts b/projects/scion/e2e-testing/src/workbench/workbench-navigator.ts index 6b2d92208..33d52a9ba 100644 --- a/projects/scion/e2e-testing/src/workbench/workbench-navigator.ts +++ b/projects/scion/e2e-testing/src/workbench/workbench-navigator.ts @@ -14,9 +14,9 @@ import {NotificationOpenerPagePO} from './page-object/notification-opener-page.p import {PopupOpenerPagePO} from './page-object/popup-opener-page.po'; import {RouterPagePO} from './page-object/router-page.po'; import {ViewPagePO} from './page-object/view-page.po'; -import {LayoutPagePO} from './page-object/layout-page.po'; -import {PerspectivePagePO} from './page-object/perspective-page.po'; +import {LayoutPagePO} from './page-object/layout-page/layout-page.po'; import {DialogOpenerPagePO} from './page-object/dialog-opener-page.po'; +import {WorkbenchLayout, WorkbenchLayoutFactory} from '@scion/workbench'; export interface Type extends Function { // eslint-disable-line @typescript-eslint/ban-types new(...args: any[]): T; @@ -54,10 +54,6 @@ export class WorkbenchNavigator { * Opens the page to change the layout in a new workbench tab. */ public openInNewTab(page: Type): Promise; - /** - * Opens the page to register a perspective in a new workbench tab. - */ - public openInNewTab(page: Type): Promise; /** * Opens the page to inspect view properties in a new workbench tab. */ @@ -92,10 +88,6 @@ export class WorkbenchNavigator { await startPage.openWorkbenchView('e2e-test-layout'); return new LayoutPagePO(this._appPO, {viewId, cssClass: 'e2e-test-layout'}); } - case PerspectivePagePO: { - await startPage.openWorkbenchView('e2e-test-perspective'); - return new PerspectivePagePO(this._appPO, {viewId, cssClass: 'e2e-test-perspective'}); - } case ViewPagePO: { await startPage.openWorkbenchView('e2e-test-view'); return new ViewPagePO(this._appPO, {viewId, cssClass: 'e2e-test-view'}); @@ -105,4 +97,30 @@ export class WorkbenchNavigator { } } } + + /** + * Creates a perspective and activates it. + * + * @see WorkbenchService.registerPerspective + * @see WorkbenchService.switchPerspective + */ + public async createPerspective(defineLayoutFn: (factory: WorkbenchLayoutFactory) => WorkbenchLayout): Promise { + const id = crypto.randomUUID(); + const layoutPage = await this.openInNewTab(LayoutPagePO); + await layoutPage.createPerspective(id, {layout: defineLayoutFn}); + await layoutPage.view.tab.close(); + await this._appPO.switchPerspective(id); + return id; + } + + /** + * Modifies the current workbench layout. + * + * @see WorkbenchRouter.navigate + */ + public async modifyLayout(modifyLayoutFn: (layout: WorkbenchLayout, activePartId: string) => WorkbenchLayout): Promise { + const layoutPage = await this.openInNewTab(LayoutPagePO); + await layoutPage.modifyLayout(modifyLayoutFn); + await layoutPage.view.tab.close(); + } } diff --git a/projects/scion/e2e-testing/src/workbench/workbench-part-action.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/workbench-part-action.e2e-spec.ts index 38016ece0..9ab45719a 100644 --- a/projects/scion/e2e-testing/src/workbench/workbench-part-action.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/workbench-part-action.e2e-spec.ts @@ -11,31 +11,23 @@ import {expect} from '@playwright/test'; import {test} from '../fixtures'; import {ViewPagePO} from './page-object/view-page.po'; -import {LayoutPagePO} from './page-object/layout-page.po'; +import {LayoutPagePO} from './page-object/layout-page/layout-page.po'; test.describe('Workbench Part Action', () => { test('should contribute action to every part', async ({appPO, workbenchNavigator}) => { - await appPO.navigateTo({microfrontendSupport: false, perspectives: ['perspective']}); - - // Switch perspective - await appPO.switchPerspective('perspective'); + await appPO.navigateTo({microfrontendSupport: false}); - // Prepare layout - // +------+-----------+-------+ - // | left | main-area | right | - // +------+-----------+-------+ - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {align: 'left', ratio: .25}); - await layoutPage.addPart('right', {align: 'right', ratio: .25}); - await layoutPage.addView('view-1', {partId: 'left', activateView: true}); - await layoutPage.addView('view-2', {partId: 'right', activateView: true}); - await layoutPage.registerRoute({path: '', outlet: 'view-1', component: 'view-page'}, {title: 'View 1'}); - await layoutPage.registerRoute({path: '', outlet: 'view-2', component: 'view-page'}, {title: 'View 2'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left', ratio: .25}) + .addPart('right', {align: 'right', ratio: .25}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: 'right', activateView: true}), + ); // Open page in main area - const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await viewPage.view.part.getPartId(); + const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); + const initialPartId = await layoutPage.view.part.getPartId(); // Register action await layoutPage.registerPartAction('Action', {cssClass: 'e2e-action'}); @@ -43,30 +35,22 @@ test.describe('Workbench Part Action', () => { // Expect the action to be displayed in every part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action'}).locator).toBeVisible(); }); test('should contribute action to parts in the workbench grid', async ({appPO, workbenchNavigator}) => { - await appPO.navigateTo({microfrontendSupport: false, perspectives: ['perspective']}); - - // Switch perspective - await appPO.switchPerspective('perspective'); + await appPO.navigateTo({microfrontendSupport: false}); - // Prepare layout - // +------+-----------+-------+ - // | left | main-area | right | - // +------+-----------+-------+ - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {align: 'left', ratio: .25}); - await layoutPage.addPart('right', {align: 'right', ratio: .25}); - await layoutPage.addView('view-1', {partId: 'left', activateView: true}); - await layoutPage.addView('view-2', {partId: 'right', activateView: true}); - await layoutPage.registerRoute({path: '', outlet: 'view-1', component: 'view-page'}, {title: 'View 1'}); - await layoutPage.registerRoute({path: '', outlet: 'view-2', component: 'view-page'}, {title: 'View 2'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left', ratio: .25}) + .addPart('right', {align: 'right', ratio: .25}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: 'right', activateView: true}), + ); // Open page in main area - const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await viewPage.view.part.getPartId(); + const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); + const initialPartId = await layoutPage.view.part.getPartId(); // Register action await layoutPage.registerPartAction('Action', {grid: 'workbench', cssClass: 'e2e-action'}); @@ -74,30 +58,22 @@ test.describe('Workbench Part Action', () => { // Expect the action to be displayed in all parts of the workbench grid await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); }); test('should contribute action to specific part(s)', async ({appPO, workbenchNavigator}) => { - await appPO.navigateTo({microfrontendSupport: false, perspectives: ['perspective']}); + await appPO.navigateTo({microfrontendSupport: false}); - // Switch perspective - await appPO.switchPerspective('perspective'); - - // Prepare layout - // +------+-----------+-------+ - // | left | main-area | right | - // +------+-----------+-------+ - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {align: 'left', ratio: .25}); - await layoutPage.addPart('right', {align: 'right', ratio: .25}); - await layoutPage.addView('view-1', {partId: 'left', activateView: true}); - await layoutPage.addView('view-2', {partId: 'right', activateView: true}); - await layoutPage.registerRoute({path: '', outlet: 'view-1', component: 'view-page'}, {title: 'View 1'}); - await layoutPage.registerRoute({path: '', outlet: 'view-2', component: 'view-page'}, {title: 'View 2'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left', ratio: .25}) + .addPart('right', {align: 'right', ratio: .25}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: 'right', activateView: true}), + ); // Open page in main area - const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await viewPage.view.part.getPartId(); + const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); + const initialPartId = await layoutPage.view.part.getPartId(); await test.step('register action in left part', async () => { await layoutPage.registerPartAction('Action 1', {partId: 'left', cssClass: 'e2e-action-1'}); @@ -105,7 +81,7 @@ test.describe('Workbench Part Action', () => { // Expect the action-1 to be displayed only in the left part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-1'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); }); await test.step('register action in left and right part', async () => { @@ -114,192 +90,175 @@ test.describe('Workbench Part Action', () => { // Expect the action-1 to be displayed only in the left part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-1'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); // Expect the action-2 to be displayed in the left and right parts await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-2'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-2'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); }); - await test.step('register action in main part', async () => { - await layoutPage.registerPartAction('Action 3', {partId: mainPartId, cssClass: 'e2e-action-3'}); + await test.step('register action in initial part', async () => { + await layoutPage.registerPartAction('Action 3', {partId: initialPartId, cssClass: 'e2e-action-3'}); // Expect the action-1 to be displayed only in the left part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-1'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); // Expect the action-2 to be displayed in the left and right parts await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-2'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-2'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); - // Expect the action-3 to be displayed only in the main part + // Expect the action-3 to be displayed only in the initial part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-3'}).locator).not.toBeAttached(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-3'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-3'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-3'}).locator).toBeVisible(); }); }); test('should contribute action to specific views(s)', async ({appPO, workbenchNavigator}) => { - await appPO.navigateTo({microfrontendSupport: false, perspectives: ['perspective']}); + await appPO.navigateTo({microfrontendSupport: false}); - // Switch perspective - await appPO.switchPerspective('perspective'); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left', ratio: .25}) + .addPart('right', {align: 'right', ratio: .25}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: 'left'}) + .addView('view.103', {partId: 'right', activateView: true}) + .addView('view.104', {partId: 'right'}), + ); - // Prepare layout - // +------+-----------+-------+ - // | left | main-area | right | - // +------+-----------+-------+ + // Open pages in main area const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {align: 'left', ratio: .25}); - await layoutPage.addPart('right', {align: 'right', ratio: .25}); - await layoutPage.addView('view-1', {partId: 'left', activateView: true}); - await layoutPage.addView('view-2', {partId: 'left'}); - await layoutPage.addView('view-3', {partId: 'right', activateView: true}); - await layoutPage.addView('view-4', {partId: 'right'}); - await layoutPage.registerRoute({path: '', outlet: 'view-1', component: 'view-page'}, {title: 'View 1'}); - await layoutPage.registerRoute({path: '', outlet: 'view-2', component: 'view-page'}, {title: 'View 2'}); - await layoutPage.registerRoute({path: '', outlet: 'view-3', component: 'view-page'}, {title: 'View 3'}); - await layoutPage.registerRoute({path: '', outlet: 'view-4', component: 'view-page'}, {title: 'View 4'}); - - // Open page in main area - const mainPage1 = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPage2 = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await mainPage1.view.part.getPartId(); + const viewPage1 = await workbenchNavigator.openInNewTab(ViewPagePO); + const viewPage2 = await workbenchNavigator.openInNewTab(ViewPagePO); + const initialPartId = await layoutPage.view.part.getPartId(); - await test.step('register action in view-1', async () => { - await layoutPage.registerPartAction('Action 1', {viewId: 'view-1', cssClass: 'e2e-action-1'}); + await test.step('register action in view.101', async () => { + await layoutPage.registerPartAction('Action 1', {viewId: 'view.101', cssClass: 'e2e-action-1'}); // Expect the action-1 to be displayed only in the left part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-1'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); }); - await test.step('register action in view-1 and view-3', async () => { - await layoutPage.registerPartAction('Action 2', {viewId: ['view-1', 'view-3'], cssClass: 'e2e-action-2'}); + await test.step('register action in view.101 and view.103', async () => { + await layoutPage.registerPartAction('Action 2', {viewId: ['view.101', 'view.103'], cssClass: 'e2e-action-2'}); // Expect the action-1 to be displayed only in the left part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-1'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); // Expect the action-2 to be displayed in the left and right parts await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-2'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-2'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); }); - await test.step('register action in main part', async () => { - await layoutPage.registerPartAction('Action 3', {viewId: await mainPage1.view.getViewId(), cssClass: 'e2e-action-3'}); - await mainPage1.view.tab.click(); + await test.step('register action in initial part', async () => { + await layoutPage.registerPartAction('Action 3', {viewId: await viewPage1.view.getViewId(), cssClass: 'e2e-action-3'}); + await viewPage1.view.tab.click(); // Expect the action-1 to be displayed only in the left part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-1'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); // Expect the action-2 to be displayed in the left and right parts await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-2'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-2'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); - // Expect the action-3 to be displayed only in the main part + // Expect the action-3 to be displayed only in the initial part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-3'}).locator).not.toBeAttached(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-3'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-3'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-3'}).locator).toBeVisible(); }); await test.step('change active view tabs', async () => { - await appPO.view({viewId: 'view-2'}).tab.click(); - await appPO.view({viewId: 'view-4'}).tab.click(); - await appPO.view({viewId: await mainPage2.view.getViewId()}).tab.click(); + await appPO.view({viewId: 'view.102'}).tab.click(); + await appPO.view({viewId: 'view.104'}).tab.click(); + await appPO.view({viewId: await viewPage2.view.getViewId()}).tab.click(); // Expect the action-1 to be displayed only in the left part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); // Expect the action-2 to be displayed in the left and right parts await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); - // Expect the action-3 to be displayed only in the main part + // Expect the action-3 to be displayed only in the initial part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-3'}).locator).not.toBeAttached(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-3'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-3'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-3'}).locator).not.toBeAttached(); }); await test.step('change active view tabs back', async () => { - await appPO.view({viewId: 'view-1'}).tab.click(); - await appPO.view({viewId: 'view-3'}).tab.click(); - await appPO.view({viewId: await mainPage1.view.getViewId()}).tab.click(); + await appPO.view({viewId: 'view.101'}).tab.click(); + await appPO.view({viewId: 'view.103'}).tab.click(); + await appPO.view({viewId: await viewPage1.view.getViewId()}).tab.click(); // Expect the action-1 to be displayed only in the left part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-1'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-1'}).locator).not.toBeAttached(); // Expect the action-2 to be displayed in the left and right parts await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-2'}).locator).toBeVisible(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-2'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-2'}).locator).not.toBeAttached(); - // Expect the action-3 to be displayed only in the main part + // Expect the action-3 to be displayed only in the initial part await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action-3'}).locator).not.toBeAttached(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action-3'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-3'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-3'}).locator).toBeVisible(); }); }); test('should contribute action to specific view in the main area', async ({appPO, workbenchNavigator}) => { - await appPO.navigateTo({microfrontendSupport: false, perspectives: ['perspective']}); - - // Switch perspective - await appPO.switchPerspective('perspective'); + await appPO.navigateTo({microfrontendSupport: false}); - // Prepare layout - // +------+-----------+-------+ - // | left | main-area | right | - // +------+-----------+-------+ - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {align: 'left', ratio: .25}); - await layoutPage.addPart('right', {align: 'right', ratio: .25}); - await layoutPage.addView('view-1', {partId: 'left', activateView: true}); - await layoutPage.addView('view-2', {partId: 'right', activateView: true}); - await layoutPage.registerRoute({path: '', outlet: 'view-1', component: 'view-page'}, {title: 'View 1'}); - await layoutPage.registerRoute({path: '', outlet: 'view-2', component: 'view-page'}, {title: 'View 2'}); + await workbenchNavigator.modifyLayout(layout => layout + .addPart('left', {align: 'left', ratio: .25}) + .addPart('right', {align: 'right', ratio: .25}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: 'right', activateView: true}), + ); // Open page in main area - const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await viewPage.view.part.getPartId(); + const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); + const initialPartId = await layoutPage.view.part.getPartId(); // Register action - await layoutPage.registerPartAction('Action', {grid: 'mainArea', viewId: 'view-1', cssClass: 'e2e-action'}); + await layoutPage.registerPartAction('Action', {grid: 'mainArea', viewId: 'view.101', cssClass: 'e2e-action'}); // Expect the action not to be displayed await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); - // Drag view-1 to main area - await appPO.view({viewId: 'view-1'}).tab.dragTo({partId: mainPartId, region: 'center'}); + // Drag view.101 to main area + await appPO.view({viewId: 'view.101'}).tab.dragTo({partId: initialPartId, region: 'center'}); // Expect the action not to be displayed await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action'}).locator).toBeVisible(); - // Drag view-1 to right part - await appPO.view({viewId: 'view-1'}).tab.dragTo({partId: 'right', region: 'center'}); + // Drag view.101 to right part + await appPO.view({viewId: 'view.101'}).tab.dragTo({partId: 'right', region: 'center'}); // Expect the action not to be displayed await expect(appPO.part({partId: 'left'}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); await expect(appPO.part({partId: 'right'}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); }); test('should display actions when dragging view to the center', async ({appPO, workbenchNavigator}) => { @@ -310,15 +269,15 @@ test.describe('Workbench Part Action', () => { // Open a view const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await appPO.activePart({inMainArea: true}).getPartId(); + const initialPartId = await appPO.activePart({inMainArea: true}).getPartId(); await layoutPage.registerPartAction('View Action', {viewId: await viewPage.view.getViewId(), cssClass: 'e2e-action-view'}); // Drag the view await viewPage.view.tab.dragTo({partId: await viewPage.view.part.getPartId(), region: 'center'}, {steps: 100, performDrop: false}); // Expect the global action to still display - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); }); test('should display actions when dragging view quickly to the center', async ({appPO, workbenchNavigator}) => { @@ -329,15 +288,15 @@ test.describe('Workbench Part Action', () => { // Open a view const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await appPO.activePart({inMainArea: true}).getPartId(); + const initialPartId = await appPO.activePart({inMainArea: true}).getPartId(); await layoutPage.registerPartAction('View Action', {viewId: await viewPage.view.getViewId(), cssClass: 'e2e-action-view'}); // Drag the view await viewPage.view.tab.dragTo({partId: await viewPage.view.part.getPartId(), region: 'center'}, {steps: 1, performDrop: false}); // Expect the global action to still display - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); }); test('should display actions when dragging view to the north', async ({appPO, workbenchNavigator}) => { @@ -348,15 +307,15 @@ test.describe('Workbench Part Action', () => { // Open a view const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await appPO.activePart({inMainArea: true}).getPartId(); + const initialPartId = await appPO.activePart({inMainArea: true}).getPartId(); await layoutPage.registerPartAction('View Action', {viewId: await viewPage.view.getViewId(), cssClass: 'e2e-action-view'}); // Drag the view await viewPage.view.tab.dragTo({partId: await viewPage.view.part.getPartId(), region: 'north'}, {steps: 100, performDrop: false}); // Expect the global action to still display - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); }); test('should display actions when dragging view quickly to the north', async ({appPO, workbenchNavigator}) => { @@ -367,15 +326,15 @@ test.describe('Workbench Part Action', () => { // Open a view const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await appPO.activePart({inMainArea: true}).getPartId(); + const initialPartId = await appPO.activePart({inMainArea: true}).getPartId(); await layoutPage.registerPartAction('View Action', {viewId: await viewPage.view.getViewId(), cssClass: 'e2e-action-view'}); // Drag the view await viewPage.view.tab.dragTo({partId: await viewPage.view.part.getPartId(), region: 'north'}, {steps: 1, performDrop: false}); // Expect the global action to still display - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); }); test('should display actions when dragging view to the east', async ({appPO, workbenchNavigator}) => { @@ -386,15 +345,15 @@ test.describe('Workbench Part Action', () => { // Open a view const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await appPO.activePart({inMainArea: true}).getPartId(); + const initialPartId = await appPO.activePart({inMainArea: true}).getPartId(); await layoutPage.registerPartAction('View Action', {viewId: await viewPage.view.getViewId(), cssClass: 'e2e-action-view'}); // Drag the view await viewPage.view.tab.dragTo({partId: await viewPage.view.part.getPartId(), region: 'east'}, {steps: 100, performDrop: false}); // Expect the global action to still display - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); }); test('should display actions when dragging view quickly to the east', async ({appPO, workbenchNavigator}) => { @@ -405,15 +364,15 @@ test.describe('Workbench Part Action', () => { // Open a view const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await appPO.activePart({inMainArea: true}).getPartId(); + const initialPartId = await appPO.activePart({inMainArea: true}).getPartId(); await layoutPage.registerPartAction('View Action', {viewId: await viewPage.view.getViewId(), cssClass: 'e2e-action-view'}); // Drag the view await viewPage.view.tab.dragTo({partId: await viewPage.view.part.getPartId(), region: 'east'}, {steps: 1, performDrop: false}); // Expect the global action to still display - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); }); test('should display actions when dragging view to the south', async ({appPO, workbenchNavigator}) => { @@ -424,15 +383,15 @@ test.describe('Workbench Part Action', () => { // Open a view const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await appPO.activePart({inMainArea: true}).getPartId(); + const initialPartId = await appPO.activePart({inMainArea: true}).getPartId(); await layoutPage.registerPartAction('View Action', {viewId: await viewPage.view.getViewId(), cssClass: 'e2e-action-view'}); // Drag the view await viewPage.view.tab.dragTo({partId: await viewPage.view.part.getPartId(), region: 'south'}, {steps: 100, performDrop: false}); // Expect the global action to still display - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); }); test('should display actions when dragging view quickly to the south', async ({appPO, workbenchNavigator}) => { @@ -443,15 +402,15 @@ test.describe('Workbench Part Action', () => { // Open a view const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await appPO.activePart({inMainArea: true}).getPartId(); + const initialPartId = await appPO.activePart({inMainArea: true}).getPartId(); await layoutPage.registerPartAction('View Action', {viewId: await viewPage.view.getViewId(), cssClass: 'e2e-action-view'}); // Drag the view await viewPage.view.tab.dragTo({partId: await viewPage.view.part.getPartId(), region: 'south'}, {steps: 1, performDrop: false}); // Expect the global action to still display - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); }); test('should display actions when dragging view to the west', async ({appPO, workbenchNavigator}) => { @@ -462,15 +421,15 @@ test.describe('Workbench Part Action', () => { // Open a view const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await appPO.activePart({inMainArea: true}).getPartId(); + const initialPartId = await appPO.activePart({inMainArea: true}).getPartId(); await layoutPage.registerPartAction('View Action', {viewId: await viewPage.view.getViewId(), cssClass: 'e2e-action-view'}); // Drag the view await viewPage.view.tab.dragTo({partId: await viewPage.view.part.getPartId(), region: 'west'}, {steps: 100, performDrop: false}); // Expect the global action to still display - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); }); test('should display actions when dragging view quickly to the west', async ({appPO, workbenchNavigator}) => { @@ -481,15 +440,15 @@ test.describe('Workbench Part Action', () => { // Open a view const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await appPO.activePart({inMainArea: true}).getPartId(); + const initialPartId = await appPO.activePart({inMainArea: true}).getPartId(); await layoutPage.registerPartAction('View Action', {viewId: await viewPage.view.getViewId(), cssClass: 'e2e-action-view'}); // Drag the view await viewPage.view.tab.dragTo({partId: await viewPage.view.part.getPartId(), region: 'west'}, {steps: 1, performDrop: false}); // Expect the global action to still display - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-global'}).locator).toBeVisible(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action-view'}).locator).not.toBeAttached(); }); test('should display actions after drop', async ({appPO, workbenchNavigator}) => { @@ -497,7 +456,7 @@ test.describe('Workbench Part Action', () => { // Open a view const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await appPO.activePart({inMainArea: true}).getPartId(); + const initialPartId = await appPO.activePart({inMainArea: true}).getPartId(); // Register view-specific action const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); @@ -508,7 +467,7 @@ test.describe('Workbench Part Action', () => { const newPartId = await viewPage.view.part.getPartId(); // Expect action to display - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); await expect(appPO.part({partId: newPartId}).action({cssClass: 'e2e-action'}).locator).toBeVisible(); }); @@ -517,7 +476,7 @@ test.describe('Workbench Part Action', () => { // Open a view const viewPage = await workbenchNavigator.openInNewTab(ViewPagePO); - const mainPartId = await appPO.activePart({inMainArea: true}).getPartId(); + const initialPartId = await appPO.activePart({inMainArea: true}).getPartId(); // Register view-specific action const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); @@ -528,7 +487,7 @@ test.describe('Workbench Part Action', () => { const newPartId = await viewPage.view.part.getPartId(); // Expect action to display - await expect(appPO.part({partId: mainPartId}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); + await expect(appPO.part({partId: initialPartId}).action({cssClass: 'e2e-action'}).locator).not.toBeAttached(); await expect(appPO.part({partId: newPartId}).action({cssClass: 'e2e-action'}).locator).toBeVisible(); }); }); diff --git a/projects/scion/e2e-testing/src/workbench/workbench-perspective-storage.e2e-spec.ts b/projects/scion/e2e-testing/src/workbench/workbench-perspective-storage.e2e-spec.ts index 3e0bf87e3..07cb4aed2 100644 --- a/projects/scion/e2e-testing/src/workbench/workbench-perspective-storage.e2e-spec.ts +++ b/projects/scion/e2e-testing/src/workbench/workbench-perspective-storage.e2e-spec.ts @@ -10,46 +10,114 @@ import {expect} from '@playwright/test'; import {test} from '../fixtures'; -import {LayoutPagePO} from './page-object/layout-page.po'; import {MAIN_AREA} from '../workbench.model'; import {ViewPagePO} from './page-object/view-page.po'; import {expectView} from '../matcher/view-matcher'; +import {MPart, MTreeNode} from '../matcher/to-equal-workbench-layout.matcher'; test.describe('Workbench Perspective Storage', () => { - test('should restore workbench grid from storage', async ({page, appPO, workbenchNavigator}) => { - await appPO.navigateTo({microfrontendSupport: false, perspectives: ['perspective-1']}); - - // Switch to perspective-1 - await appPO.switchPerspective('perspective-1'); + test('should restore workbench grid from storage', async ({appPO, workbenchNavigator}) => { + await appPO.navigateTo({microfrontendSupport: false}); // Add part and view to the workbench grid - const layoutPage = await workbenchNavigator.openInNewTab(LayoutPagePO); - await layoutPage.addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}); - await layoutPage.addView('outline', {partId: 'left', activateView: true}); - await layoutPage.addView('console', {partId: 'left', activateView: true}); + await workbenchNavigator.modifyLayout((layout, activePartId) => layout + .addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) + .addView('view.101', {partId: 'left'}) + .addView('view.102', {partId: 'left', activateView: true, activatePart: true}) + .addView('view.103', {partId: activePartId, activateView: true}) + .navigateView('view.101', ['test-view']) + .navigateView('view.102', ['test-view']) + .navigateView('view.103', ['test-view']), + ); - const testee1ViewPage = new ViewPagePO(appPO, {viewId: 'outline'}); - const testee2ViewPage = new ViewPagePO(appPO, {viewId: 'console'}); + const viewPage1 = new ViewPagePO(appPO, {viewId: 'view.101'}); + const viewPage2 = new ViewPagePO(appPO, {viewId: 'view.102'}); + const viewPage3 = new ViewPagePO(appPO, {viewId: 'view.103'}); // Reopen the page - await page.goto('about:blank'); - await appPO.navigateTo({microfrontendSupport: false, perspectives: ['perspective-1']}); + await appPO.reload(); + + // Expect perspective to be restored from the storage + await expect(appPO.workbench).toEqualWorkbenchLayout({ + workbenchGrid: { + root: new MTreeNode({ + direction: 'row', + ratio: .25, + child1: new MPart({ + id: 'left', + views: [{id: 'view.101'}, {id: 'view.102'}], + activeViewId: 'view.102', + }), + child2: new MPart({ + id: MAIN_AREA, + }), + }), + activePartId: 'left', + }, + mainAreaGrid: { + root: new MPart({ + views: [{id: 'view.103'}], + activeViewId: 'view.103', + }), + }, + }); - // Expect perspective-1 to be restored from the storage - await expect.poll(() => appPO.header.perspectiveToggleButton({perspectiveId: 'perspective-1'}).isActive()).toBe(true); - await expectView(testee1ViewPage).toBeInactive(); - await expectView(testee2ViewPage).toBeActive(); + await expectView(viewPage1).toBeInactive(); + await expectView(viewPage2).toBeActive(); + await expectView(viewPage3).toBeActive(); // Close view - await testee2ViewPage.view.tab.close(); + await viewPage2.view.tab.close(); // Reopen the page - await page.goto('about:blank'); - await appPO.navigateTo({microfrontendSupport: false, perspectives: ['perspective-1']}); + await appPO.reload(); + + // Expect perspective to be restored from the storage + await expect(appPO.workbench).toEqualWorkbenchLayout({ + workbenchGrid: { + root: new MTreeNode({ + direction: 'row', + ratio: .25, + child1: new MPart({ + id: 'left', + views: [{id: 'view.101'}], + activeViewId: 'view.101', + }), + child2: new MPart({ + id: MAIN_AREA, + }), + }), + activePartId: 'left', + }, + mainAreaGrid: { + root: new MPart({ + views: [{id: 'view.103'}], + activeViewId: 'view.103', + }), + }, + }); + await expectView(viewPage1).toBeActive(); + await expectView(viewPage2).not.toBeAttached(); + await expectView(viewPage3).toBeActive(); + }); + + test('should not set the initial perspective as the active perspective in storage and window', async ({appPO}) => { + await appPO.navigateTo({microfrontendSupport: false}); + + await expect.poll(() => appPO.getWindowName()).toEqual(''); + await expect.poll(() => appPO.getLocalStorageItem('scion.workbench.perspective')).toBeNull(); + }); + + test('should select the initial perspective from storage', async ({appPO}) => { + await appPO.navigateTo({ + microfrontendSupport: false, + perspectives: ['testee-1', 'testee-2', 'testee-3'], + localStorage: { + 'scion.workbench.perspective': 'testee-2', + }, + }); - // Expect perspective-1 to be restored from the storage - await expectView(testee1ViewPage).toBeActive(); - await expectView(testee2ViewPage).not.toBeAttached(); + await expect.poll(() => appPO.header.perspectiveToggleButton({perspectiveId: 'testee-2'}).isActive()).toBe(true); }); }); diff --git a/projects/scion/workbench-client/src/lib/dialog/workbench-dialog-capability.ts b/projects/scion/workbench-client/src/lib/dialog/workbench-dialog-capability.ts index d392b1580..0471bee53 100644 --- a/projects/scion/workbench-client/src/lib/dialog/workbench-dialog-capability.ts +++ b/projects/scion/workbench-client/src/lib/dialog/workbench-dialog-capability.ts @@ -47,7 +47,7 @@ export interface WorkbenchDialogCapability extends Capability { /** * Specifies parameters required by the dialog. * - * Parameters are available in the path or title for placeholder substitution, or can be read in the microfrontend by injecting the {@link WorkbenchDialog} handle. + * Parameters are available in the path and title for placeholder substitution, or can be read in the microfrontend by injecting the {@link WorkbenchDialog} handle. * * @inheritDoc */ @@ -65,7 +65,7 @@ export interface WorkbenchDialogCapability extends Capability { * The path supports placeholders that will be replaced with parameter values. A placeholder * starts with a colon (`:`) followed by the parameter name. * - * #### Usage of named parameters in the path: + * Usage: * ```json * { * "type": "dialog", @@ -115,7 +115,7 @@ export interface WorkbenchDialogCapability extends Capability { */ showSplash?: boolean; /** - * Specifies CSS class(es) to be added to the dialog, useful in end-to-end tests for locating the dialog. + * Specifies CSS class(es) to add to the dialog, e.g., to locate the dialog in tests. */ cssClass?: string | string[]; }; diff --git a/projects/scion/workbench-client/src/lib/dialog/workbench-dialog-service.ts b/projects/scion/workbench-client/src/lib/dialog/workbench-dialog-service.ts index 260521287..02df7bfc7 100644 --- a/projects/scion/workbench-client/src/lib/dialog/workbench-dialog-service.ts +++ b/projects/scion/workbench-client/src/lib/dialog/workbench-dialog-service.ts @@ -22,6 +22,10 @@ import {WorkbenchView} from '../view/workbench-view'; * A dialog is a visual element for focused interaction with the user, such as prompting the user for input or confirming actions. * The user can move or resize a dialog. * + * A microfrontend provided as a dialog capability can be opened in a dialog. The qualifier differentiates between different + * dialog capabilities. An application can open the public dialog capabilities of other applications if it manifests a respective + * intention. + * * Displayed on top of other content, a dialog blocks interaction with other parts of the application. Multiple dialogs are stacked, * and only the topmost dialog in each modality stack can be interacted with. * @@ -35,12 +39,12 @@ import {WorkbenchView} from '../view/workbench-view'; export class WorkbenchDialogService { /** - * Opens a microfrontend in a workbench dialog based on the given qualifier and options. + * Opens a microfrontend of a dialog capability in a workbench dialog based on the given qualifier and options. * * By default, the calling context determines the modality of the dialog. If the dialog is opened from a view, only this view is blocked. * To open the dialog with a different modality, specify the modality in {@link WorkbenchDialogOptions.modality}. * - * @param qualifier - Identifies the dialog capability that provides the microfrontend to be displayed in a dialog. + * @param qualifier - Identifies the dialog capability that provides the microfrontend to open in a dialog. * @param options - Controls how to open the dialog. * @returns Promise that resolves to the dialog result, if any, or that rejects if the dialog couldn't be opened or was closed with an error. * diff --git a/projects/scion/workbench-client/src/lib/dialog/workbench-dialog.options.ts b/projects/scion/workbench-client/src/lib/dialog/workbench-dialog.options.ts index 2c57228e6..87797a0c4 100644 --- a/projects/scion/workbench-client/src/lib/dialog/workbench-dialog.options.ts +++ b/projects/scion/workbench-client/src/lib/dialog/workbench-dialog.options.ts @@ -8,6 +8,8 @@ * SPDX-License-Identifier: EPL-2.0 */ +import {ViewId} from '../view/workbench-view'; + /** * Configures the dialog to display a microfrontend in a workbench dialog using {@link WorkbenchDialogService}. * @@ -15,8 +17,9 @@ */ export interface WorkbenchDialogOptions { /** - * Passes data to the dialog microfrontend. The dialog provider can declare mandatory and optional parameters. - * No additional parameters may be included. Refer to the documentation of the dialog capability provider for more information. + * Passes data to the dialog. + * + * The dialog can declare mandatory and optional parameters. No additional parameters are allowed. Refer to the documentation of the capability for more information. */ params?: Map | {[param: string]: unknown}; /** @@ -39,14 +42,14 @@ export interface WorkbenchDialogOptions { * * By default, if opening the dialog in the context of a view, that view is used as the contextual view. */ - viewId?: string; + viewId?: ViewId; }; /** * Controls whether to animate the opening of the dialog. Defaults is `false`. */ animate?: boolean; /** - * Specifies CSS class(es) to be added to the dialog, useful in end-to-end tests for locating the dialog. + * Specifies CSS class(es) to add to the dialog, e.g., to locate the dialog in tests. */ cssClass?: string | string[]; } diff --git a/projects/scion/workbench-client/src/lib/message-box/workbench-message-box.config.ts b/projects/scion/workbench-client/src/lib/message-box/workbench-message-box.config.ts index 36e349bcb..8e0bababa 100644 --- a/projects/scion/workbench-client/src/lib/message-box/workbench-message-box.config.ts +++ b/projects/scion/workbench-client/src/lib/message-box/workbench-message-box.config.ts @@ -9,6 +9,7 @@ */ import {Dictionary} from '@scion/toolkit/util'; +import {ViewId} from '../view/workbench-view'; /** * Configures the content and appearance of a message presented to the user in the form of a message box. @@ -46,8 +47,9 @@ export interface WorkbenchMessageBoxConfig { content?: any; /** - * Allows passing data to the message box. The message box provider can declare mandatory and optional parameters. - * No additional parameters may be included. Refer to the documentation of the message box capability provider for more information. + * Passes data to the message box. + * + * The message box can declare mandatory and optional parameters. No additional parameters are allowed. Refer to the documentation of the capability for more information. */ params?: Map | Dictionary; @@ -90,7 +92,7 @@ export interface WorkbenchMessageBoxConfig { contentSelectable?: boolean; /** - * Specifies CSS class(es) to be added to the message box, useful in end-to-end tests for locating the message box. + * Specifies CSS class(es) to add to the message box, e.g., to locate the message box in tests. */ cssClass?: string | string[]; @@ -103,6 +105,6 @@ export interface WorkbenchMessageBoxConfig { * * By default, if opening the message box in the context of a view, that view is used as the contextual view. */ - viewId?: string; + viewId?: ViewId; }; } diff --git a/projects/scion/workbench-client/src/lib/notification/workbench-notification.config.ts b/projects/scion/workbench-client/src/lib/notification/workbench-notification.config.ts index 10992142b..00fbbb3e5 100644 --- a/projects/scion/workbench-client/src/lib/notification/workbench-notification.config.ts +++ b/projects/scion/workbench-client/src/lib/notification/workbench-notification.config.ts @@ -37,8 +37,9 @@ export interface WorkbenchNotificationConfig { content?: any; /** - * Allows passing data to the notification. The notification provider can declare mandatory and optional parameters. - * No additional parameters may be included. Refer to the documentation of the notification capability provider for more information. + * Passes data to the notification. + * + * The notification can declare mandatory and optional parameters. No additional parameters are allowed. Refer to the documentation of the capability for more information. */ params?: Map | Dictionary; @@ -60,7 +61,7 @@ export interface WorkbenchNotificationConfig { group?: string; /** - * Specifies CSS class(es) to be added to the notification, useful in end-to-end tests for locating the notification. + * Specifies CSS class(es) to add to the notification, e.g., to locate the notification in tests. */ cssClass?: string | string[]; } diff --git a/projects/scion/workbench-client/src/lib/popup/workbench-popup-capability.ts b/projects/scion/workbench-client/src/lib/popup/workbench-popup-capability.ts index e7495ad75..ccd2a0bdf 100644 --- a/projects/scion/workbench-client/src/lib/popup/workbench-popup-capability.ts +++ b/projects/scion/workbench-client/src/lib/popup/workbench-popup-capability.ts @@ -84,7 +84,7 @@ export interface WorkbenchPopupCapability extends Capability { */ showSplash?: boolean; /** - * Specifies CSS class(es) to be added to the popup, useful in end-to-end tests for locating the popup. + * Specifies CSS class(es) to add to the popup, e.g., to locate the popup in tests. */ cssClass?: string | string[]; }; diff --git a/projects/scion/workbench-client/src/lib/popup/workbench-popup-open-command.ts b/projects/scion/workbench-client/src/lib/popup/workbench-popup-open-command.ts index bd7c2274b..ca1306cc1 100644 --- a/projects/scion/workbench-client/src/lib/popup/workbench-popup-open-command.ts +++ b/projects/scion/workbench-client/src/lib/popup/workbench-popup-open-command.ts @@ -9,6 +9,7 @@ */ import {CloseStrategy} from './workbench-popup.config'; +import {ViewId} from '../view/workbench-view'; /** * Command object for instructing the Workbench to open the microfrontend of given popup capability in a popup. @@ -22,6 +23,6 @@ export interface ɵWorkbenchPopupCommand { closeStrategy?: CloseStrategy; cssClass?: string | string[]; context?: { - viewId?: string | null; + viewId?: ViewId | null; }; } diff --git a/projects/scion/workbench-client/src/lib/popup/workbench-popup-referrer.ts b/projects/scion/workbench-client/src/lib/popup/workbench-popup-referrer.ts index a7a04e406..1813e7697 100644 --- a/projects/scion/workbench-client/src/lib/popup/workbench-popup-referrer.ts +++ b/projects/scion/workbench-client/src/lib/popup/workbench-popup-referrer.ts @@ -8,6 +8,8 @@ * SPDX-License-Identifier: EPL-2.0 */ +import {ViewId} from '../view/workbench-view'; + /** * Information about the context in which a popup was opened. * @@ -17,7 +19,7 @@ export interface WorkbenchPopupReferrer { /** * Identity of the view if opened in the context of a view. */ - viewId?: string; + viewId?: ViewId; /** * Identity of the view capability if opened in the context of a view microfrontend. */ diff --git a/projects/scion/workbench-client/src/lib/popup/workbench-popup-service.ts b/projects/scion/workbench-client/src/lib/popup/workbench-popup-service.ts index f5ecf8330..ba6cabf4f 100644 --- a/projects/scion/workbench-client/src/lib/popup/workbench-popup-service.ts +++ b/projects/scion/workbench-client/src/lib/popup/workbench-popup-service.ts @@ -53,8 +53,8 @@ export class WorkbenchPopupService { * * By setting the alignment of the popup, you can control the region where to open the popup relative to its anchor. * - * You can pass data to the popup microfrontend using parameters. The popup provider can declare mandatory and optional parameters. - * No additional parameters may be included. Refer to the documentation of the popup capability provider for more information. + * You can pass data to the popup using parameters. The popup can declare mandatory and optional parameters. + * No additional parameters are allowed. Refer to the documentation of the capability for more information. * * By default, the popup will close on focus loss, or when the user hits the escape key. * diff --git a/projects/scion/workbench-client/src/lib/popup/workbench-popup.config.ts b/projects/scion/workbench-client/src/lib/popup/workbench-popup.config.ts index 9cfd3df52..4e42c6966 100644 --- a/projects/scion/workbench-client/src/lib/popup/workbench-popup.config.ts +++ b/projects/scion/workbench-client/src/lib/popup/workbench-popup.config.ts @@ -11,6 +11,7 @@ import {Observable} from 'rxjs'; import {Dictionary} from '@scion/toolkit/util'; import {PopupOrigin} from './popup.origin'; +import {ViewId} from '../view/workbench-view'; /** * Configures the popup to display a microfrontend in a workbench popup using {@link WorkbenchPopupService}. @@ -41,8 +42,9 @@ export interface WorkbenchPopupConfig { */ anchor: Element | PopupOrigin | Observable; /** - * Allows passing data to the popup microfrontend. The popup provider can declare mandatory and optional parameters. - * No additional parameters may be included. Refer to the documentation of the popup capability provider for more information. + * Passes data to the popup. + * + * The popup can declare mandatory and optional parameters. No additional parameters are allowed. Refer to the documentation of the capability for more information. */ params?: Map | Dictionary; /** @@ -55,7 +57,7 @@ export interface WorkbenchPopupConfig { */ closeStrategy?: CloseStrategy; /** - * Specifies CSS class(es) to be added to the popup, useful in end-to-end tests for locating the popup. + * Specifies CSS class(es) to add to the popup, e.g., to locate the popup in tests. */ cssClass?: string | string[]; /** @@ -70,7 +72,7 @@ export interface WorkbenchPopupConfig { * By default, if opening the popup in the context of a view, that view is used as the popup's contextual view. * If you set the view id to `null`, the popup will open without referring to the contextual view. */ - viewId?: string | null; + viewId?: ViewId | null; }; } diff --git a/projects/scion/workbench-client/src/lib/routing/workbench-router.ts b/projects/scion/workbench-client/src/lib/routing/workbench-router.ts index 7f4800b7e..e3f247e9d 100644 --- a/projects/scion/workbench-client/src/lib/routing/workbench-router.ts +++ b/projects/scion/workbench-client/src/lib/routing/workbench-router.ts @@ -17,16 +17,13 @@ import {ɵWorkbenchCommands} from '../ɵworkbench-commands'; import {lastValueFrom} from 'rxjs'; /** - * Allows navigating to a microfrontend in a workbench view. + * Enables navigation of workbench views. * - * A view is a visual workbench component for displaying content stacked or side-by-side. + * A view is a visual workbench element for displaying content side-by-side or stacked. * - * In SCION Workbench Client, routing means instructing a workbench view to display the microfrontend of a registered view capability. - * A qualifier is used to differentiate view capabilities. A micro application can provide multiple view capabilities and make them - * publicly available to other micro applications. - * - * As a prerequisite for routing, the navigating micro application must declare a fulfilling view intention in its manifest unless navigating - * to views that the app provides itself. Navigation to microfrontends of other apps is only allowed for public view capabilities. + * A microfrontend provided as a view capability can be opened in a view. The qualifier differentiates between different + * view capabilities. An application can open the public view capabilities of other applications if it manifests a respective + * intention. * * @category Router * @category View @@ -34,22 +31,17 @@ import {lastValueFrom} from 'rxjs'; export class WorkbenchRouter { /** - * Navigates to a view microfrontend based on the given qualifier. - * - * The qualifier identifies the view microfrontend(s) which to open. If multiple view microfrontends match the qualifier, they are all opened. + * Navigates to a microfrontend of a view capability based on the given qualifier and extras. * - * By passing navigation extras, you can control where the microfrontend should open. By default, the router opens the microfrontend in a new view - * tab if no view is found that matches the specified qualifier and required params. Optional parameters do not affect view resolution. If one - * (or more) view(s) match the qualifier and required params, they are navigated instead of opening the microfrontend in a new view tab. + * By default, the router opens a new view if no view is found that matches the specified qualifier and required params. Optional parameters do not affect view resolution. + * If one or more views match the qualifier and required params, they will be navigated instead of opening the microfrontend in a new view tab. + * This behavior can be changed by setting an explicit navigation target in navigation extras. * - * @param qualifier - Identifies the view capability that provides the microfrontend. - * By passing an empty qualifier (`{}`), the currently loaded microfrontend can update its parameters in the workbench URL, - * e.g., to support persistent navigation. This type of navigation is referred to as self-navigation and is supported only - * if in the context of a view. Setting {@link WorkbenchNavigationExtras#paramsHandling} allows instructing the workbench - * router how to handle params. By default, new params replace params contained in the URL. + * @param qualifier - Identifies the view capability that provides the microfrontend to display in a view. + * Passing an empty qualifier (`{}`) allows the microfrontend to update its parameters, restoring updated parameters when the page reloads. + * Parameter handling can be controlled using the {@link WorkbenchNavigationExtras#paramsHandling} option. * @param extras - Options to control navigation. - * @return Promise that resolves to `true` when navigation succeeds, to `false` when navigation fails, or is rejected on error, - * e.g., if not qualified or because no application provides the requested view. + * @return Promise that resolves to `true` on successful navigation, or `false` otherwise. */ public async navigate(qualifier: Qualifier | {}, extras?: WorkbenchNavigationExtras): Promise { if (this.isSelfNavigation(qualifier)) { @@ -97,7 +89,7 @@ export class WorkbenchRouter { private isSelfNavigation(qualifier: Qualifier | {}): boolean { if (!qualifier || Object.keys(qualifier).length === 0) { if (!Beans.opt(WorkbenchView)) { - throw Error('[WorkbenchRouterError] Self-navigation is supported only if in the context of a view.'); + throw Error('[NavigateError] Self-navigation is supported only if in the context of a view.'); } return true; } @@ -113,57 +105,51 @@ export class WorkbenchRouter { */ export interface WorkbenchNavigationExtras { /** - * Allows passing additional data to the microfrontend. In contrast to the qualifier, params have no effect on the intent routing. - * If the fulfilling capability(-ies) declare(s) mandatory parameters, be sure to include them, otherwise navigation will be rejected. + * Passes data to the view. + * + * The view can declare mandatory and optional parameters. No additional parameters are allowed. Refer to the documentation of the capability for more information. */ params?: Map | Dictionary; /** * Instructs the workbench router how to handle params in self-navigation. * - * Self-navigation allows a view to update its parameters in the workbench URL to support persistent navigation. Setting a `paramsHandling` - * strategy has no effect on navigations other than self-navigation. A self-navigation is initiated by passing an empty qualifier. + * Self-navigation allows the microfrontend to update its parameters, restoring updated parameters when the page reloads. + * Setting a `paramsHandling` strategy has no effect on navigations other than self-navigation. A self-navigation is + * initiated by passing an empty qualifier. * * One of: - * * `replace`: Discards parameters in the URL and uses the new parameters instead (which is by default if not set). - * * `merge`: Merges new parameters with the parameters currently contained in the URL. In case of a key collision, new parameters overwrite - * the parameters contained in the URL. A parameter can be removed by passing `undefined` as its value. + * * `replace`: Replaces current parameters (default). + * * `merge`: Merges new parameters with current parameters, with new parameters of equal name overwriting existing parameters. + * A parameter can be removed by passing `undefined` as its value. */ paramsHandling?: 'merge' | 'replace'; /** - * Instructs the router to activate the view. Defaults to `true` if not specified. + * Controls where to open the view. Default is `auto`. + * + * One of: + * - 'auto': Navigates existing views that match the qualifier and required params, or opens a new view otherwise. Optional parameters do not affect view resolution. + * - 'blank': Navigates in a new view. + * - : Navigates the specified view. If already opened, replaces it, or opens a new view otherwise. */ - activate?: boolean; + target?: string | 'blank' | 'auto'; /** - * Closes the view(s) that match the specified qualifier and required parameter(s). Optional parameters do not affect view resolution. - * - * To match views with any value for a specific required parameter, use the asterisk wildcard character (`*`) as the parameter value. - * - * Note that you can only close view(s) for which you have an intention and which are visible to your app. + * Instructs the router to activate the view. Default is `true`. */ - close?: boolean; + activate?: boolean; /** - * Controls where to open the view. + * Closes views that match the specified qualifier and required parameters. Optional parameters do not affect view resolution. * - * One of: - * - 'auto': Opens the microfrontend in a new view tab if no view is found that matches the specified qualifier and required params. Optional parameters - * do not affect view resolution. If one (or more) view(s) match the qualifier and required params, they are navigated instead of - * opening the microfrontend in a new view tab, e.g., to update optional parameters. This is the default behavior if not set. - * - 'blank': Opens the microfrontend in a new view tab. - * - : Navigates the specified view. If already opened, replaces it, or opens the view in a new view tab otherwise. - * Note that the passed view identifier must start with `view.`, e.g., `view.5`. + * The parameters support the asterisk wildcard value (`*`) to match views with any value for a parameter. * - * If not specified, defaults to `auto`. + * Only views for which the application has an intention can be closed. */ - target?: string | 'blank' | 'auto'; + close?: boolean; /** - * Specifies the position where to insert the view into the tab bar when using 'blank' view target strategy. - * If not specified, the view is inserted after the active view. Set the index to 'start' or 'end' for inserting - * the view at the beginning or at the end. + * Specifies where to insert the view into the tab bar. Has no effect if navigating an existing view. Default is after the active view. */ - blankInsertionIndex?: number | 'start' | 'end'; + position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; /** - * Specifies CSS class(es) to be added to the view, useful in end-to-end tests for locating view and view tab. - * CSS class(es) will not be added to the browser URL, consequently will not survive a page reload. + * Specifies CSS class(es) to add to the view, e.g., to locate the view in tests. */ cssClass?: string | string[]; } diff --git a/projects/scion/workbench-client/src/lib/view/workbench-view-capability.ts b/projects/scion/workbench-client/src/lib/view/workbench-view-capability.ts index fe9a22021..1affc39c4 100644 --- a/projects/scion/workbench-client/src/lib/view/workbench-view-capability.ts +++ b/projects/scion/workbench-client/src/lib/view/workbench-view-capability.ts @@ -14,35 +14,48 @@ import {WorkbenchCapabilities} from '../workbench-capabilities.enum'; /** * Represents a microfrontend for display in a workbench view. * - * A view is a visual workbench component for displaying content stacked or side-by-side. + * A view is a visual workbench element for displaying content stacked or side-by-side. + * + * The microfrontend can inject the {@link WorkbenchView} handle to interact with the view, such as setting the title, reading + * parameters, or closing it. * * @category View + * @see WorkbenchView + * @see WorkbenchRouter */ export interface WorkbenchViewCapability extends Capability { - + /** + * @inheritDoc + */ type: WorkbenchCapabilities.View; - /** * Qualifies this view. The qualifier is required for views. + * + * @inheritDoc */ qualifier: Qualifier; - + /** + * Specifies parameters required by the view. + * + * Parameters are available in the path and title for placeholder substitution, or can be read in the microfrontend by injecting the {@link WorkbenchView} handle. + * + * @inheritDoc + */ params?: ViewParamDefinition[]; - + /** + * @inheritDoc + */ properties: { /** - * Specifies the path of the microfrontend to be opened when navigating to this view capability. + * Specifies the path to the microfrontend. * - * The path is relative to the base URL, as specified in the application manifest. If the + * The path is relative to the base URL specified in the application manifest. If the * application does not declare a base URL, it is relative to the origin of the manifest file. * - * You can refer to qualifiers or parameters in the form of named parameters to be replaced during navigation. - * Named parameters begin with a colon (`:`) followed by the parameter name or qualifier key, and are allowed in path segments, - * query parameters, matrix parameters and the fragment part. Empty query and matrix params are removed, but not empty path params. - * - * In addition to using qualifier and parameter values as named parameters in the URL, params are available in the microfrontend via {@link WorkbenchView.params$} Observable. + * The path supports placeholders that will be replaced with parameter values. A placeholder + * starts with a colon (`:`) followed by the parameter name. * - * #### Usage of named parameters in the path: + * Usage: * ```json * { * "type": "view", @@ -56,33 +69,24 @@ export interface WorkbenchViewCapability extends Capability { * } * } * ``` - * - * #### Path parameter example: - * segment/:param1/segment/:param2 - * - * #### Matrix parameter example: - * segment/segment;matrixParam1=:param1;matrixParam2=:param2 - * - * #### Query parameter example: - * segment/segment?queryParam1=:param1&queryParam2=:param2 */ path: string; /** - * Specifies the title to be displayed in the view tab. + * Specifies the title of this view. * - * You can refer to qualifiers or parameters in the form of named parameters to be replaced during navigation. - * Named parameters begin with a colon (`:`) followed by the parameter name or qualifier key. + * The title supports placeholders that will be replaced with parameter values. A placeholder starts with a colon (`:`) followed by the parameter name. + * The title can also be set in the microfrontend via {@link WorkbenchView} handle. */ title?: string; /** - * Specifies the subtitle to be displayed in the view tab. + * Specifies the subtitle of this view. * - * You can refer to qualifiers or parameters in the form of named parameters to be replaced during navigation. - * Named parameters begin with a colon (`:`) followed by the parameter name or qualifier key. + * The heading supports placeholders that will be replaced with parameter values. A placeholder starts with a colon (`:`) followed by the parameter name. + * The heading can also be set in the microfrontend via {@link WorkbenchView} handle. */ heading?: string; /** - * Specifies if a close button should be displayed in the view tab. + * Specifies if to display a close button in the view tab. Default is `true`. */ closable?: boolean; /** @@ -94,7 +98,7 @@ export interface WorkbenchViewCapability extends Capability { */ showSplash?: boolean; /** - * Specifies CSS class(es) to be added to the view, useful in end-to-end tests for locating view and view tab. + * Specifies CSS class(es) to add to the view, e.g., to locate the view in tests. */ cssClass?: string | string[]; }; @@ -102,18 +106,23 @@ export interface WorkbenchViewCapability extends Capability { /** * Describes a parameter to be passed along with a view intent. + * + * @category View + * @inheritDoc */ export interface ViewParamDefinition extends ParamDefinition { /** - * Controls how the workbench router should pass the parameter to the workbench view that embeds the microfrontend. + * Controls how the workbench router should pass this parameter to the workbench view. + * + * By default, parameters are passed via the workbench URL as matrix parameters. + * Marking a parameter as "transient" instructs the router to pass it via navigational state, useful for large objects. * - * By default, the workbench router passes the parameter via the workbench URL as matrix parameter to the workbench view - * that embeds the microfrontend. By marking the parameter as "transient", you can instruct the workbench router to pass it - * via navigational state instead of the workbench URL, for example to pass large objects. Since a transient parameter is not - * included in the workbench URL, it does not survive a page reload, i.e., is only available during the initial navigation of - * the microfrontend. Consequently, the microfrontend must be able to restore its state without this parameter present. + * Transient parameters are stored in the browser's session history, supporting back/forward navigation, but are lost on page reload. + * Therefore, microfrontends must be able to restore their state without relying on transient parameters. */ transient?: boolean; - + /** + * @inheritDoc + */ [property: string]: any; } diff --git a/projects/scion/workbench-client/src/lib/view/workbench-view-initializer.ts b/projects/scion/workbench-client/src/lib/view/workbench-view-initializer.ts index b0aa28f3f..24512d115 100644 --- a/projects/scion/workbench-client/src/lib/view/workbench-view-initializer.ts +++ b/projects/scion/workbench-client/src/lib/view/workbench-view-initializer.ts @@ -10,7 +10,7 @@ import {Beans, Initializer} from '@scion/toolkit/bean-manager'; import {ContextService} from '@scion/microfrontend-platform'; -import {WorkbenchView} from './workbench-view'; +import {ViewId, WorkbenchView} from './workbench-view'; import {ɵVIEW_ID_CONTEXT_KEY, ɵWorkbenchView} from './ɵworkbench-view'; /** @@ -21,7 +21,7 @@ import {ɵVIEW_ID_CONTEXT_KEY, ɵWorkbenchView} from './ɵworkbench-view'; export class WorkbenchViewInitializer implements Initializer { public async init(): Promise { - const viewId = await Beans.get(ContextService).lookup(ɵVIEW_ID_CONTEXT_KEY); + const viewId = await Beans.get(ContextService).lookup(ɵVIEW_ID_CONTEXT_KEY); if (viewId !== null) { const workbenchView = new ɵWorkbenchView(viewId); Beans.register(WorkbenchView, {useValue: workbenchView}); diff --git a/projects/scion/workbench-client/src/lib/view/workbench-view.ts b/projects/scion/workbench-client/src/lib/view/workbench-view.ts index 0fdb84143..3f2b231c6 100644 --- a/projects/scion/workbench-client/src/lib/view/workbench-view.ts +++ b/projects/scion/workbench-client/src/lib/view/workbench-view.ts @@ -12,25 +12,21 @@ import {Observable} from 'rxjs'; import {WorkbenchViewCapability} from './workbench-view-capability'; /** - * A view is a visual workbench component for displaying content stacked or side-by-side. + * Handle to interact with a view opened via {@link WorkbenchRouter}. * - * If a microfrontend lives in the context of a workbench view, regardless of its embedding level, it can inject an instance - * of this class to interact with the workbench view, such as setting view tab properties or closing the view. It further - * provides you access to the microfrontend capability and passed parameters. - * - * This object's lifecycle is bound to the workbench view and not to the navigation. In other words: If using hash-based routing - * in your app, no new instance will be constructed when navigating to a different microfrontend of the same application, or when - * re-routing to the same view capability, e.g., for updating the browser URL to persist navigation. Consequently, do not forget - * to unsubscribe from Observables of this class before displaying another microfrontend. + * The view microfrontend can inject this handle to interact with the view, such as setting the title, + * reading parameters, or closing it. * * @category View + * @see WorkbenchViewCapability + * @see WorkbenchRouter */ export abstract class WorkbenchView { /** * Represents the identity of this workbench view. */ - public abstract readonly id: string; + public abstract readonly id: ViewId; /** * Signals readiness, notifying the workbench that this view has completed initialization. @@ -107,44 +103,46 @@ export abstract class WorkbenchView { public abstract close(): void; /** - * Adds a listener to be notified just before closing this view. The closing event is cancelable, - * i.e., you can invoke {@link ViewClosingEvent.preventDefault} to prevent closing. + * Registers a guard to decide whether this view can be closed or not. + * The guard will be removed when navigating to another microfrontend. * - * The listener is removed when navigating to another microfrontend, whether from the same app or a different one. + * @see CanClose */ - public abstract addClosingListener(listener: ViewClosingListener): void; + public abstract addCanClose(canClose: CanClose): void; /** - * Removes the given listener. + * Unregisters the given guard. */ - public abstract removeClosingListener(listener: ViewClosingListener): void; + public abstract removeCanClose(canClose: CanClose): void; } /** - * Listener to be notified just before closing the workbench view. + * Guard that can be registered in {@link WorkbenchView} to decide whether the view can be closed. * - * @category View + * The following example registers a `CanClose` guard that asks the user whether the view can be closed. + * + * ```ts + * class MicrofrontendComponent implements CanClose { + * + * constructor() { + * Beans.get(WorkbenchView).addCanClose(this); + * } + * + * public async canClose(): Promise { + * const action = await Beans.get(WorkbenchMessageBoxService).open('Do you want to close this view?', { + * actions: {yes: 'Yes', no: 'No'}, + * }); + * return action === 'yes'; + * } + * } + * ``` */ -export interface ViewClosingListener { +export interface CanClose { /** - * Method invoked just before closing the workbench view. - * - * The closing event is cancelable, i.e., you can invoke {@link ViewClosingEvent.preventDefault} to prevent closing. - * - * Note that you can cancel the event only until the returned Promise resolves. For example, to ask the user - * for confirmation, you can use an async block and await user confirmation, as following: - * - * ```ts - * public async onClosing(event: ViewClosingEvent): Promise { - * const shouldClose = await askUserToConfirmClosing(); - * if (!shouldClose) { - * event.preventDefault(); - * } - * } - * ``` - */ - onClosing(event: ViewClosingEvent): void | Promise; + * Decides whether this view can be closed. + */ + canClose(): Observable | Promise | boolean; } /** @@ -179,3 +177,12 @@ export class ViewClosingEvent { export interface ViewSnapshot { params: ReadonlyMap; } + +/** + * Format of a view identifier. + * + * Each view is assigned a unique identifier (e.g., `view.1`, `view.2`, etc.). + * + * @category View + */ +export type ViewId = `view.${number}`; diff --git "a/projects/scion/workbench-client/src/lib/view/\311\265workbench-view.ts" "b/projects/scion/workbench-client/src/lib/view/\311\265workbench-view.ts" index e3ae41246..4b3ebfb4d 100644 --- "a/projects/scion/workbench-client/src/lib/view/\311\265workbench-view.ts" +++ "b/projects/scion/workbench-client/src/lib/view/\311\265workbench-view.ts" @@ -1,4 +1,4 @@ -/* + /* * Copyright (c) 2018-2024 Swiss Federal Railways * * This program and the accompanying materials are made @@ -9,14 +9,14 @@ */ import {Beans, PreDestroy} from '@scion/toolkit/bean-manager'; -import {merge, Observable, OperatorFunction, pipe, Subject, Subscription, take} from 'rxjs'; +import {firstValueFrom, merge, Observable, OperatorFunction, pipe, Subject, Subscription, take} from 'rxjs'; import {WorkbenchViewCapability} from './workbench-view-capability'; -import {ManifestService, mapToBody, Message, MessageClient, MessageHeaders, MicrofrontendPlatformClient, ResponseStatusCodes} from '@scion/microfrontend-platform'; +import {ManifestService, mapToBody, MessageClient, MicrofrontendPlatformClient} from '@scion/microfrontend-platform'; import {ɵWorkbenchCommands} from '../ɵworkbench-commands'; import {distinctUntilChanged, filter, map, mergeMap, shareReplay, skip, switchMap, takeUntil, tap} from 'rxjs/operators'; import {ɵMicrofrontendRouteParams} from '../routing/workbench-router'; import {Observables} from '@scion/toolkit/util'; -import {ViewClosingEvent, ViewClosingListener, ViewSnapshot, WorkbenchView} from './workbench-view'; +import {CanClose, ViewId, ViewSnapshot, WorkbenchView} from './workbench-view'; import {decorateObservable} from '../observable-decorator'; export class ɵWorkbenchView implements WorkbenchView, PreDestroy { @@ -32,8 +32,8 @@ export class ɵWorkbenchView implements WorkbenchView, PreDestroy { * Observable that emits before navigating to a different microfrontend of the same app. */ private _beforeInAppNavigation$ = new Subject(); - private _closingListeners = new Set(); - private _closingSubscription: Subscription | undefined; + private _canCloseGuards = new Set(); + private _canCloseSubscription: Subscription | undefined; public active$: Observable; public params$: Observable>; @@ -43,9 +43,9 @@ export class ɵWorkbenchView implements WorkbenchView, PreDestroy { params: new Map(), }; - constructor(public id: string) { + constructor(public id: ViewId) { this._beforeUnload$ = Beans.get(MessageClient).observe$(ɵWorkbenchCommands.viewUnloadingTopic(this.id)) - .pipe(map(() => undefined)); + .pipe(map(() => undefined), shareReplay({refCount: false, bufferSize: 1})); this.params$ = Beans.get(MessageClient).observe$>(ɵWorkbenchCommands.viewParamsTopic(this.id)) .pipe( @@ -84,8 +84,15 @@ export class ɵWorkbenchView implements WorkbenchView, PreDestroy { ) .subscribe(() => { this._beforeInAppNavigation$.next(); - this._closingListeners.clear(); - this._closingSubscription?.unsubscribe(); + this._canCloseGuards.clear(); + this._canCloseSubscription?.unsubscribe(); + }); + + // Detect navigation to a different view capability of another app. + this._beforeUnload$ + .pipe(takeUntil(this._destroy$)) + .subscribe(() => { + this._canCloseSubscription?.unsubscribe(); }); } @@ -162,60 +169,38 @@ export class ɵWorkbenchView implements WorkbenchView, PreDestroy { /** * @inheritDoc */ - public addClosingListener(listener: ViewClosingListener): void { - if (!this._closingListeners.has(listener) && this._closingListeners.add(listener).size === 1) { - // Subscribe to the closing event lazily when registering the first listener, so that the workbench only has to ask for a - // closing confirmation if a listener is actually installed. - this._closingSubscription = this.installClosingHandler(); + public addCanClose(canClose: CanClose): void { + // Subscribe to `CanClose` requests lazily when registering the first guard. + // The workbench will only invoke this guard if a guard is installed. + if (!this._canCloseGuards.has(canClose) && this._canCloseGuards.add(canClose).size === 1) { + this._canCloseSubscription = Beans.get(MessageClient).onMessage(ɵWorkbenchCommands.canCloseTopic(this.id), () => this.canClose()); } } /** * @inheritDoc */ - public removeClosingListener(listener: ViewClosingListener): void { - if (this._closingListeners.delete(listener) && this._closingListeners.size === 0) { - this._closingSubscription?.unsubscribe(); - this._closingSubscription = undefined; + public removeCanClose(canClose: CanClose): void { + if (this._canCloseGuards.delete(canClose) && this._canCloseGuards.size === 0) { + this._canCloseSubscription?.unsubscribe(); + this._canCloseSubscription = undefined; } } /** - * Installs a handler to be invoked by the workbench before closing this view. - */ - private installClosingHandler(): Subscription { - return Beans.get(MessageClient).observe$(ɵWorkbenchCommands.viewClosingTopic(this.id)) - .pipe( - switchMap(async (closeRequest: Message) => { - // Do not move the publishing of the response to a subsequent handler, because the subscription gets canceled when the last closing listener unsubscribes. - // See {@link removeClosingListener}. For example, if the listener unsubscribes immediately after handled prevention, subsequent handlers of this Observable - // chain would not be called, and neither would the subscribe handler. - const preventViewClosing = await this.isViewClosingPrevented(); - const replyTo = closeRequest.headers.get(MessageHeaders.ReplyTo); - await Beans.get(MessageClient).publish(replyTo, !preventViewClosing, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); - }), - takeUntil(merge(this._beforeUnload$, this._destroy$)), - ) - .subscribe(); - } - - /** - * Lets registered listeners prevent this view from closing. - * - * @return Promise that resolves to `true` if at least one listener prevents closing, or that resolves to `false` otherwise. + * Decides whether this view can be closed. */ - private async isViewClosingPrevented(): Promise { - for (const listener of this._closingListeners) { - const event = new ViewClosingEvent(); - await listener.onClosing(event); - if (event.isDefaultPrevented()) { - return true; + private async canClose(): Promise { + for (const guard of this._canCloseGuards) { + if (!await firstValueFrom(Observables.coerce(guard.canClose()), {defaultValue: true})) { + return false; } } - return false; + return true; } public preDestroy(): void { + this._canCloseSubscription?.unsubscribe(); this._destroy$.next(); } } diff --git a/projects/scion/workbench-client/src/lib/workbench-capabilities.enum.ts b/projects/scion/workbench-client/src/lib/workbench-capabilities.enum.ts index 8a8667e9b..cfaadf4d6 100644 --- a/projects/scion/workbench-client/src/lib/workbench-capabilities.enum.ts +++ b/projects/scion/workbench-client/src/lib/workbench-capabilities.enum.ts @@ -15,7 +15,7 @@ export enum WorkbenchCapabilities { /** * Contributes a microfrontend for display in workbench view. * - * A view is a visual workbench component for displaying content stacked or side-by-side. + * A view is a visual workbench element for displaying content stacked or side-by-side. */ View = 'view', /** diff --git "a/projects/scion/workbench-client/src/lib/\311\265workbench-commands.ts" "b/projects/scion/workbench-client/src/lib/\311\265workbench-commands.ts" index 11fc71eca..c4dc32989 100644 --- "a/projects/scion/workbench-client/src/lib/\311\265workbench-commands.ts" +++ "b/projects/scion/workbench-client/src/lib/\311\265workbench-commands.ts" @@ -8,6 +8,8 @@ * SPDX-License-Identifier: EPL-2.0 */ +import {ViewId} from './view/workbench-view'; + /** * Defines command endpoints for the communication between SCION Workbench and SCION Workbench Client. * @@ -18,53 +20,59 @@ export const ɵWorkbenchCommands = { /** * Computes the topic via which the title of a workbench view tab can be set. */ - viewTitleTopic: (viewId: string) => `ɵworkbench/views/${viewId}/title`, + viewTitleTopic: (viewId: ViewId | ':viewId') => `ɵworkbench/views/${viewId}/title`, /** * Computes the topic via which the heading of a workbench view tab can be set. */ - viewHeadingTopic: (viewId: string) => `ɵworkbench/views/${viewId}/heading`, + viewHeadingTopic: (viewId: ViewId | ':viewId') => `ɵworkbench/views/${viewId}/heading`, /** * Computes the topic via which a view tab can be marked dirty or pristine. */ - viewDirtyTopic: (viewId: string) => `ɵworkbench/views/${viewId}/dirty`, + viewDirtyTopic: (viewId: ViewId | ':viewId') => `ɵworkbench/views/${viewId}/dirty`, /** * Computes the topic via which a view tab can be made closable. */ - viewClosableTopic: (viewId: string) => `ɵworkbench/views/${viewId}/closable`, + viewClosableTopic: (viewId: ViewId | ':viewId') => `ɵworkbench/views/${viewId}/closable`, /** * Computes the topic via which a view can be closed. */ - viewCloseTopic: (viewId: string) => `ɵworkbench/views/${viewId}/close`, + viewCloseTopic: (viewId: ViewId | ':viewId') => `ɵworkbench/views/${viewId}/close`, /** * Computes the topic for notifying about view active state changes. * * The active state is published as a retained message. */ - viewActiveTopic: (viewId: string) => `ɵworkbench/views/${viewId}/active`, + viewActiveTopic: (viewId: ViewId) => `ɵworkbench/views/${viewId}/active`, /** - * Computes the topic for signaling that a view is about to be closed. + * Computes the topic to request closing confirmation of a view. + * + * When closing a view and if the microfrontend has subscribed to this topic, the workbench requests closing confirmation + * via this topic. By sending a `true` reply, the workbench continues with closing the view, by sending a `false` reply, + * closing is prevented. + */ + canCloseTopic: (viewId: ViewId) => `ɵworkbench/views/${viewId}/canClose`, + /** + * TODO [Angular 20] Remove legacy topic. * - * Just before closing the view and if the microfrontend has subscribed to this topic, the workbench requests - * a closing confirmation via this topic. By sending a `true` reply, the workbench continues with closing the view, - * by sending a `false` reply, closing is prevented. + * @deprecated since version 17.0.0-beta.8; Use `canCloseTopic` instead. */ - viewClosingTopic: (viewId: string) => `ɵworkbench/views/${viewId}/closing`, + viewClosingTopic: (viewId: ViewId) => `ɵworkbench/views/${viewId}/closing`, /** * Computes the topic for signaling that a microfrontend is about to be replaced by a microfrontend of another app. */ - viewUnloadingTopic: (viewId: string) => `ɵworkbench/views/${viewId}/unloading`, + viewUnloadingTopic: (viewId: ViewId) => `ɵworkbench/views/${viewId}/unloading`, /** * Computes the topic for updating params of a microfrontend view. */ - viewParamsUpdateTopic: (viewId: string, viewCapabilityId: string) => `ɵworkbench/views/${viewId}/capabilities/${viewCapabilityId}/params/update`, + viewParamsUpdateTopic: (viewId: ViewId, viewCapabilityId: string) => `ɵworkbench/views/${viewId}/capabilities/${viewCapabilityId}/params/update`, /** * Computes the topic for providing params to a view microfrontend. @@ -74,7 +82,7 @@ export const ɵWorkbenchCommands = { * * Params are published as a retained message. */ - viewParamsTopic: (viewId: string) => `ɵworkbench/views/${viewId}/params`, + viewParamsTopic: (viewId: ViewId) => `ɵworkbench/views/${viewId}/params`, /** * Computes the topic for observing the popup origin. diff --git a/projects/scion/workbench-client/src/public-api.ts b/projects/scion/workbench-client/src/public-api.ts index e54b5a67b..9ecacd055 100644 --- a/projects/scion/workbench-client/src/public-api.ts +++ b/projects/scion/workbench-client/src/public-api.ts @@ -14,7 +14,7 @@ export {WorkbenchClient} from './lib/workbench-client'; export {WorkbenchRouter, WorkbenchNavigationExtras, ɵMicrofrontendRouteParams, ɵViewParamsUpdateCommand} from './lib/routing/workbench-router'; export {WorkbenchViewCapability, ViewParamDefinition} from './lib/view/workbench-view-capability'; -export {WorkbenchView, ViewClosingListener, ViewClosingEvent, ViewSnapshot} from './lib/view/workbench-view'; +export {WorkbenchView, CanClose, ViewClosingEvent, ViewSnapshot, ViewId} from './lib/view/workbench-view'; export {ɵVIEW_ID_CONTEXT_KEY, ɵWorkbenchView} from './lib/view/ɵworkbench-view'; export {WorkbenchCapabilities} from './lib/workbench-capabilities.enum'; export {ɵWorkbenchCommands} from './lib/ɵworkbench-commands'; diff --git a/projects/scion/workbench/README.md b/projects/scion/workbench/README.md index d3b3130c7..2f75c2610 100644 --- a/projects/scion/workbench/README.md +++ b/projects/scion/workbench/README.md @@ -7,7 +7,7 @@ The workbench layout is a grid of parts. Parts are aligned relative to each othe The layout can be divided into a main and a peripheral area, with the main area as the primary place for opening views. The peripheral area arranges parts around the main area to provide navigation or context-sensitive assistance to support the user's workflow. Defining a main area is optional and recommended for applications requiring a dedicated and maximizable area for user interaction. -Multiple layouts, called perspectives, are supported. Perspectives can be switched with one perspective active at a time. Perspectives share the same main area, if any. +Multiple layouts, called perspectives, are supported. Perspectives can be switched. Only one perspective is active at a time. Perspectives share the same main area, if any. The sources for this package are in [SCION Workbench](https://github.com/SchweizerischeBundesbahnen/scion-workbench) repo. Please file issues and pull requests against that repo. diff --git a/projects/scion/workbench/package.json b/projects/scion/workbench/package.json index b124e19ab..31188ee17 100644 --- a/projects/scion/workbench/package.json +++ b/projects/scion/workbench/package.json @@ -23,16 +23,16 @@ "tslib": "^2.5.0" }, "peerDependencies": { - "@angular/common": "^17.0.0", - "@angular/core": "^17.0.0", - "@angular/cdk": "^17.0.0", - "@angular/animations": "^17.0.0", - "@angular/forms": "^17.0.0", - "@angular/router": "^17.0.0", + "@angular/common": "^17.0.6", + "@angular/core": "^17.0.6", + "@angular/cdk": "^17.0.6", + "@angular/animations": "^17.0.6", + "@angular/forms": "^17.0.6", + "@angular/router": "^17.0.6", "@scion/components": "^17.0.0", "@scion/toolkit": "^1.4.0", "@scion/microfrontend-platform": "^1.2.1", - "@scion/workbench-client": "^1.0.0-beta.21", + "@scion/workbench-client": "^1.0.0-beta.22", "rxjs": "^7.8.0" }, "peerDependenciesMeta": { diff --git a/projects/scion/workbench/src/lib/common/asserts.util.ts b/projects/scion/workbench/src/lib/common/asserts.util.ts index 70224bae5..0ff40d986 100644 --- a/projects/scion/workbench/src/lib/common/asserts.util.ts +++ b/projects/scion/workbench/src/lib/common/asserts.util.ts @@ -13,7 +13,7 @@ export function assertType(object: any, assert: {toBeOneOf: Type[] | Type object instanceof expectedType)) { const expectedType = Arrays.coerce(assert.toBeOneOf).map(it => it.name).join(' or '); const actualType = object.constructor.name; throw Error(`[AssertError] Object not of the expected type [expected=${expectedType}, actual=${actualType}].`); diff --git a/projects/scion/workbench/src/lib/common/grid-element-if-visible.pipe.ts b/projects/scion/workbench/src/lib/common/grid-element-if-visible.pipe.ts index d78d1cc2b..6b8d262a0 100644 --- a/projects/scion/workbench/src/lib/common/grid-element-if-visible.pipe.ts +++ b/projects/scion/workbench/src/lib/common/grid-element-if-visible.pipe.ts @@ -10,7 +10,7 @@ import {Pipe, PipeTransform} from '@angular/core'; import {MPart, MTreeNode} from '../layout/workbench-layout.model'; -import {isGridElementVisible} from '../layout/ɵworkbench-layout'; +import {WorkbenchLayouts} from '../layout/workbench-layouts.util'; /** * Returns given grid element, but only if visible. @@ -21,7 +21,7 @@ import {isGridElementVisible} from '../layout/ɵworkbench-layout'; export class GridElementIfVisiblePipe implements PipeTransform { public transform(gridElement: MTreeNode | MPart | null | undefined): MTreeNode | MPart | null { - if (gridElement && isGridElementVisible(gridElement)) { + if (gridElement && WorkbenchLayouts.isGridElementVisible(gridElement)) { return gridElement; } return null; diff --git a/projects/scion/workbench/src/lib/common/objects.util.spec.ts b/projects/scion/workbench/src/lib/common/objects.util.spec.ts new file mode 100644 index 000000000..3597adc74 --- /dev/null +++ b/projects/scion/workbench/src/lib/common/objects.util.spec.ts @@ -0,0 +1,48 @@ +/* + * 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 + */ + +import {Objects} from './objects.util'; + +describe('Objects.keys', () => { + + it('should return keys', () => { + const object = {key1: 'value1', key2: 'value2'}; + expect(Objects.keys(object)).toEqual(['key1', 'key2']); + }); + + it('should preserve data type of keys', () => { + type Key = `key.${number}`; + const object: Record = {'key.1': 'value1', 'key.2': 'value2'}; + expect(Objects.keys(object) satisfies Key[]).toEqual(['key.1', 'key.2']); + }); +}); + +describe('Objects.entries', () => { + + it('should return entries', () => { + const object = {key1: 'value1', key2: 'value2'}; + expect(Objects.entries(object)).toEqual([['key1', 'value1'], ['key2', 'value2']]); + }); + + it('should preserve data type of keys', () => { + type Key = `key.${number}`; + const object: Record = {'key.1': 'value1', 'key.2': 'value2'}; + expect(Objects.entries(object) satisfies Array<[Key, string]>).toEqual([['key.1', 'value1'], ['key.2', 'value2']]); + }); +}); + +describe('Objects.withoutUndefinedEntries', () => { + + it('should preserve data type', () => { + type Type = {key1?: string; key2?: string; key3?: string}; + const object: Type = {key1: 'value1', key2: undefined, key3: 'value3'}; + expect(Objects.withoutUndefinedEntries(object)).toEqual({key1: 'value1', key3: 'value3'}); + }); +}); diff --git a/projects/scion/workbench/src/lib/common/objects.util.ts b/projects/scion/workbench/src/lib/common/objects.util.ts new file mode 100644 index 000000000..d6b946c86 --- /dev/null +++ b/projects/scion/workbench/src/lib/common/objects.util.ts @@ -0,0 +1,38 @@ +/* + * 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 + */ + +import {Dictionaries} from '@scion/toolkit/util'; + +/** + * Provides helper functions for working with objects. + */ +export const Objects = { + + /** + * Like {@link Object.keys}, but preserving the data type of keys. + */ + keys: (object: T): Array => { + return Object.keys(object as Record) as Array; + }, + + /** + * Like {@link Object.entries}, but preserving the data type of keys. + */ + entries: (object: Record | ArrayLike): Array<[K, V]> => { + return Object.entries(object) as Array<[K, V]>; + }, + + /** + * Like {@link Dictionaries.withoutUndefinedEntries}, but preserving the object data type. + */ + withoutUndefinedEntries: (object: T & Record): T => { + return Dictionaries.withoutUndefinedEntries(object) as T; + }, +} as const; diff --git a/projects/scion/workbench/src/lib/common/uuid.util.ts b/projects/scion/workbench/src/lib/common/uuid.util.ts new file mode 100644 index 000000000..def2596b4 --- /dev/null +++ b/projects/scion/workbench/src/lib/common/uuid.util.ts @@ -0,0 +1,23 @@ +/* + * 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 + */ + +import {UUID} from '@scion/toolkit/uuid'; + +/** + * Format of a UUID (universally unique identifier) compliant with the RFC 4122 version 4. + */ +export type UUID = `${string}-${string}-${string}-${string}-${string}`; + +/** + * Generates a UUID (universally unique identifier) compliant with the RFC 4122 version 4. + */ +export function randomUUID(): UUID { + return UUID.randomUUID() as UUID; +} diff --git a/projects/scion/workbench/src/lib/dialog/workbench-dialog.options.ts b/projects/scion/workbench/src/lib/dialog/workbench-dialog.options.ts index 93ae9ca42..4a08b38d6 100644 --- a/projects/scion/workbench/src/lib/dialog/workbench-dialog.options.ts +++ b/projects/scion/workbench/src/lib/dialog/workbench-dialog.options.ts @@ -9,6 +9,7 @@ */ import {Injector} from '@angular/core'; +import {ViewId} from '../view/workbench-view.model'; /** * Controls how to open a dialog. @@ -30,7 +31,7 @@ export interface WorkbenchDialogOptions { * Controls which area of the application to block by the dialog. * * - **Application-modal:** - * Use to block the workbench, or the browser's viewport if configured in {@link WorkbenchModuleConfig.dialog.modalityScope}. + * Use to block the workbench, or the browser's viewport if configured in {@link WorkbenchConfig.dialog.modalityScope}. * * - **View-modal:** * Use to block only the contextual view of the dialog, allowing the user to interact with other views. @@ -56,7 +57,7 @@ export interface WorkbenchDialogOptions { injector?: Injector; /** - * Specifies CSS class(es) to be added to the dialog, useful in end-to-end tests for locating the dialog. + * Specifies CSS class(es) to add to the dialog, e.g., to locate the dialog in tests. */ cssClass?: string | string[]; @@ -74,6 +75,6 @@ export interface WorkbenchDialogOptions { * * By default, if opening the dialog in the context of a view, that view is used as the contextual view. */ - viewId?: string; + viewId?: ViewId; }; } diff --git a/projects/scion/workbench/src/lib/dialog/workbench-dialog.registry.ts b/projects/scion/workbench/src/lib/dialog/workbench-dialog.registry.ts index 8444629aa..f5ed52436 100644 --- a/projects/scion/workbench/src/lib/dialog/workbench-dialog.registry.ts +++ b/projects/scion/workbench/src/lib/dialog/workbench-dialog.registry.ts @@ -12,6 +12,7 @@ import {Injectable, OnDestroy} from '@angular/core'; import {ɵWorkbenchDialog} from './ɵworkbench-dialog'; import {BehaviorSubject, Observable} from 'rxjs'; import {distinctUntilChanged, map} from 'rxjs/operators'; +import {ViewId} from '../view/workbench-view.model'; /** * Registry for {@link ɵWorkbenchDialog} objects. @@ -40,7 +41,7 @@ export class WorkbenchDialogRegistry implements OnDestroy { /** * Returns currently opened dialogs, sorted by the time they were opened, based on the specified filter. */ - public dialogs(filter?: {viewId?: string} | ((dialog: ɵWorkbenchDialog) => boolean)): ɵWorkbenchDialog[] { + public dialogs(filter?: {viewId?: ViewId} | ((dialog: ɵWorkbenchDialog) => boolean)): ɵWorkbenchDialog[] { const filterFn = typeof filter === 'function' ? filter : (dialog: ɵWorkbenchDialog) => !filter?.viewId || dialog.context.view?.id === filter.viewId; return this._dialogs$.value.filter(filterFn); } @@ -52,7 +53,7 @@ export class WorkbenchDialogRegistry implements OnDestroy { * This can be either a view-modal or an application-modal dialog. Otherwise, returns * the topmost application-modal dialog. */ - public top$(context?: {viewId?: string}): Observable<ɵWorkbenchDialog | null> { + public top$(context?: {viewId?: ViewId}): Observable<ɵWorkbenchDialog | null> { return this._dialogs$ .pipe( map(() => this.top(context)), @@ -67,7 +68,7 @@ export class WorkbenchDialogRegistry implements OnDestroy { * This can be either a view-modal or an application-modal dialog. Otherwise, returns * the topmost application-modal dialog. */ - public top(context?: {viewId?: string}): ɵWorkbenchDialog | null { + public top(context?: {viewId?: ViewId}): ɵWorkbenchDialog | null { return this.dialogs(dialog => !dialog.context.view || dialog.context.view.id === context?.viewId).at(-1) ?? null; } diff --git a/projects/scion/workbench/src/lib/dialog/workbench-dialog.service.ts b/projects/scion/workbench/src/lib/dialog/workbench-dialog.service.ts index f5b315633..5b8b8bd9e 100644 --- a/projects/scion/workbench/src/lib/dialog/workbench-dialog.service.ts +++ b/projects/scion/workbench/src/lib/dialog/workbench-dialog.service.ts @@ -25,7 +25,7 @@ import {ɵWorkbenchDialogService} from './ɵworkbench-dialog.service'; * A dialog can be view-modal or application-modal. * * A view-modal dialog blocks only a specific view, allowing the user to interact with other views. An application-modal dialog blocks - * the workbench, or the browser's viewport if configured in {@link WorkbenchModuleConfig.dialog.modalityScope}. + * the workbench, or the browser's viewport if configured in {@link WorkbenchConfig.dialog.modalityScope}. * * ## Dialog Stack * Multiple dialogs are stacked, and only the topmost dialog in each modality stack can be interacted with. diff --git a/projects/scion/workbench/src/lib/dialog/workbench-dialog.ts b/projects/scion/workbench/src/lib/dialog/workbench-dialog.ts index 67bfa8f6c..70ebccf14 100644 --- a/projects/scion/workbench/src/lib/dialog/workbench-dialog.ts +++ b/projects/scion/workbench/src/lib/dialog/workbench-dialog.ts @@ -49,7 +49,7 @@ export abstract class WorkbenchDialog { public abstract resizable: boolean; /** - * Specifies CSS class(es) to be added to the dialog, useful in end-to-end tests for locating the dialog. + * Specifies CSS class(es) to add to the dialog, e.g., to locate the dialog in tests. */ public abstract cssClass: string | string[]; diff --git "a/projects/scion/workbench/src/lib/dialog/\311\265workbench-dialog.ts" "b/projects/scion/workbench/src/lib/dialog/\311\265workbench-dialog.ts" index e365ed659..63e56f02b 100644 --- "a/projects/scion/workbench/src/lib/dialog/\311\265workbench-dialog.ts" +++ "b/projects/scion/workbench/src/lib/dialog/\311\265workbench-dialog.ts" @@ -25,7 +25,7 @@ import {subscribeInside} from '@scion/toolkit/operators'; import {ViewDragService} from '../view-dnd/view-drag.service'; import {WORKBENCH_ELEMENT_REF} from '../content-projection/view-container.reference'; import {Arrays} from '@scion/toolkit/util'; -import {WorkbenchModuleConfig} from '../workbench-module-config'; +import {WorkbenchConfig} from '../workbench-config'; import {distinctUntilChanged, filter} from 'rxjs/operators'; import {WorkbenchDialogActionDirective} from './dialog-footer/workbench-dialog-action.directive'; import {WorkbenchDialogFooterDirective} from './dialog-footer/workbench-dialog-footer.directive'; @@ -33,7 +33,7 @@ import {WorkbenchDialogHeaderDirective} from './dialog-header/workbench-dialog-h import {Disposable} from '../common/disposable'; import {Blockable} from '../glass-pane/blockable'; import {Blocking} from '../glass-pane/blocking'; -import {UUID} from '@scion/toolkit/uuid'; +import {randomUUID} from '../common/uuid.util'; /** @inheritDoc */ export class ɵWorkbenchDialog implements WorkbenchDialog, Blockable, Blocking { @@ -42,7 +42,7 @@ export class ɵWorkbenchDialog implements WorkbenchDialog, Block private readonly _portal: ComponentPortal; private readonly _workbenchDialogRegistry = inject(WorkbenchDialogRegistry); private readonly _zone = inject(NgZone); - private readonly _workbenchModuleConfig = inject(WorkbenchModuleConfig); + private readonly _workbenchConfig = inject(WorkbenchConfig); private readonly _destroyRef = new ɵDestroyRef(); private readonly _attached$: Observable; private _blink$ = new Subject(); @@ -57,7 +57,7 @@ export class ɵWorkbenchDialog implements WorkbenchDialog, Block /** * Unique identity of this dialog. */ - public readonly id = UUID.randomUUID(); + public readonly id = randomUUID(); /** * Indicates whether this dialog is blocked by other dialog(s) that overlay this dialog. */ @@ -255,7 +255,7 @@ export class ɵWorkbenchDialog implements WorkbenchDialog, Block if (this.context.view) { return this.context.view.portal.attached$; } - if (this._workbenchModuleConfig.dialog?.modalityScope === 'viewport') { + if (this._workbenchConfig.dialog?.modalityScope === 'viewport') { return of(true); } return inject(WORKBENCH_ELEMENT_REF).ref$.pipe(map(ref => !!ref)); @@ -289,7 +289,7 @@ export class ɵWorkbenchDialog implements WorkbenchDialog, Block * Aligns this dialog with the boundaries of the host element. */ private stickToHostElement(): void { - if (this._options.modality === 'application' && this._workbenchModuleConfig.dialog?.modalityScope === 'viewport') { + if (this._options.modality === 'application' && this._workbenchConfig.dialog?.modalityScope === 'viewport') { setStyle(this._overlayRef.hostElement, {inset: '0'}); } else { diff --git a/projects/scion/workbench/src/lib/filter-field/filter-field.component.ts b/projects/scion/workbench/src/lib/filter-field/filter-field.component.ts index 71d4d2983..80654c684 100644 --- a/projects/scion/workbench/src/lib/filter-field/filter-field.component.ts +++ b/projects/scion/workbench/src/lib/filter-field/filter-field.component.ts @@ -13,8 +13,8 @@ import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, ReactiveFormsModul import {noop} from 'rxjs'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y'; -import {UUID} from '@scion/toolkit/uuid'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {randomUUID} from '../common/uuid.util'; /** * Provides a filter control. @@ -35,7 +35,7 @@ export class FilterFieldComponent implements ControlValueAccessor, OnDestroy { private _cvaChangeFn: (value: any) => void = noop; private _cvaTouchedFn: () => void = noop; - public readonly id = UUID.randomUUID(); + public readonly id = randomUUID(); /** * Sets focus order in sequential keyboard navigation. diff --git a/projects/scion/workbench/src/lib/layout/grid-element/grid-element.component.ts b/projects/scion/workbench/src/lib/layout/grid-element/grid-element.component.ts index b20a2f64d..1c573e155 100644 --- a/projects/scion/workbench/src/lib/layout/grid-element/grid-element.component.ts +++ b/projects/scion/workbench/src/lib/layout/grid-element/grid-element.component.ts @@ -10,14 +10,14 @@ import {Component, HostBinding, Input, OnChanges, SimpleChanges, TrackByFunction} from '@angular/core'; import {MPart, MTreeNode} from '../workbench-layout.model'; -import {WorkbenchRouter} from '../../routing/workbench-router.service'; +import {ɵWorkbenchRouter} from '../../routing/ɵworkbench-router.service'; import {WorkbenchLayoutService} from '../workbench-layout.service'; import {InstanceofPipe} from '../../common/instanceof.pipe'; import {PortalModule} from '@angular/cdk/portal'; import {PartPortalPipe} from '../../part/part-portal.pipe'; import {NgFor, NgIf} from '@angular/common'; import {SciSashboxComponent, SciSashDirective} from '@scion/components/sashbox'; -import {isGridElementVisible} from '../ɵworkbench-layout'; +import {WorkbenchLayouts} from '../workbench-layouts.util'; /** * Renders a {@link MTreeNode} or {@link MPart}. @@ -65,7 +65,7 @@ export class GridElementComponent implements OnChanges { @Input({required: true}) public element!: MTreeNode | MPart; - constructor(private _workbenchRouter: WorkbenchRouter, private _workbenchLayoutService: WorkbenchLayoutService) { + constructor(private _workbenchRouter: ɵWorkbenchRouter, private _workbenchLayoutService: WorkbenchLayoutService) { } public ngOnChanges(changes: SimpleChanges): void { @@ -82,12 +82,12 @@ export class GridElementComponent implements OnChanges { public onSashEnd(treeNode: MTreeNode, [sashSize1, sashSize2]: number[]): void { const ratio = sashSize1 / (sashSize1 + sashSize2); this._workbenchLayoutService.notifyDragEnding(); - this._workbenchRouter.ɵnavigate(layout => layout.setSplitRatio(treeNode.nodeId, ratio)).then(); + this._workbenchRouter.navigate(layout => layout.setSplitRatio(treeNode.nodeId, ratio)).then(); } private computeChildren(treeNode: MTreeNode): ChildElement[] { - const child1Visible = isGridElementVisible(treeNode.child1); - const child2Visible = isGridElementVisible(treeNode.child2); + const child1Visible = WorkbenchLayouts.isGridElementVisible(treeNode.child1); + const child2Visible = WorkbenchLayouts.isGridElementVisible(treeNode.child2); if (child1Visible && child2Visible) { const [size1, size2] = calculateSashSizes(treeNode.ratio); diff --git a/projects/scion/workbench/src/lib/layout/main-area-layout/main-area-layout.component.ts b/projects/scion/workbench/src/lib/layout/main-area-layout/main-area-layout.component.ts index 0b9beb086..d36b2f338 100644 --- a/projects/scion/workbench/src/lib/layout/main-area-layout/main-area-layout.component.ts +++ b/projects/scion/workbench/src/lib/layout/main-area-layout/main-area-layout.component.ts @@ -86,7 +86,9 @@ export class MainAreaLayoutComponent { workbenchId: event.dragData.workbenchId, partId: event.dragData.partId, viewId: event.dragData.viewId, + alternativeViewId: event.dragData.alternativeViewId, viewUrlSegments: event.dragData.viewUrlSegments, + navigationHint: event.dragData.navigationHint, classList: event.dragData.classList, }, target: GridDropTargets.resolve({ diff --git a/projects/scion/workbench/src/lib/layout/migration/model/workbench-layout-migration-v1.model.ts b/projects/scion/workbench/src/lib/layout/migration/model/workbench-layout-migration-v1.model.ts new file mode 100644 index 000000000..43e266fd6 --- /dev/null +++ b/projects/scion/workbench/src/lib/layout/migration/model/workbench-layout-migration-v1.model.ts @@ -0,0 +1,28 @@ +/* + * 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 + */ + +export interface MPartV1 { + partId: string; + viewIds: string[]; + activeViewId?: string; +} + +export interface MTreeNodeV1 { + nodeId: string; + child1: MTreeNodeV1 | MPartV1; + child2: MTreeNodeV1 | MPartV1; + ratio: number; + direction: 'column' | 'row'; +} + +export interface MPartsLayoutV1 { + root: MTreeNodeV1 | MPartV1; + activePartId: string; +} diff --git a/projects/scion/workbench/src/lib/layout/migration/model/workbench-layout-migration-v2.model.ts b/projects/scion/workbench/src/lib/layout/migration/model/workbench-layout-migration-v2.model.ts new file mode 100644 index 000000000..f1e0c63e2 --- /dev/null +++ b/projects/scion/workbench/src/lib/layout/migration/model/workbench-layout-migration-v2.model.ts @@ -0,0 +1,35 @@ +/* + * 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 + */ + +export interface MPartV2 { + type: 'MPart'; + id: string; + views: MViewV2[]; + activeViewId?: string; + structural: boolean; +} + +export interface MTreeNodeV2 { + type: 'MTreeNode'; + nodeId: string; + child1: MTreeNodeV2 | MPartV2; + child2: MTreeNodeV2 | MPartV2; + ratio: number; + direction: 'column' | 'row'; +} + +export interface MPartGridV2 { + root: MTreeNodeV2 | MPartV2; + activePartId: string; +} + +export interface MViewV2 { + id: string; +} diff --git a/projects/scion/workbench/src/lib/layout/migration/model/workbench-layout-migration-v3.model.ts b/projects/scion/workbench/src/lib/layout/migration/model/workbench-layout-migration-v3.model.ts new file mode 100644 index 000000000..d75f1fe46 --- /dev/null +++ b/projects/scion/workbench/src/lib/layout/migration/model/workbench-layout-migration-v3.model.ts @@ -0,0 +1,45 @@ +/* + * 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 + */ + +export interface MPartV3 { + type: 'MPart'; + id: string; + views: MViewV3[]; + activeViewId?: ViewIdV3; + structural: boolean; +} + +export interface MTreeNodeV3 { + type: 'MTreeNode'; + nodeId: string; + child1: MTreeNodeV3 | MPartV3; + child2: MTreeNodeV3 | MPartV3; + ratio: number; + direction: 'column' | 'row'; +} + +export interface MPartGridV3 { + root: MTreeNodeV3 | MPartV3; + activePartId: string; +} + +export interface MViewV3 { + id: ViewIdV3; + alternativeId?: string; + cssClass?: string[]; + navigation?: { + hint?: string; + cssClass?: string[]; + }; +} + +export type ViewIdV3 = `view.${number}`; + +export const VIEW_ID_PREFIX_V3 = 'view.'; diff --git a/projects/scion/workbench/src/lib/layout/migration/model/workbench-layout-migration-v4.model.ts b/projects/scion/workbench/src/lib/layout/migration/model/workbench-layout-migration-v4.model.ts new file mode 100644 index 000000000..a265eaa19 --- /dev/null +++ b/projects/scion/workbench/src/lib/layout/migration/model/workbench-layout-migration-v4.model.ts @@ -0,0 +1,46 @@ +/* + * 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 + */ + +export interface MPartV4 { + type: 'MPart'; + id: string; + views: MViewV4[]; + activeViewId?: ViewIdV4; + structural: boolean; +} + +export interface MTreeNodeV4 { + type: 'MTreeNode'; + nodeId: string; + child1: MTreeNodeV4 | MPartV4; + child2: MTreeNodeV4 | MPartV4; + ratio: number; + direction: 'column' | 'row'; +} + +export interface MPartGridV4 { + root: MTreeNodeV4 | MPartV4; + activePartId: string; +} + +export interface MViewV4 { + id: ViewIdV4; + alternativeId?: string; + uid: string; + cssClass?: string[]; + markedForRemoval?: true; + navigation?: { + hint?: string; + cssClass?: string[]; + }; +} + +export type ViewIdV4 = `view.${number}`; + diff --git a/projects/scion/workbench/src/lib/layout/migration/workbench-layout-v1-migrator.service.ts b/projects/scion/workbench/src/lib/layout/migration/workbench-layout-migration-v2.service.ts similarity index 68% rename from projects/scion/workbench/src/lib/layout/migration/workbench-layout-v1-migrator.service.ts rename to projects/scion/workbench/src/lib/layout/migration/workbench-layout-migration-v2.service.ts index 4e25e98dd..1afba079e 100644 --- a/projects/scion/workbench/src/lib/layout/migration/workbench-layout-v1-migrator.service.ts +++ b/projects/scion/workbench/src/lib/layout/migration/workbench-layout-migration-v2.service.ts @@ -9,26 +9,27 @@ */ import {Injectable} from '@angular/core'; -import {MPart, MPartGrid, MTreeNode} from '../workbench-layout.model'; +import {MPartsLayoutV1, MPartV1, MTreeNodeV1} from './model/workbench-layout-migration-v1.model'; +import {MPartGridV2, MPartV2, MTreeNodeV2} from './model/workbench-layout-migration-v2.model'; +import {WorkbenchMigration} from '../../migration/workbench-migration'; /** - * Migrates a workbench layout in version 1 to the latest version. + * Migrates the workbench layout from version 1 to version 2. * - * TODO [Angular 18] Remove migrator. + * TODO [Angular 20] Remove migrator. */ @Injectable({providedIn: 'root'}) -export class WorkbenchLayoutV1Migrator { - +export class WorkbenchLayoutMigrationV2 implements WorkbenchMigration { public migrate(json: string): string { const partsLayoutV1: MPartsLayoutV1 = JSON.parse(json); - const partsGrid: MPartGrid = { + const partsGridV2: MPartGridV2 = { root: this.migrateGridElement(partsLayoutV1.root), activePartId: partsLayoutV1.activePartId, }; - return JSON.stringify(partsGrid); + return JSON.stringify(partsGridV2); } - private migrateGridElement(elementV1: MTreeNodeV1 | MPartV1): MTreeNode | MPart { + private migrateGridElement(elementV1: MTreeNodeV1 | MPartV1): MTreeNodeV2 | MPartV2 { if (elementV1.hasOwnProperty('partId')) { // eslint-disable-line no-prototype-builtins const partV1 = elementV1 as MPartV1; return { @@ -51,27 +52,7 @@ export class WorkbenchLayoutV1Migrator { }; } else { - throw Error(`[WorkbenchLayoutError] Unable to migrate to the latest version. Expected element to be of type 'MPart' or 'MTreeNode'. [version=1, element=${elementV1}]`); + throw Error(`[WorkbenchLayoutError] Unable to migrate to the latest version. Expected element to be of type 'MPart' or 'MTreeNode'. [version=1, element=${JSON.stringify(elementV1)}]`); } } } - -interface MPartsLayoutV1 { - root: MTreeNodeV1 | MPartV1; - activePartId: string; -} - -interface MTreeNodeV1 { - nodeId: string; - child1: MTreeNodeV1 | MPartV1; - child2: MTreeNodeV1 | MPartV1; - ratio: number; - direction: 'column' | 'row'; -} - -interface MPartV1 { - partId: string; - parent?: MTreeNodeV1; - viewIds: string[]; - activeViewId?: string; -} diff --git a/projects/scion/workbench/src/lib/layout/migration/workbench-layout-migration-v3.service.ts b/projects/scion/workbench/src/lib/layout/migration/workbench-layout-migration-v3.service.ts new file mode 100644 index 000000000..f79ae86ac --- /dev/null +++ b/projects/scion/workbench/src/lib/layout/migration/workbench-layout-migration-v3.service.ts @@ -0,0 +1,128 @@ +/* + * 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 + */ + +import {Injectable} from '@angular/core'; +import {MPartGridV2, MPartV2, MTreeNodeV2, MViewV2} from './model/workbench-layout-migration-v2.model'; +import {MPartGridV3, MPartV3, MTreeNodeV3, MViewV3, VIEW_ID_PREFIX_V3, ViewIdV3} from './model/workbench-layout-migration-v3.model'; +import {Router, UrlTree} from '@angular/router'; +import {WorkbenchMigration} from '../../migration/workbench-migration'; +import {RouterUtils} from '../../routing/router.util'; + +/** + * Migrates the workbench layout from version 2 to version 3. + * + * TODO [Angular 20] Remove migrator. + */ +@Injectable({providedIn: 'root'}) +export class WorkbenchLayoutMigrationV3 implements WorkbenchMigration { + + constructor(private _router: Router) { + } + + public migrate(json: string): string { + const partGridV2: MPartGridV2 = JSON.parse(json); + + // Consider the ids of views contained in the URL as already used. + // Otherwise, when migrating the main area and using a view id already present in the perspective, + // the view outlet would not be removed from the URL, resulting the migrated view to display + // "Page Not Found" or incorrect content. + const viewOutlets = RouterUtils.parseViewOutlets(this.getCurrentUrl()); + const usedViewIds = new Set([...viewOutlets.keys(), ...collectViewIds(partGridV2.root)]); + + // Migrate the grid. + const partGridV3: MPartGridV3 = { + root: migrateGridElement(partGridV2.root), + activePartId: partGridV2.activePartId, + }; + return JSON.stringify(partGridV3); + + function migrateGridElement(elementV2: MTreeNodeV2 | MPartV2): MTreeNodeV3 | MPartV3 { + switch (elementV2.type) { + case 'MTreeNode': + return migrateNode(elementV2); + case 'MPart': + return migratePart(elementV2); + default: + throw Error(`[WorkbenchLayoutError] Unable to migrate to the latest version. Expected element to be of type 'MPart' or 'MTreeNode'. [version=2, element=${JSON.stringify(elementV2)}]`); + } + } + + function migrateNode(nodeV2: MTreeNodeV2): MTreeNodeV3 { + return { + type: 'MTreeNode', + nodeId: nodeV2.nodeId, + child1: migrateGridElement(nodeV2.child1), + child2: migrateGridElement(nodeV2.child2), + ratio: nodeV2.ratio, + direction: nodeV2.direction, + }; + } + + function migratePart(partV2: MPartV2): MPartV3 { + const partV3: MPartV3 = { + type: 'MPart', + id: partV2.id, + structural: partV2.structural, + views: [], + }; + + // Add views and set the active view. + partV2.views.forEach((viewV2: MViewV2) => { + const viewV3: MViewV3 = migrateView(viewV2); + if (partV2.activeViewId === viewV2.id) { + partV3.activeViewId = viewV3.id; + } + partV3.views.push(viewV3); + usedViewIds.add(viewV3.id); + }); + + return partV3; + } + + function migrateView(viewV2: MViewV2): MViewV3 { + if (isViewId(viewV2.id)) { + return {id: viewV2.id, navigation: {}}; + } + else { + return {id: computeNextViewId(usedViewIds), navigation: {hint: viewV2.id}}; + } + } + } + + private getCurrentUrl(): UrlTree { + return this._router.getCurrentNavigation()?.initialUrl ?? this._router.parseUrl(this._router.url); + } +} + +function computeNextViewId(viewIds: Iterable): ViewIdV3 { + const ids = Array.from(viewIds) + .map(viewId => Number(viewId.substring(VIEW_ID_PREFIX_V3.length))) + .reduce((set, id) => set.add(id), new Set()); + + for (let i = 1; i <= ids.size; i++) { + if (!ids.has(i)) { + return VIEW_ID_PREFIX_V3.concat(`${i}`) as ViewIdV3; + } + } + return VIEW_ID_PREFIX_V3.concat(`${ids.size + 1}`) as ViewIdV3; +} + +function isViewId(viewId: string): viewId is ViewIdV3 { + return viewId.startsWith(VIEW_ID_PREFIX_V3); +} + +function collectViewIds(node: MPartV2 | MTreeNodeV2): Set { + if (node.type === 'MPart') { + return new Set(node.views.map(view => view.id).filter(isViewId)); + } + else { + return new Set([...collectViewIds(node.child1), ...collectViewIds(node.child2)]); + } +} diff --git a/projects/scion/workbench/src/lib/layout/migration/workbench-layout-migration-v4.service.ts b/projects/scion/workbench/src/lib/layout/migration/workbench-layout-migration-v4.service.ts new file mode 100644 index 000000000..0b97f5268 --- /dev/null +++ b/projects/scion/workbench/src/lib/layout/migration/workbench-layout-migration-v4.service.ts @@ -0,0 +1,61 @@ +/* + * 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 + */ + +import {Injectable} from '@angular/core'; +import {MPartGridV3, MPartV3, MTreeNodeV3, MViewV3} from './model/workbench-layout-migration-v3.model'; +import {WorkbenchMigration} from '../../migration/workbench-migration'; +import {MPartGridV4, MPartV4, MTreeNodeV4, MViewV4} from './model/workbench-layout-migration-v4.model'; + +/** + * Migrates the workbench layout from version 3 to version 4. + * + * TODO [Angular 20] Remove migrator. + */ +@Injectable({providedIn: 'root'}) +export class WorkbenchLayoutMigrationV4 implements WorkbenchMigration { + + public migrate(json: string): string { + const partGridV3: MPartGridV3 = JSON.parse(json); + + // Migrate the grid. + const partGridV4: MPartGridV4 = { + ...partGridV3, + root: migrateGridElement(partGridV3.root), + }; + return JSON.stringify(partGridV4); + + function migrateGridElement(elementV3: MTreeNodeV3 | MPartV3): MTreeNodeV4 | MPartV4 { + switch (elementV3.type) { + case 'MTreeNode': + return migrateNode(elementV3); + case 'MPart': + return migratePart(elementV3); + default: + throw Error(`[WorkbenchLayoutError] Unable to migrate to the latest version. Expected element to be of type 'MPart' or 'MTreeNode'. [version=3, element=${JSON.stringify(elementV3)}]`); + } + } + + function migrateNode(nodeV3: MTreeNodeV3): MTreeNodeV4 { + return { + ...nodeV3, + child1: migrateGridElement(nodeV3.child1), + child2: migrateGridElement(nodeV3.child2), + }; + } + + function migratePart(partV3: MPartV3): MPartV4 { + return {...partV3, views: partV3.views.map(migrateView)}; + } + + function migrateView(viewV3: MViewV3): MViewV4 { + return {...viewV3, uid: undefined!}; // `uid` is transient, i.e., set when deserializing the grid. + } + } +} diff --git a/projects/scion/workbench/src/lib/layout/migration/workbench-layout-migrator.service.ts b/projects/scion/workbench/src/lib/layout/migration/workbench-layout-migrator.service.ts deleted file mode 100644 index 4b4730dd0..000000000 --- a/projects/scion/workbench/src/lib/layout/migration/workbench-layout-migrator.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 - */ - -import {Injectable} from '@angular/core'; -import {WorkbenchLayoutV1Migrator} from './workbench-layout-v1-migrator.service'; - -/** - * Migrates a workbench layout to the latest version. - */ -@Injectable({providedIn: 'root'}) -export class WorkbenchLayoutMigrator { - - constructor(private _workbenchLayoutV1Migrator: WorkbenchLayoutV1Migrator) { - } - - /** - * Migrates a workbench layout to the latest version. - */ - public migrate(version: number, json: string): string { - switch (version) { - case 1: - return this._workbenchLayoutV1Migrator.migrate(json); - default: - throw Error(`[WorkbenchLayoutError] Unsupported workbench layout version. Unable to migrate to the latest version. [version=${version}, layout=${json}]`); - } - } -} diff --git a/projects/scion/workbench/src/lib/layout/workbench-layout.component.spec.ts b/projects/scion/workbench/src/lib/layout/workbench-layout.component.spec.ts index 5b9324e8e..2ea47982f 100644 --- a/projects/scion/workbench/src/lib/layout/workbench-layout.component.spec.ts +++ b/projects/scion/workbench/src/lib/layout/workbench-layout.component.spec.ts @@ -9,25 +9,23 @@ */ import {TestBed} from '@angular/core/testing'; -import {Router, RouterOutlet, UrlSegment} from '@angular/router'; +import {provideRouter, Router, RouterOutlet} from '@angular/router'; import {WorkbenchRouter} from '../routing/workbench-router.service'; -import {toEqualWorkbenchLayoutCustomMatcher} from '../testing/jasmine/matcher/to-equal-workbench-layout.matcher'; +import {MPart, MTreeNode, toEqualWorkbenchLayoutCustomMatcher} from '../testing/jasmine/matcher/to-equal-workbench-layout.matcher'; import {toBeRegisteredCustomMatcher} from '../testing/jasmine/matcher/to-be-registered.matcher'; import {WorkbenchLayoutComponent} from './workbench-layout.component'; import {Component} from '@angular/core'; import {expect} from '../testing/jasmine/matcher/custom-matchers.definition'; import {ViewDragService} from '../view-dnd/view-drag.service'; -import {WorkbenchModule} from '../workbench.module'; import {By} from '@angular/platform-browser'; import {MAIN_AREA} from './workbench-layout'; import {toHaveTransientStateCustomMatcher} from '../testing/jasmine/matcher/to-have-transient-state.matcher'; import {enterTransientViewState, TestComponent, withComponentContent, withTransientStateInputElement} from '../testing/test.component'; -import {styleFixture, waitForInitialWorkbenchLayout, waitUntilStable} from '../testing/testing.util'; -import {WorkbenchTestingModule} from '../testing/workbench-testing.module'; -import {RouterTestingModule} from '@angular/router/testing'; -import {MPart, MTreeNode} from './workbench-layout.model'; +import {segments, styleFixture, waitForInitialWorkbenchLayout, waitUntilStable} from '../testing/testing.util'; import {WorkbenchPartRegistry} from '../part/workbench-part.registry'; import {WORKBENCH_ID} from '../workbench-id'; +import {provideWorkbenchForTest} from '../testing/workbench.provider'; +import {WorkbenchComponent} from '../workbench.component'; describe('WorkbenchLayout', () => { @@ -43,11 +41,11 @@ describe('WorkbenchLayout', () => { */ it('should support navigating via Angular router', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest(), - RouterTestingModule.withRoutes([ - {path: 'view', component: TestComponent}, - {path: 'outlet', component: TestComponent, outlet: 'outlet', providers: [withComponentContent('routed content')]}, + providers: [ + provideWorkbenchForTest(), + provideRouter([ + {path: 'path/to/view', component: TestComponent}, + {path: 'path/to/outlet', component: TestComponent, outlet: 'outlet', providers: [withComponentContent('routed content')]}, ]), ], }); @@ -55,22 +53,13 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Create initial workbench layout. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => { - return { - layout: layout - .addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .2}) - .addPart('right', {relativeTo: 'main', align: 'right', ratio: .5}) - .addView('view.1', {partId: 'left', activateView: true}) - .addView('view.2', {partId: 'main', activateView: true}) - .addView('view.3', {partId: 'right', activateView: true}) - , - viewOutlets: { - 'view.1': ['view'], - 'view.2': ['view'], - 'view.3': ['view'], - }, - }; - }); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout + .addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .2}) + .addPart('right', {relativeTo: 'main', align: 'right', ratio: .5}) + .addView('view.1', {partId: 'left', activateView: true}) + .addView('view.2', {partId: 'main', activateView: true}) + .addView('view.3', {partId: 'right', activateView: true}), + ); await waitUntilStable(); // Assert initial workbench layout @@ -94,7 +83,7 @@ describe('WorkbenchLayout', () => { }); // Navigate using the Angular router. - await TestBed.inject(Router).navigate([{outlets: {outlet: ['outlet']}}]); + await TestBed.inject(Router).navigate([{outlets: {outlet: ['path', 'to', 'outlet']}}]); await waitUntilStable(); // Expect the layout not to be discarded. @@ -118,7 +107,7 @@ describe('WorkbenchLayout', () => { }); // Navigate using the Workbench router. - await TestBed.inject(WorkbenchRouter).navigate(['view'], {target: 'blank'}); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {target: 'blank'}); await waitUntilStable(); // Expect the layout to be changed. @@ -144,10 +133,10 @@ describe('WorkbenchLayout', () => { it('allows moving a view in the tabbar', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view', component: TestComponent}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view', component: TestComponent}, ]), ], }); @@ -155,10 +144,10 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // GIVEN four views (view.1, view.2, view.3, view.4). - await TestBed.inject(WorkbenchRouter).navigate(['view'], {target: 'blank'}); - await TestBed.inject(WorkbenchRouter).navigate(['view'], {target: 'blank'}); - await TestBed.inject(WorkbenchRouter).navigate(['view'], {target: 'blank'}); - await TestBed.inject(WorkbenchRouter).navigate(['view'], {target: 'blank'}); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {target: 'blank'}); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {target: 'blank'}); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {target: 'blank'}); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {target: 'blank'}); // WHEN moving view.3 to position 0 TestBed.inject(ViewDragService).dispatchViewMoveEvent({ @@ -166,7 +155,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -186,7 +175,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -206,7 +195,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -226,7 +215,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -246,7 +235,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -263,11 +252,11 @@ describe('WorkbenchLayout', () => { it('allows to move a view to a new part in the east', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -275,7 +264,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -288,7 +277,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -308,7 +297,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -337,11 +326,11 @@ describe('WorkbenchLayout', () => { it('allows to move a view to a new part in the west', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -349,7 +338,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -362,7 +351,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -382,7 +371,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -411,11 +400,11 @@ describe('WorkbenchLayout', () => { it('allows to move a view to a new part in the north', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -423,7 +412,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -436,7 +425,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -456,7 +445,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -485,11 +474,11 @@ describe('WorkbenchLayout', () => { it('allows to move a view to a new part in the south', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -497,7 +486,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -510,7 +499,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -530,7 +519,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -559,11 +548,11 @@ describe('WorkbenchLayout', () => { it('disallows to move a view to a new part in the center', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -571,7 +560,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -584,7 +573,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -604,7 +593,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -626,12 +615,12 @@ describe('WorkbenchLayout', () => { it('allows to move views to another part', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-3', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/3', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -639,7 +628,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -652,7 +641,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -667,7 +656,7 @@ describe('WorkbenchLayout', () => { expect('view.2').toHaveTransientState('B'); // Add view 3 - await TestBed.inject(WorkbenchRouter).navigate(['view-3']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/3']); await waitUntilStable(); enterTransientViewState(fixture, 'view.3', 'C'); @@ -689,7 +678,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view-3', {})], + viewUrlSegments: segments(['path/to/view/3']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -724,7 +713,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -756,7 +745,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.1', - viewUrlSegments: [new UrlSegment('view-1', {})], + viewUrlSegments: segments(['path/to/view/1']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -781,11 +770,11 @@ describe('WorkbenchLayout', () => { it('allows to move the last view of a part to a new part in the east', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -793,7 +782,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -806,7 +795,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -826,7 +815,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -859,7 +848,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST-1', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -888,11 +877,11 @@ describe('WorkbenchLayout', () => { it('allows to move the last view of a part to a new part in the west', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -900,7 +889,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -913,7 +902,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -933,7 +922,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -966,7 +955,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -995,11 +984,11 @@ describe('WorkbenchLayout', () => { it('allows to move the last view of a part to a new part in the north', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -1007,7 +996,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -1020,7 +1009,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -1040,7 +1029,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1072,7 +1061,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1101,11 +1090,11 @@ describe('WorkbenchLayout', () => { it('allows to move the last view of a part to a new part in the south', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -1113,7 +1102,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -1126,7 +1115,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -1146,7 +1135,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1178,7 +1167,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1207,12 +1196,12 @@ describe('WorkbenchLayout', () => { it('allows to move a view around parts', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-3', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/3', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -1220,7 +1209,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -1233,7 +1222,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -1248,7 +1237,7 @@ describe('WorkbenchLayout', () => { expect('view.2').toHaveTransientState('B'); // Add view 3 - await TestBed.inject(WorkbenchRouter).navigate(['view-3']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/3']); await waitUntilStable(); enterTransientViewState(fixture, 'view.3', 'C'); @@ -1270,7 +1259,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view-3', {})], + viewUrlSegments: segments(['path/to/view/3']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1305,7 +1294,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1344,7 +1333,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'SOUTH-EAST', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1378,13 +1367,13 @@ describe('WorkbenchLayout', () => { expect('view.3').toHaveTransientState('C'); }); - it('allows to move a view to a new part in the south and back to the main part ', async () => { + it('allows to move a view to a new part in the south and back to the initial part ', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -1392,7 +1381,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -1405,7 +1394,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -1425,7 +1414,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1451,13 +1440,13 @@ describe('WorkbenchLayout', () => { expect('view.2').toBeRegistered({partId: 'SOUTH', active: true}); expect('view.2').toHaveTransientState('B'); - // Move view 2 back to the main part + // Move view 2 back to the initial part TestBed.inject(ViewDragService).dispatchViewMoveEvent({ source: { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'SOUTH', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1477,13 +1466,13 @@ describe('WorkbenchLayout', () => { expect('view.2').toHaveTransientState('B'); }); - it('allows to move a view to a new part in the east and then to the south of the main part ', async () => { + it('allows to move a view to a new part in the east and then to the south of the initial part ', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -1491,7 +1480,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -1504,7 +1493,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -1524,7 +1513,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1556,7 +1545,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1583,13 +1572,13 @@ describe('WorkbenchLayout', () => { expect('view.2').toHaveTransientState('B'); }); - it('allows to move a view to a new part in the west and then to the south of the main part ', async () => { + it('allows to move a view to a new part in the west and then to the south of the initial part ', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -1597,7 +1586,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -1610,7 +1599,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -1630,7 +1619,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1662,7 +1651,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'WEST', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1691,11 +1680,11 @@ describe('WorkbenchLayout', () => { it('should open the same view multiple times', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -1703,7 +1692,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -1717,7 +1706,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -1732,7 +1721,7 @@ describe('WorkbenchLayout', () => { expect('view.2').toHaveTransientState('B'); // Add view 2 again - await TestBed.inject(WorkbenchRouter).navigate(['view-2'], {blankPartId: 'main'}); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2'], {partId: 'main'}); await waitUntilStable(); expect(fixture).toEqualWorkbenchLayout({ @@ -1748,11 +1737,11 @@ describe('WorkbenchLayout', () => { it('should open the same view multiple times', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -1760,7 +1749,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -1773,7 +1762,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -1788,7 +1777,7 @@ describe('WorkbenchLayout', () => { expect('view.2').toHaveTransientState('B'); // Add view 2 again - await TestBed.inject(WorkbenchRouter).navigate(['view-2'], {blankPartId: 'main', target: 'blank'}); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2'], {partId: 'main', target: 'blank'}); await waitUntilStable(); enterTransientViewState(fixture, 'view.3', 'C'); @@ -1807,13 +1796,13 @@ describe('WorkbenchLayout', () => { it('should open views to the right and to the left, and then close them', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view-1', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-2', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-3', component: TestComponent, providers: [withTransientStateInputElement()]}, - {path: 'view-4', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view/1', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/2', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/3', component: TestComponent, providers: [withTransientStateInputElement()]}, + {path: 'path/to/view/4', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -1821,7 +1810,7 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Add view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1']); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); @@ -1834,7 +1823,7 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); // Add view 2 - await TestBed.inject(WorkbenchRouter).navigate(['view-2']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/2']); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -1854,7 +1843,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1881,7 +1870,7 @@ describe('WorkbenchLayout', () => { expect('view.2').toHaveTransientState('B'); // Add view 3 to part EAST-1 - await TestBed.inject(WorkbenchRouter).navigate(['view-3']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/3']); await waitUntilStable(); enterTransientViewState(fixture, 'view.3', 'C'); @@ -1908,7 +1897,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST-1', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view-3', {})], + viewUrlSegments: segments(['path/to/view/3']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -1942,7 +1931,7 @@ describe('WorkbenchLayout', () => { expect('view.3').toHaveTransientState('C'); // Add view 4 to part EAST-2 - await TestBed.inject(WorkbenchRouter).navigate(['view-4']); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/4']); await waitUntilStable(); enterTransientViewState(fixture, 'view.4', 'D'); @@ -1976,7 +1965,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST-2', viewId: 'view.4', - viewUrlSegments: [new UrlSegment('view-4', {})], + viewUrlSegments: segments(['path/to/view/4']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2022,7 +2011,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST-2', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view-3', {})], + viewUrlSegments: segments(['path/to/view/3']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2068,7 +2057,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST-1', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view-2', {})], + viewUrlSegments: segments(['path/to/view/2']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2110,7 +2099,7 @@ describe('WorkbenchLayout', () => { expect('view.4').toHaveTransientState('D'); // Close view 1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1'], {close: true}); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/1'], {close: true}); await waitUntilStable(); expect(fixture).toEqualWorkbenchLayout({ @@ -2137,7 +2126,7 @@ describe('WorkbenchLayout', () => { expect('view.4').toHaveTransientState('D'); // Close view 3 - await TestBed.inject(WorkbenchRouter).navigate(['view-3'], {close: true}); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/3'], {close: true}); await waitUntilStable(); expect(fixture).toEqualWorkbenchLayout({ @@ -2158,7 +2147,7 @@ describe('WorkbenchLayout', () => { expect('view.4').toHaveTransientState('D'); // Close view 4 - await TestBed.inject(WorkbenchRouter).navigate(['view-4'], {close: true}); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view/4'], {close: true}); await waitUntilStable(); expect(fixture).toEqualWorkbenchLayout({ @@ -2175,10 +2164,10 @@ describe('WorkbenchLayout', () => { it('should detach views before being re-parented in the DOM (1)', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -2186,32 +2175,27 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Create initial workbench layout. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => { - return { - layout: layout - .addView('view.1', {partId: 'main'}) - .addView('view.2', {partId: 'main'}) - .addView('view.3', {partId: 'main'}) - .activateView('view.3'), - viewOutlets: { - 'view.1': ['view'], - 'view.2': ['view'], - 'view.3': ['view'], - }, - }; - }); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout + .addView('view.1', {partId: 'main'}) + .addView('view.2', {partId: 'main'}) + .addView('view.3', {partId: 'main'}) + .navigateView('view.1', ['path/to/view']) + .navigateView('view.2', ['path/to/view']) + .navigateView('view.3', ['path/to/view']) + .activateView('view.3'), + ); await waitUntilStable(); // Enter transient states. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.1')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.1')); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.2')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.2')); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.3')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.3')); await waitUntilStable(); enterTransientViewState(fixture, 'view.3', 'C'); @@ -2221,7 +2205,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2252,7 +2236,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2285,10 +2269,10 @@ describe('WorkbenchLayout', () => { it('should detach views before being re-parented in the DOM (2)', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -2296,32 +2280,27 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Create initial workbench layout. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => { - return { - layout: layout - .addView('view.1', {partId: 'main'}) - .addView('view.2', {partId: 'main'}) - .addView('view.3', {partId: 'main'}) - .activateView('view.3'), - viewOutlets: { - 'view.1': ['view'], - 'view.2': ['view'], - 'view.3': ['view'], - }, - }; - }); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout + .addView('view.1', {partId: 'main'}) + .addView('view.2', {partId: 'main'}) + .addView('view.3', {partId: 'main'}) + .navigateView('view.1', ['path/to/view']) + .navigateView('view.2', ['path/to/view']) + .navigateView('view.3', ['path/to/view']) + .activateView('view.3'), + ); await waitUntilStable(); // Enter transient states. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.1')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.1')); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.2')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.2')); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.3')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.3')); await waitUntilStable(); enterTransientViewState(fixture, 'view.3', 'C'); @@ -2331,7 +2310,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2356,13 +2335,13 @@ describe('WorkbenchLayout', () => { expect('view.2').toHaveTransientState('B'); expect('view.3').toHaveTransientState('C'); - // Move view 2 to a new part in the west of the main part + // Move view 2 to a new part in the west of the initial part TestBed.inject(ViewDragService).dispatchViewMoveEvent({ source: { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2390,10 +2369,10 @@ describe('WorkbenchLayout', () => { it('should detach views before being re-parented in the DOM (3)', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -2401,38 +2380,33 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Create initial workbench layout. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => { - return { - layout: layout - .addView('view.1', {partId: 'main'}) - .addView('view.2', {partId: 'main'}) - .addView('view.3', {partId: 'main'}) - .addView('view.4', {partId: 'main'}) - .activateView('view.4'), - viewOutlets: { - 'view.1': ['view'], - 'view.2': ['view'], - 'view.3': ['view'], - 'view.4': ['view'], - }, - }; - }); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout + .addView('view.1', {partId: 'main'}) + .addView('view.2', {partId: 'main'}) + .addView('view.3', {partId: 'main'}) + .addView('view.4', {partId: 'main'}) + .navigateView('view.1', ['path/to/view']) + .navigateView('view.2', ['path/to/view']) + .navigateView('view.3', ['path/to/view']) + .navigateView('view.4', ['path/to/view']) + .activateView('view.4'), + ); await waitUntilStable(); // Enter transient states. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.1')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.1')); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.2')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.2')); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.3')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.3')); await waitUntilStable(); enterTransientViewState(fixture, 'view.3', 'C'); - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.4')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.4')); await waitUntilStable(); enterTransientViewState(fixture, 'view.4', 'D'); @@ -2442,7 +2416,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2474,7 +2448,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2511,7 +2485,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.4', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2553,7 +2527,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST-2', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2595,7 +2569,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST-1', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2685,10 +2659,10 @@ describe('WorkbenchLayout', () => { it('should detach views before being re-parented in the DOM (4)', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -2696,32 +2670,27 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Create initial workbench layout. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => { - return { - layout: layout - .addView('view.1', {partId: 'main'}) - .addView('view.2', {partId: 'main'}) - .addView('view.3', {partId: 'main'}) - .activateView('view.3'), - viewOutlets: { - 'view.1': ['view'], - 'view.2': ['view'], - 'view.3': ['view'], - }, - }; - }); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout + .addView('view.1', {partId: 'main'}) + .addView('view.2', {partId: 'main'}) + .addView('view.3', {partId: 'main'}) + .navigateView('view.1', ['path/to/view']) + .navigateView('view.2', ['path/to/view']) + .navigateView('view.3', ['path/to/view']) + .activateView('view.3'), + ); await waitUntilStable(); // Enter transient states. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.1')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.1')); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.2')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.2')); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.3')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.3')); await waitUntilStable(); enterTransientViewState(fixture, 'view.3', 'C'); @@ -2731,7 +2700,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2762,7 +2731,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2798,7 +2767,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2831,10 +2800,10 @@ describe('WorkbenchLayout', () => { it('should detach views before being re-parented in the DOM (5)', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -2842,32 +2811,27 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Create initial workbench layout. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => { - return { - layout: layout - .addView('view.1', {partId: 'main'}) - .addView('view.2', {partId: 'main'}) - .addView('view.3', {partId: 'main'}) - .activateView('view.3'), - viewOutlets: { - 'view.1': ['view'], - 'view.2': ['view'], - 'view.3': ['view'], - }, - }; - }); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout + .addView('view.1', {partId: 'main'}) + .addView('view.2', {partId: 'main'}) + .addView('view.3', {partId: 'main'}) + .navigateView('view.1', ['path/to/view']) + .navigateView('view.2', ['path/to/view']) + .navigateView('view.3', ['path/to/view']) + .activateView('view.3'), + ); await waitUntilStable(); // Enter transient states. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.1')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.1')); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.2')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.2')); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.3')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.3')); await waitUntilStable(); enterTransientViewState(fixture, 'view.3', 'C'); @@ -2877,7 +2841,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2908,7 +2872,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2944,7 +2908,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST', viewId: 'view.3', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -2977,10 +2941,10 @@ describe('WorkbenchLayout', () => { it('should detach views before being re-parented in the DOM (6)', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({startup: {launcher: 'APP_INITIALIZER'}}), - RouterTestingModule.withRoutes([ - {path: 'view', component: TestComponent, providers: [withTransientStateInputElement()]}, + providers: [ + provideWorkbenchForTest({startup: {launcher: 'APP_INITIALIZER'}}), + provideRouter([ + {path: 'path/to/view', component: TestComponent, providers: [withTransientStateInputElement()]}, ]), ], }); @@ -2988,26 +2952,21 @@ describe('WorkbenchLayout', () => { await waitForInitialWorkbenchLayout(); // Create initial workbench layout. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => { - return { - layout: layout - .addView('view.1', {partId: 'main'}) - .addView('view.2', {partId: 'main'}) - .activateView('view.2'), - viewOutlets: { - 'view.1': ['view'], - 'view.2': ['view'], - }, - }; - }); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout + .addView('view.1', {partId: 'main'}) + .addView('view.2', {partId: 'main'}) + .navigateView('view.1', ['path/to/view']) + .navigateView('view.2', ['path/to/view']) + .activateView('view.2'), + ); await waitUntilStable(); // Enter transient states. - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.1')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.1')); await waitUntilStable(); enterTransientViewState(fixture, 'view.1', 'A'); - await TestBed.inject(WorkbenchRouter).ɵnavigate(layout => layout.activateView('view.2')); + await TestBed.inject(WorkbenchRouter).navigate(layout => layout.activateView('view.2')); await waitUntilStable(); enterTransientViewState(fixture, 'view.2', 'B'); @@ -3017,7 +2976,7 @@ describe('WorkbenchLayout', () => { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'main', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -3041,13 +3000,13 @@ describe('WorkbenchLayout', () => { expect('view.1').toHaveTransientState('A'); expect('view.2').toHaveTransientState('B'); - // Move view 2 to a new part in the west of main part + // Move view 2 to a new part in the west of initial part TestBed.inject(ViewDragService).dispatchViewMoveEvent({ source: { workbenchId: TestBed.inject(WORKBENCH_ID), partId: 'EAST', viewId: 'view.2', - viewUrlSegments: [new UrlSegment('view', {})], + viewUrlSegments: segments(['path/to/view']), }, target: { workbenchId: TestBed.inject(WORKBENCH_ID), @@ -3106,7 +3065,7 @@ describe('WorkbenchLayout', () => { standalone: true, imports: [ RouterOutlet, - WorkbenchModule, + WorkbenchComponent, ], }) class RouterOutletPlusWorkbenchTestFixtureComponent { diff --git a/projects/scion/workbench/src/lib/layout/workbench-layout.component.ts b/projects/scion/workbench/src/lib/layout/workbench-layout.component.ts index 9a3616492..797d92be7 100644 --- a/projects/scion/workbench/src/lib/layout/workbench-layout.component.ts +++ b/projects/scion/workbench/src/lib/layout/workbench-layout.component.ts @@ -85,7 +85,9 @@ export class WorkbenchLayoutComponent { workbenchId: event.dragData.workbenchId, partId: event.dragData.partId, viewId: event.dragData.viewId, + alternativeViewId: event.dragData.alternativeViewId, viewUrlSegments: event.dragData.viewUrlSegments, + navigationHint: event.dragData.navigationHint, classList: event.dragData.classList, }, target: GridDropTargets.resolve({ diff --git a/projects/scion/workbench/src/lib/layout/workbench-layout.factory.ts b/projects/scion/workbench/src/lib/layout/workbench-layout.factory.ts index 425629d1d..405bbbf0a 100644 --- a/projects/scion/workbench/src/lib/layout/workbench-layout.factory.ts +++ b/projects/scion/workbench/src/lib/layout/workbench-layout.factory.ts @@ -23,7 +23,7 @@ export abstract class WorkbenchLayoutFactory { * * @param id - The id of the part. Use {@link MAIN_AREA} to add the main area. * @param options - Controls how to add the part to the layout. - * @property activate - Controls whether to activate the part. If not set, defaults to `false`. + * @param options.activate - Controls whether to activate the part. If not set, defaults to `false`. * @return layout with the part added. */ public abstract addPart(id: string | MAIN_AREA, options?: {activate?: boolean}): WorkbenchLayout; diff --git a/projects/scion/workbench/src/lib/layout/workbench-layout.model.ts b/projects/scion/workbench/src/lib/layout/workbench-layout.model.ts index fb6e63694..0a716c1ed 100644 --- a/projects/scion/workbench/src/lib/layout/workbench-layout.model.ts +++ b/projects/scion/workbench/src/lib/layout/workbench-layout.model.ts @@ -10,11 +10,13 @@ import {assertType} from '../common/asserts.util'; import {Defined} from '@scion/toolkit/util'; +import {ViewId} from '../view/workbench-view.model'; +import {UUID} from '../common/uuid.util'; /** * Represents the arrangement of parts as grid. * - * The M-prefix indicates that {@link MPartGrid} is a layout model object that will be serialized into the URL. + * The M-prefix indicates this object is a model object that is serialized and stored, requiring migration on breaking change. */ export interface MPartGrid { root: MTreeNode | MPart; @@ -28,7 +30,7 @@ export interface ɵMPartGrid extends MPartGrid { /** * Indicates if this grid was migrated from an older version. */ - migrated: boolean; + migrated?: true; } /** @@ -37,7 +39,7 @@ export interface ɵMPartGrid extends MPartGrid { * A node contains two children, which are either a {@link MPart} or a {@link MTreeNode}, respectively. * The ratio together with the direction describes how to arrange the two children. * - * The M-prefix indicates that {@link MTreeNode} is a layout model object that will be serialized into the URL. + * The M-prefix indicates this object is a model object that is serialized and stored, requiring migration on breaking change. */ export class MTreeNode { @@ -52,7 +54,7 @@ export class MTreeNode { public direction!: 'column' | 'row'; public parent?: MTreeNode; - constructor(treeNode: Partial>) { + constructor(treeNode: Omit) { treeNode.parent && assertType(treeNode.parent, {toBeOneOf: MTreeNode}); // assert not to be an object literal assertType(treeNode.child1, {toBeOneOf: [MTreeNode, MPart]}); // assert not to be an object literal assertType(treeNode.child2, {toBeOneOf: [MTreeNode, MPart]}); // assert not to be an object literal @@ -63,7 +65,7 @@ export class MTreeNode { * Tests if the given object is a {@link MTreeNode}. */ public static isMTreeNode(object: any): object is MTreeNode { - return (object as MTreeNode).type === 'MTreeNode'; + return object && (object as MTreeNode).type === 'MTreeNode'; } } @@ -72,7 +74,7 @@ export class MTreeNode { * * A part is a container for views. * - * The M-prefix indicates that {@link MPart} is a layout model object that will be serialized into the URL. + * The M-prefix indicates this object is a model object that is serialized and stored, requiring migration on breaking change. */ export class MPart { @@ -82,11 +84,11 @@ export class MPart { public readonly type = 'MPart'; public readonly id!: string; public parent?: MTreeNode; - public views: MView[] = []; - public activeViewId?: string; + public views!: MView[]; + public activeViewId?: ViewId; public structural!: boolean; - constructor(part: Partial>) { + constructor(part: Omit) { Defined.orElseThrow(part.id, () => Error('MPart requires an id')); part.parent && assertType(part.parent, {toBeOneOf: MTreeNode}); // assert not to be an object literal Object.assign(this, part); @@ -96,15 +98,23 @@ export class MPart { * Tests if the given object is a {@link MPart}. */ public static isMPart(object: any): object is MPart { - return (object as MPart).type === 'MPart'; + return object && (object as MPart).type === 'MPart'; } } /** * Represents a view contained in a {@link MPart}. * - * The M-prefix indicates that {@link MView} is a layout model object that will be serialized into the URL. + * The M-prefix indicates this object is a model object that is serialized and stored, requiring migration on breaking change. */ export interface MView { - readonly id: string; + id: ViewId; + alternativeId?: string; + uid: UUID; + cssClass?: string[]; + markedForRemoval?: true; + navigation?: { + hint?: string; + cssClass?: string[]; + }; } diff --git a/projects/scion/workbench/src/lib/layout/workbench-layout.spec.ts b/projects/scion/workbench/src/lib/layout/workbench-layout.spec.ts index 0b1109aa4..ae6d28453 100644 --- a/projects/scion/workbench/src/lib/layout/workbench-layout.spec.ts +++ b/projects/scion/workbench/src/lib/layout/workbench-layout.spec.ts @@ -8,14 +8,16 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {MPart, MTreeNode} from './workbench-layout.model'; import {MAIN_AREA_INITIAL_PART_ID, PartActivationInstantProvider, ViewActivationInstantProvider, ɵWorkbenchLayout} from './ɵworkbench-layout'; import {MAIN_AREA, WorkbenchLayout} from './workbench-layout'; -import {toEqualWorkbenchLayoutCustomMatcher} from '../testing/jasmine/matcher/to-equal-workbench-layout.matcher'; +import {MPart, MTreeNode, toEqualWorkbenchLayoutCustomMatcher} from '../testing/jasmine/matcher/to-equal-workbench-layout.matcher'; import {expect} from '../testing/jasmine/matcher/custom-matchers.definition'; import {TestBed} from '@angular/core/testing'; import {WorkbenchLayoutFactory} from './workbench-layout.factory'; import {ɵWorkbenchLayoutFactory} from './ɵworkbench-layout.factory'; +import {UrlSegmentMatcher} from '../routing/url-segment-matcher'; +import {anything, segments} from '../testing/testing.util'; +import {MPart as _MPart, MTreeNode as _MTreeNode, MView} from './workbench-layout.model'; describe('WorkbenchLayout', () => { @@ -31,35 +33,35 @@ describe('WorkbenchLayout', () => { // add view without specifying position expect(layout .addView('view.4', {partId: 'A'}) - .part({by: {partId: 'A'}}) + .part({partId: 'A'}) .views.map(view => view.id), ).toEqual(['view.1', 'view.2', 'view.3', 'view.4']); // add view at the start expect(layout .addView('view.4', {partId: 'A', position: 'start'}) - .part({by: {partId: 'A'}}) + .part({partId: 'A'}) .views.map(view => view.id), ).toEqual(['view.4', 'view.1', 'view.2', 'view.3']); // add view at the end expect(layout .addView('view.4', {partId: 'A', position: 'end'}) - .part({by: {partId: 'A'}}) + .part({partId: 'A'}) .views.map(view => view.id), ).toEqual(['view.1', 'view.2', 'view.3', 'view.4']); // add view before the active view expect(layout .addView('view.4', {partId: 'A', position: 'before-active-view'}) - .part({by: {partId: 'A'}}) + .part({partId: 'A'}) .views.map(view => view.id), ).toEqual(['view.1', 'view.4', 'view.2', 'view.3']); // add view after the active view expect(layout .addView('view.4', {partId: 'A', position: 'after-active-view'}) - .part({by: {partId: 'A'}}) + .part({partId: 'A'}) .views.map(view => view.id), ).toEqual(['view.1', 'view.2', 'view.4', 'view.3']); }); @@ -250,7 +252,7 @@ describe('WorkbenchLayout', () => { root: new MPart({id: 'B'}), }, }); - expect(workbenchLayout.part({by: {partId: 'B'}}).parent).toBeUndefined(); + expect(workbenchLayout.part({partId: 'B'}).parent).toBeUndefined(); }); /** @@ -683,32 +685,32 @@ describe('WorkbenchLayout', () => { const workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory).create({workbenchGrid: serializedLayout.workbenchGrid, mainAreaGrid: serializedLayout.mainAreaGrid}); // verify the main area root node. - const rootNode = workbenchLayout.mainAreaGrid!.root as MTreeNode; - expect(rootNode.constructor).toEqual(MTreeNode); + const rootNode = workbenchLayout.mainAreaGrid!.root as _MTreeNode; + expect(rootNode).toBeInstanceOf(_MTreeNode); expect(rootNode.parent).toBeUndefined(); // verify the left sashbox - const bcNode = rootNode.child1 as MTreeNode; - expect(bcNode.constructor).toEqual(MTreeNode); + const bcNode = rootNode.child1 as _MTreeNode; + expect(bcNode).toBeInstanceOf(_MTreeNode); expect(bcNode.parent).toBe(rootNode); // verify the 'B' part - const topLeftPart = bcNode.child1 as MPart; - expect(topLeftPart.constructor).toEqual(MPart); + const topLeftPart = bcNode.child1 as _MPart; + expect(topLeftPart).toBeInstanceOf(_MPart); expect(topLeftPart.parent).toBe(bcNode); expect(topLeftPart.id).toEqual('B'); // verify the 'C' part - const bottomLeftPart = bcNode.child2 as MPart; - expect(bottomLeftPart.constructor).toEqual(MPart); + const bottomLeftPart = bcNode.child2 as _MPart; + expect(bottomLeftPart).toBeInstanceOf(_MPart); expect(bottomLeftPart.parent).toBe(bcNode); expect(bottomLeftPart.id).toEqual('C'); - // verify the main part - const mainPart = rootNode.child2 as MPart; - expect(mainPart.constructor).toEqual(MPart); - expect(mainPart.parent).toBe(rootNode); - expect(mainPart.id).toEqual('A'); + // verify the initial part + const initialPart = rootNode.child2 as _MPart; + expect(initialPart).toBeInstanceOf(_MPart); + expect(initialPart.parent).toBe(rootNode); + expect(initialPart.id).toEqual('A'); }); /** @@ -761,9 +763,9 @@ describe('WorkbenchLayout', () => { .addView('view.3', {partId: 'A'}) .addView('view.4', {partId: 'C'}); - expect(workbenchLayout.part({by: {partId: 'B'}}).views).toEqual([{id: 'view.1'}, {id: 'view.2'}]); - expect(workbenchLayout.part({by: {partId: 'A'}}).views).toEqual([{id: 'view.3'}]); - expect(workbenchLayout.part({by: {partId: 'C'}}).views).toEqual([{id: 'view.4'}]); + expect(workbenchLayout.part({partId: 'B'}).views.map(view => view.id)).toEqual(['view.1', 'view.2']); + expect(workbenchLayout.part({partId: 'A'}).views.map(view => view.id)).toEqual(['view.3']); + expect(workbenchLayout.part({partId: 'C'}).views.map(view => view.id)).toEqual(['view.4']); }); it('should remove non-structural part when removing its last view', () => { @@ -774,10 +776,10 @@ describe('WorkbenchLayout', () => { .addPart('left', {relativeTo: 'main', align: 'left'}, {structural: false}) .addView('view.1', {partId: 'left'}) .addView('view.2', {partId: 'left'}) - .removeView('view.1') - .removeView('view.2'); + .removeView('view.1', {force: true}) + .removeView('view.2', {force: true}); - expect(() => workbenchLayout.part({by: {partId: 'left'}})).toThrowError(/NullPartError/); + expect(() => workbenchLayout.part({partId: 'left'})).toThrowError(/NullPartError/); expect(workbenchLayout.hasPart('left')).toBeFalse(); }); @@ -792,7 +794,7 @@ describe('WorkbenchLayout', () => { .removeView('view.1') .removeView('view.2'); - expect(workbenchLayout.part({by: {partId: 'left'}})).toEqual(jasmine.objectContaining({id: 'left'})); + expect(workbenchLayout.part({partId: 'left'})).toEqual(jasmine.objectContaining({id: 'left'})); }); /** @@ -817,7 +819,7 @@ describe('WorkbenchLayout', () => { .addView('view.3', {partId: 'A'}) .addView('view.4', {partId: 'A'}) .moveView('view.1', 'A', {position: 2}) - .part({by: {partId: 'A'}}) + .part({partId: 'A'}) .views.map(view => view.id), ).toEqual(['view.2', 'view.1', 'view.3', 'view.4']); @@ -831,7 +833,7 @@ describe('WorkbenchLayout', () => { .addView('view.3', {partId: 'A'}) .addView('view.4', {partId: 'A'}) .moveView('view.4', 'A', {position: 2}) - .part({by: {partId: 'A'}}) + .part({partId: 'A'}) .views.map(view => view.id), ).toEqual(['view.1', 'view.2', 'view.4', 'view.3']); @@ -845,7 +847,7 @@ describe('WorkbenchLayout', () => { .addView('view.3', {partId: 'A'}) .addView('view.4', {partId: 'A'}) .moveView('view.2', 'A', {position: 'end'}) - .part({by: {partId: 'A'}}) + .part({partId: 'A'}) .views.map(view => view.id), ).toEqual(['view.1', 'view.3', 'view.4', 'view.2']); @@ -859,7 +861,7 @@ describe('WorkbenchLayout', () => { .addView('view.3', {partId: 'A'}) .addView('view.4', {partId: 'A'}) .moveView('view.3', 'A', {position: 'start'}) - .part({by: {partId: 'A'}}) + .part({partId: 'A'}) .views.map(view => view.id), ).toEqual(['view.3', 'view.1', 'view.2', 'view.4']); @@ -873,7 +875,7 @@ describe('WorkbenchLayout', () => { .addView('view.3', {partId: 'A', activateView: true}) .addView('view.4', {partId: 'A'}) .moveView('view.1', 'A', {position: 'before-active-view'}) - .part({by: {partId: 'A'}}) + .part({partId: 'A'}) .views.map(view => view.id), ).toEqual(['view.2', 'view.1', 'view.3', 'view.4']); @@ -889,7 +891,7 @@ describe('WorkbenchLayout', () => { .addView('view.5', {partId: 'C', activateView: true}) .addView('view.6', {partId: 'C'}) .moveView('view.2', 'C', {position: 'before-active-view'}) - .part({by: {partId: 'C'}}) + .part({partId: 'C'}) .views.map(view => view.id), ).toEqual(['view.4', 'view.2', 'view.5', 'view.6']); @@ -903,7 +905,7 @@ describe('WorkbenchLayout', () => { .addView('view.3', {partId: 'A', activateView: true}) .addView('view.4', {partId: 'A'}) .moveView('view.1', 'A', {position: 'after-active-view'}) - .part({by: {partId: 'A'}}) + .part({partId: 'A'}) .views.map(view => view.id), ).toEqual(['view.2', 'view.3', 'view.1', 'view.4']); @@ -919,7 +921,7 @@ describe('WorkbenchLayout', () => { .addView('view.5', {partId: 'C', activateView: true}) .addView('view.6', {partId: 'C'}) .moveView('view.2', 'C', {position: 'after-active-view'}) - .part({by: {partId: 'C'}}) + .part({partId: 'C'}) .views.map(view => view.id), ).toEqual(['view.4', 'view.5', 'view.2', 'view.6']); @@ -933,7 +935,7 @@ describe('WorkbenchLayout', () => { .addView('view.3', {partId: 'A'}) .addView('view.4', {partId: 'A'}) .moveView('view.2', 'A') - .part({by: {partId: 'A'}}) + .part({partId: 'A'}) .views.map(view => view.id), ).toEqual(['view.1', 'view.2', 'view.3', 'view.4']); @@ -949,7 +951,7 @@ describe('WorkbenchLayout', () => { .addView('view.5', {partId: 'C', activateView: true}) .addView('view.6', {partId: 'C'}) .moveView('view.2', 'C') - .part({by: {partId: 'C'}}) + .part({partId: 'C'}) .views.map(view => view.id), ).toEqual(['view.4', 'view.5', 'view.6', 'view.2']); }); @@ -978,9 +980,113 @@ describe('WorkbenchLayout', () => { .moveView('view.2', 'C') .moveView('view.3', 'C'); - expect(workbenchLayout.part({by: {partId: 'B'}}).views).toEqual([{id: 'view.1'}]); - expect(workbenchLayout.part({by: {partId: 'C'}}).views).toEqual([{id: 'view.2'}, {id: 'view.3'}]); - expect(workbenchLayout.part({by: {partId: 'A'}}).views).toEqual([{id: 'view.4'}]); + expect(workbenchLayout.part({partId: 'B'}).views.map(view => view.id)).toEqual(['view.1']); + expect(workbenchLayout.part({partId: 'C'}).views.map(view => view.id)).toEqual(['view.2', 'view.3']); + expect(workbenchLayout.part({partId: 'A'}).views.map(view => view.id)).toEqual(['view.4']); + }); + + it('should retain navigation when moving view to another part', () => { + const workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart('left') + .addPart('right', {relativeTo: 'left', align: 'right'}) + .addView('view.1', {partId: 'left', cssClass: 'class-view'}) + .addView('view.2', {partId: 'left'}) + .navigateView('view.1', ['path/to/view'], {cssClass: 'class-navigation'}) + .navigateView('view.2', [], {hint: 'some-hint'}) + .moveView('view.1', 'right') + .moveView('view.2', 'right'); + + expect(workbenchLayout.part({partId: 'right'}).views).toEqual(jasmine.arrayWithExactContents([ + {id: 'view.1', navigation: {cssClass: ['class-navigation']}, cssClass: ['class-view'], uid: anything()} satisfies MView, + {id: 'view.2', navigation: {hint: 'some-hint'}, uid: anything()} satisfies MView, + ])); + expect(workbenchLayout.urlSegments({viewId: 'view.1'})).toEqual(segments(['path/to/view'])); + expect(workbenchLayout.urlSegments({viewId: 'view.2'})).toEqual([]); + }); + + it('should retain state when moving view to another part', () => { + const workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart('left') + .addPart('right', {relativeTo: 'left', align: 'right'}) + .addView('view.1', {partId: 'left'}) + .navigateView('view.1', ['path/to/view'], {state: {some: 'state'}}) + .moveView('view.1', 'right'); + + expect(workbenchLayout.part({partId: 'right'}).views).toEqual([{id: 'view.1', navigation: {}, uid: anything()} satisfies MView]); + expect(workbenchLayout.viewState({viewId: 'view.1'})).toEqual({some: 'state'}); + }); + + it('should clear hint of previous navigation when navigating without hint', () => { + const workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart('part') + .addView('view.1', {partId: 'part'}) + .navigateView('view.1', [], {hint: 'some-hint'}) + .navigateView('view.1', ['path/to/view']); + + expect(workbenchLayout.view({viewId: 'view.1'})).toEqual({id: 'view.1', navigation: {}, uid: anything()} satisfies MView); + expect(workbenchLayout.urlSegments({viewId: 'view.1'})).toEqual(segments(['path/to/view'])); + }); + + it('should clear URL of previous navigation when navigating without URL', () => { + const workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart('part') + .addView('view.1', {partId: 'part'}) + .navigateView('view.1', ['path/to/view']) + .navigateView('view.1', [], {hint: 'some-hint'}); + + expect(workbenchLayout.view({viewId: 'view.1'})).toEqual({id: 'view.1', navigation: {hint: 'some-hint'}, uid: anything()} satisfies MView); + expect(workbenchLayout.urlSegments({viewId: 'view.1'})).toEqual([]); + }); + + it('should clear state of previous navigation when navigating without state', () => { + const workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart('part') + .addView('view.1', {partId: 'part'}) + .navigateView('view.1', ['path/to/view'], {state: {some: 'state'}}) + .navigateView('view.1', ['path/to/view']); + + expect(workbenchLayout.view({viewId: 'view.1'})).toEqual({id: 'view.1', navigation: {}, uid: anything()} satisfies MView); + expect(workbenchLayout.viewState({viewId: 'view.1'})).toEqual({}); + expect(workbenchLayout.urlSegments({viewId: 'view.1'})).toEqual(segments(['path/to/view'])); + }); + + it('should remove views of a part when removing a part', () => { + const workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart(MAIN_AREA) + .addPart('part', {align: 'right'}) + .addView('view.1', {partId: 'part'}) + .navigateView('view.1', ['path/to/view'], {state: {some: 'state'}}) + .removePart('part'); + + expect(workbenchLayout.view({viewId: 'view.1'}, {orElse: null})).toBeNull(); + expect(workbenchLayout.viewState({viewId: 'view.1'})).toEqual({}); + expect(workbenchLayout.urlSegments({viewId: 'view.1'})).toEqual([]); + }); + + it('should remove associated data when removing view', () => { + const workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart('part') + .addView('view.1', {partId: 'part'}) + .navigateView('view.1', ['path/to/view'], {state: {some: 'state'}}) + .removeView('view.1', {force: true}); + + expect(workbenchLayout.view({viewId: 'view.1'}, {orElse: null})).toBeNull(); + expect(workbenchLayout.viewState({viewId: 'view.1'})).toEqual({}); + expect(workbenchLayout.urlSegments({viewId: 'view.1'})).toEqual([]); + }); + + it('should also rename associated data when renaming view', () => { + const workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart('part') + .addView('view.1', {partId: 'part'}) + .navigateView('view.1', ['path/to/view'], {state: {some: 'state'}}) + .renameView('view.1', 'view.2'); + + expect(workbenchLayout.viewState({viewId: 'view.1'})).toEqual({}); + expect(workbenchLayout.urlSegments({viewId: 'view.1'})).toEqual([]); + + expect(workbenchLayout.viewState({viewId: 'view.2'})).toEqual({some: 'state'}); + expect(workbenchLayout.urlSegments({viewId: 'view.2'})).toEqual(segments(['path/to/view'])); }); it('should activate part and view when moving view to another part', () => { @@ -998,15 +1104,15 @@ describe('WorkbenchLayout', () => { .activateView('view.3'); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('left'); - expect(workbenchLayout.part({by: {partId: 'left'}}).activeViewId).toEqual('view.1'); - expect(workbenchLayout.part({by: {partId: 'right'}}).activeViewId).toEqual('view.3'); + expect(workbenchLayout.part({partId: 'left'}).activeViewId).toEqual('view.1'); + expect(workbenchLayout.part({partId: 'right'}).activeViewId).toEqual('view.3'); // Move view.1 to part right workbenchLayout = workbenchLayout.moveView('view.1', 'right', {activatePart: true, activateView: true}); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('right'); - expect(workbenchLayout.part({by: {partId: 'left'}}).activeViewId).toEqual('view.2'); - expect(workbenchLayout.part({by: {partId: 'right'}}).activeViewId).toEqual('view.1'); + expect(workbenchLayout.part({partId: 'left'}).activeViewId).toEqual('view.2'); + expect(workbenchLayout.part({partId: 'right'}).activeViewId).toEqual('view.1'); }); it('should not activate part and view when moving view to another part', () => { @@ -1024,15 +1130,15 @@ describe('WorkbenchLayout', () => { .activateView('view.3'); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('left'); - expect(workbenchLayout.part({by: {partId: 'left'}}).activeViewId).toEqual('view.1'); - expect(workbenchLayout.part({by: {partId: 'right'}}).activeViewId).toEqual('view.3'); + expect(workbenchLayout.part({partId: 'left'}).activeViewId).toEqual('view.1'); + expect(workbenchLayout.part({partId: 'right'}).activeViewId).toEqual('view.3'); // Move view.1 to part right workbenchLayout = workbenchLayout.moveView('view.1', 'right'); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('left'); - expect(workbenchLayout.part({by: {partId: 'left'}}).activeViewId).toEqual('view.2'); - expect(workbenchLayout.part({by: {partId: 'right'}}).activeViewId).toEqual('view.3'); + expect(workbenchLayout.part({partId: 'left'}).activeViewId).toEqual('view.2'); + expect(workbenchLayout.part({partId: 'right'}).activeViewId).toEqual('view.3'); }); it('should activate part and view when moving view inside the part', () => { @@ -1050,15 +1156,15 @@ describe('WorkbenchLayout', () => { .activateView('view.3'); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('right'); - expect(workbenchLayout.part({by: {partId: 'left'}}).activeViewId).toEqual('view.1'); - expect(workbenchLayout.part({by: {partId: 'right'}}).activeViewId).toEqual('view.3'); + expect(workbenchLayout.part({partId: 'left'}).activeViewId).toEqual('view.1'); + expect(workbenchLayout.part({partId: 'right'}).activeViewId).toEqual('view.3'); // Move view.1 to part right workbenchLayout = workbenchLayout.moveView('view.2', 'left', {position: 0, activatePart: true, activateView: true}); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('left'); - expect(workbenchLayout.part({by: {partId: 'left'}}).activeViewId).toEqual('view.2'); - expect(workbenchLayout.part({by: {partId: 'right'}}).activeViewId).toEqual('view.3'); + expect(workbenchLayout.part({partId: 'left'}).activeViewId).toEqual('view.2'); + expect(workbenchLayout.part({partId: 'right'}).activeViewId).toEqual('view.3'); }); it('should not activate part and view when moving view inside the part', () => { @@ -1076,15 +1182,15 @@ describe('WorkbenchLayout', () => { .activateView('view.3'); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('right'); - expect(workbenchLayout.part({by: {partId: 'left'}}).activeViewId).toEqual('view.1'); - expect(workbenchLayout.part({by: {partId: 'right'}}).activeViewId).toEqual('view.3'); + expect(workbenchLayout.part({partId: 'left'}).activeViewId).toEqual('view.1'); + expect(workbenchLayout.part({partId: 'right'}).activeViewId).toEqual('view.3'); // Move view.1 to part right workbenchLayout = workbenchLayout.moveView('view.2', 'left', {position: 0}); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('right'); - expect(workbenchLayout.part({by: {partId: 'left'}}).activeViewId).toEqual('view.1'); - expect(workbenchLayout.part({by: {partId: 'right'}}).activeViewId).toEqual('view.3'); + expect(workbenchLayout.part({partId: 'left'}).activeViewId).toEqual('view.1'); + expect(workbenchLayout.part({partId: 'right'}).activeViewId).toEqual('view.3'); }); /** @@ -1111,8 +1217,8 @@ describe('WorkbenchLayout', () => { .moveView('view.3', 'C'); expect(workbenchLayout.hasPart('B')).toBeFalse(); - expect(workbenchLayout.part({by: {partId: 'A'}}).views).toEqual([{id: 'view.1'}, {id: 'view.2'}]); - expect(workbenchLayout.part({by: {partId: 'C'}}).views).toEqual([{id: 'view.3'}]); + expect(workbenchLayout.part({partId: 'A'}).views.map(view => view.id)).toEqual(['view.1', 'view.2']); + expect(workbenchLayout.part({partId: 'C'}).views.map(view => view.id)).toEqual(['view.3']); }); /** @@ -1138,9 +1244,9 @@ describe('WorkbenchLayout', () => { .moveView('view.2', 'A') .moveView('view.3', 'C'); - expect(workbenchLayout.part({by: {partId: 'B'}})).toEqual(jasmine.objectContaining({id: 'B'})); - expect(workbenchLayout.part({by: {partId: 'A'}}).views).toEqual([{id: 'view.1'}, {id: 'view.2'}]); - expect(workbenchLayout.part({by: {partId: 'C'}}).views).toEqual([{id: 'view.3'}]); + expect(workbenchLayout.part({partId: 'B'}).id).toEqual('B'); + expect(workbenchLayout.part({partId: 'A'}).views.map(view => view.id)).toEqual(['view.1', 'view.2']); + expect(workbenchLayout.part({partId: 'C'}).views.map(view => view.id)).toEqual(['view.3']); }); it('should activate the most recently activated view when removing a view', () => { @@ -1152,8 +1258,8 @@ describe('WorkbenchLayout', () => { .addView('view.1', {partId: 'main'}) .addView('view.5', {partId: 'main'}) .addView('view.2', {partId: 'main'}) - .addView('view.4', {partId: 'main'}) - .addView('view.3', {partId: 'main'}); + .addView('view.3', {partId: 'main'}) + .addView('view.4', {partId: 'main'}); // prepare the activation history viewActivationInstantProviderSpyObj.getActivationInstant @@ -1165,17 +1271,17 @@ describe('WorkbenchLayout', () => { workbenchLayout = workbenchLayout .activateView('view.1') - .removeView('view.1'); - expect(workbenchLayout.part({by: {partId: 'main'}}).activeViewId).toEqual('view.4'); + .removeView('view.1', {force: true}); + expect(workbenchLayout.part({partId: 'main'}).activeViewId).toEqual('view.4'); - workbenchLayout = workbenchLayout.removeView('view.4'); - expect(workbenchLayout.part({by: {partId: 'main'}}).activeViewId).toEqual('view.2'); + workbenchLayout = workbenchLayout.removeView('view.4', {force: true}); + expect(workbenchLayout.part({partId: 'main'}).activeViewId).toEqual('view.2'); - workbenchLayout = workbenchLayout.removeView('view.2'); - expect(workbenchLayout.part({by: {partId: 'main'}}).activeViewId).toEqual('view.5'); + workbenchLayout = workbenchLayout.removeView('view.2', {force: true}); + expect(workbenchLayout.part({partId: 'main'}).activeViewId).toEqual('view.5'); - workbenchLayout = workbenchLayout.removeView('view.5'); - expect(workbenchLayout.part({by: {partId: 'main'}}).activeViewId).toEqual('view.3'); + workbenchLayout = workbenchLayout.removeView('view.5', {force: true}); + expect(workbenchLayout.part({partId: 'main'}).activeViewId).toEqual('view.3'); }); /** @@ -1436,7 +1542,7 @@ describe('WorkbenchLayout', () => { expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('B'); }); - it('should compute next view id for views that are target of a primary route', async () => { + it('should compute next view id', async () => { TestBed.overrideProvider(MAIN_AREA_INITIAL_PART_ID, {useValue: 'main'}); let workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory).addPart(MAIN_AREA); @@ -1450,9 +1556,6 @@ describe('WorkbenchLayout', () => { workbenchLayout = workbenchLayout.addView('view.2', {partId: 'main'}); expect(workbenchLayout.computeNextViewId()).toEqual('view.3'); - workbenchLayout = workbenchLayout.addView('view.6', {partId: 'main'}); - expect(workbenchLayout.computeNextViewId()).toEqual('view.3'); - workbenchLayout = workbenchLayout.addView('view.3', {partId: 'main'}); expect(workbenchLayout.computeNextViewId()).toEqual('view.4'); @@ -1460,12 +1563,19 @@ describe('WorkbenchLayout', () => { expect(workbenchLayout.computeNextViewId()).toEqual('view.5'); workbenchLayout = workbenchLayout.addView('view.5', {partId: 'main'}); + expect(workbenchLayout.computeNextViewId()).toEqual('view.6'); + + workbenchLayout = workbenchLayout.addView('view.6', {partId: 'main'}); + expect(workbenchLayout.computeNextViewId()).toEqual('view.7'); + + workbenchLayout = workbenchLayout.removeView('view.3'); // marked for removal expect(workbenchLayout.computeNextViewId()).toEqual('view.7'); - workbenchLayout = workbenchLayout.removeView('view.3'); + workbenchLayout = workbenchLayout.removeView('view.3'); // marked for removal + workbenchLayout = await workbenchLayout.removeViewsMarkedForRemoval(); expect(workbenchLayout.computeNextViewId()).toEqual('view.3'); - workbenchLayout = workbenchLayout.removeView('view.1'); + workbenchLayout = workbenchLayout.removeView('view.1', {force: true}); expect(workbenchLayout.computeNextViewId()).toEqual('view.1'); workbenchLayout = workbenchLayout.addView('view.1', {partId: 'main'}); @@ -1475,6 +1585,91 @@ describe('WorkbenchLayout', () => { expect(workbenchLayout.computeNextViewId()).toEqual('view.7'); }); + it('should remove view', () => { + TestBed.overrideProvider(MAIN_AREA_INITIAL_PART_ID, {useValue: 'main'}); + + const workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart(MAIN_AREA) + .addView('view.1', {partId: 'main'}) + .addView('view.2', {partId: 'main'}) + .addView('view.3', {partId: 'main'}) + .removeView('view.2', {force: true}); + + expect(workbenchLayout.view({viewId: 'view.1'}, {orElse: null})).toBeDefined(); + expect(workbenchLayout.view({viewId: 'view.2'}, {orElse: null})).toBeNull(); + expect(workbenchLayout.view({viewId: 'view.3'}, {orElse: null})).toBeDefined(); + }); + + it('should mark view for removal', async () => { + TestBed.overrideProvider(MAIN_AREA_INITIAL_PART_ID, {useValue: 'main'}); + + let workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart(MAIN_AREA) + .addView('view.1', {partId: 'main'}) + .addView('view.2', {partId: 'main'}) + .addView('view.3', {partId: 'main'}); + + // Mark views for removal. + workbenchLayout = workbenchLayout.removeView('view.1'); + workbenchLayout = workbenchLayout.removeView('view.2'); + + // Expect views not to be removed, but marked for removal. + const view1 = workbenchLayout.view({viewId: 'view.1'}); + const view2 = workbenchLayout.view({viewId: 'view.2'}); + const view3 = workbenchLayout.view({viewId: 'view.3'}); + + expect(view1.markedForRemoval).toBeTrue(); + expect(view2.markedForRemoval).toBeTrue(); + expect(view3.markedForRemoval).toBeUndefined(); + + // Remove views marked for removal if guard returns true. + workbenchLayout = await workbenchLayout.removeViewsMarkedForRemoval(viewUid => view2.uid === viewUid); + + // Expect views to be removed. + expect(workbenchLayout.view({viewId: 'view.1'}, {orElse: null})).toEqual(view1); + expect(workbenchLayout.view({viewId: 'view.2'}, {orElse: null})).toBeNull(); + expect(workbenchLayout.view({viewId: 'view.3'}, {orElse: null})).toEqual(view3); + + // Remove views marked for removal. + workbenchLayout = await workbenchLayout.removeViewsMarkedForRemoval(); + + // Expect views to be removed. + expect(workbenchLayout.view({viewId: 'view.1'}, {orElse: null})).toBeNull(); + expect(workbenchLayout.view({viewId: 'view.2'}, {orElse: null})).toBeNull(); + expect(workbenchLayout.view({viewId: 'view.3'}, {orElse: null})).toEqual(view3); + }); + + it('should not serialize `markedForRemoval` flag', () => { + TestBed.overrideProvider(MAIN_AREA_INITIAL_PART_ID, {useValue: 'main'}); + + const workbenchLayout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart(MAIN_AREA) + .addView('view.1', {partId: 'main'}) + .addView('view.2', {partId: 'main'}) + .addView('view.3', {partId: 'main'}); + + // Remove view and serialize the layout. + const serializedLayout = workbenchLayout + .removeView('view.2') + .serialize(); + + const deserializedLayout = TestBed.inject(ɵWorkbenchLayoutFactory).create({workbenchGrid: serializedLayout.workbenchGrid, mainAreaGrid: serializedLayout.mainAreaGrid}); + + const view1 = deserializedLayout.view({viewId: 'view.1'}); + const view2 = deserializedLayout.view({viewId: 'view.2'}); + const view3 = deserializedLayout.view({viewId: 'view.3'}); + + // Expect views not to be removed. + expect(view1).toBeDefined(); + expect(view2).toBeDefined(); + expect(view3).toBeDefined(); + + // Expect views not to be marked for removal. + expect(view1.markedForRemoval).toBeUndefined(); + expect(view2.markedForRemoval).toBeUndefined(); + expect(view3.markedForRemoval).toBeUndefined(); + }); + it('should find parts by criteria', () => { TestBed.overrideProvider(MAIN_AREA_INITIAL_PART_ID, {useValue: 'main'}); @@ -1524,40 +1719,40 @@ describe('WorkbenchLayout', () => { .addView('view.4', {partId: 'outerRight'}); // Find by part id - expect(workbenchLayout.part({by: {partId: 'outerLeft'}}).id).toEqual('outerLeft'); - expect(workbenchLayout.part({by: {partId: 'innerLeft'}}).id).toEqual('innerLeft'); - expect(workbenchLayout.part({by: {partId: 'innerRight'}}).id).toEqual('innerRight'); - expect(workbenchLayout.part({by: {partId: 'outerRight'}}).id).toEqual('outerRight'); + expect(workbenchLayout.part({partId: 'outerLeft'}).id).toEqual('outerLeft'); + expect(workbenchLayout.part({partId: 'innerLeft'}).id).toEqual('innerLeft'); + expect(workbenchLayout.part({partId: 'innerRight'}).id).toEqual('innerRight'); + expect(workbenchLayout.part({partId: 'outerRight'}).id).toEqual('outerRight'); // Find by grid and part id - expect(workbenchLayout.part({grid: 'workbench', by: {partId: 'outerLeft'}}).id).toEqual('outerLeft'); - expect(workbenchLayout.part({grid: 'mainArea', by: {partId: 'innerLeft'}}).id).toEqual('innerLeft'); - expect(workbenchLayout.part({grid: 'mainArea', by: {partId: 'innerRight'}}).id).toEqual('innerRight'); - expect(workbenchLayout.part({grid: 'workbench', by: {partId: 'outerRight'}}).id).toEqual('outerRight'); + expect(workbenchLayout.part({grid: 'workbench', partId: 'outerLeft'}).id).toEqual('outerLeft'); + expect(workbenchLayout.part({grid: 'mainArea', partId: 'innerLeft'}).id).toEqual('innerLeft'); + expect(workbenchLayout.part({grid: 'mainArea', partId: 'innerRight'}).id).toEqual('innerRight'); + expect(workbenchLayout.part({grid: 'workbench', partId: 'outerRight'}).id).toEqual('outerRight'); // Find by view id - expect(workbenchLayout.part({by: {viewId: 'view.1'}}).id).toEqual('innerLeft'); - expect(workbenchLayout.part({by: {viewId: 'view.2'}}).id).toEqual('innerRight'); - expect(workbenchLayout.part({by: {viewId: 'view.3'}}).id).toEqual('outerLeft'); - expect(workbenchLayout.part({by: {viewId: 'view.4'}}).id).toEqual('outerRight'); + expect(workbenchLayout.part({viewId: 'view.1'}).id).toEqual('innerLeft'); + expect(workbenchLayout.part({viewId: 'view.2'}).id).toEqual('innerRight'); + expect(workbenchLayout.part({viewId: 'view.3'}).id).toEqual('outerLeft'); + expect(workbenchLayout.part({viewId: 'view.4'}).id).toEqual('outerRight'); // Find by grid and view id - expect(workbenchLayout.part({grid: 'mainArea', by: {viewId: 'view.1'}}).id).toEqual('innerLeft'); - expect(workbenchLayout.part({grid: 'mainArea', by: {viewId: 'view.2'}}).id).toEqual('innerRight'); - expect(workbenchLayout.part({grid: 'workbench', by: {viewId: 'view.3'}}).id).toEqual('outerLeft'); - expect(workbenchLayout.part({grid: 'workbench', by: {viewId: 'view.4'}}).id).toEqual('outerRight'); + expect(workbenchLayout.part({grid: 'mainArea', viewId: 'view.1'}).id).toEqual('innerLeft'); + expect(workbenchLayout.part({grid: 'mainArea', viewId: 'view.2'}).id).toEqual('innerRight'); + expect(workbenchLayout.part({grid: 'workbench', viewId: 'view.3'}).id).toEqual('outerLeft'); + expect(workbenchLayout.part({grid: 'workbench', viewId: 'view.4'}).id).toEqual('outerRight'); // Find by part id and view id - expect(workbenchLayout.part({by: {partId: 'innerLeft', viewId: 'view.1'}}).id).toEqual('innerLeft'); - expect(workbenchLayout.part({by: {partId: 'innerRight', viewId: 'view.2'}}).id).toEqual('innerRight'); - expect(workbenchLayout.part({by: {partId: 'outerLeft', viewId: 'view.3'}}).id).toEqual('outerLeft'); - expect(workbenchLayout.part({by: {partId: 'outerRight', viewId: 'view.4'}}).id).toEqual('outerRight'); + expect(workbenchLayout.part({partId: 'innerLeft', viewId: 'view.1'}).id).toEqual('innerLeft'); + expect(workbenchLayout.part({partId: 'innerRight', viewId: 'view.2'}).id).toEqual('innerRight'); + expect(workbenchLayout.part({partId: 'outerLeft', viewId: 'view.3'}).id).toEqual('outerLeft'); + expect(workbenchLayout.part({partId: 'outerRight', viewId: 'view.4'}).id).toEqual('outerRight'); // Find by grid, part id and view id - expect(workbenchLayout.part({grid: 'mainArea', by: {partId: 'innerLeft', viewId: 'view.1'}}).id).toEqual('innerLeft'); - expect(workbenchLayout.part({grid: 'mainArea', by: {partId: 'innerRight', viewId: 'view.2'}}).id).toEqual('innerRight'); - expect(workbenchLayout.part({grid: 'workbench', by: {partId: 'outerLeft', viewId: 'view.3'}}).id).toEqual('outerLeft'); - expect(workbenchLayout.part({grid: 'workbench', by: {partId: 'outerRight', viewId: 'view.4'}}).id).toEqual('outerRight'); + expect(workbenchLayout.part({grid: 'mainArea', partId: 'innerLeft', viewId: 'view.1'}).id).toEqual('innerLeft'); + expect(workbenchLayout.part({grid: 'mainArea', partId: 'innerRight', viewId: 'view.2'}).id).toEqual('innerRight'); + expect(workbenchLayout.part({grid: 'workbench', partId: 'outerLeft', viewId: 'view.3'}).id).toEqual('outerLeft'); + expect(workbenchLayout.part({grid: 'workbench', partId: 'outerRight', viewId: 'view.4'}).id).toEqual('outerRight'); }); it('should throw an error if not finding the part', () => { @@ -1567,12 +1762,12 @@ describe('WorkbenchLayout', () => { .addPart(MAIN_AREA) .addView('view.1', {partId: 'main'}); - expect(() => workbenchLayout.part({by: {partId: 'does-not-exist'}})).toThrowError(/NullPartError/); - expect(() => workbenchLayout.part({by: {partId: 'does-not-exist', viewId: 'view.1'}})).toThrowError(/NullPartError/); - expect(() => workbenchLayout.part({by: {partId: 'main', viewId: 'view.2'}})).toThrowError(/NullPartError/); - expect(() => workbenchLayout.part({grid: 'workbench', by: {partId: 'main', viewId: 'view.1'}})).toThrowError(/NullPartError/); - expect(() => workbenchLayout.part({grid: 'workbench', by: {viewId: 'view.1'}})).toThrowError(/NullPartError/); - expect(() => workbenchLayout.part({grid: 'workbench', by: {partId: 'main'}})).toThrowError(/NullPartError/); + expect(() => workbenchLayout.part({partId: 'does-not-exist'})).toThrowError(/NullPartError/); + expect(() => workbenchLayout.part({partId: 'does-not-exist', viewId: 'view.1'})).toThrowError(/NullPartError/); + expect(() => workbenchLayout.part({partId: 'main', viewId: 'view.2'})).toThrowError(/NullPartError/); + expect(() => workbenchLayout.part({grid: 'workbench', partId: 'main', viewId: 'view.1'})).toThrowError(/NullPartError/); + expect(() => workbenchLayout.part({grid: 'workbench', viewId: 'view.1'})).toThrowError(/NullPartError/); + expect(() => workbenchLayout.part({grid: 'workbench', partId: 'main'})).toThrowError(/NullPartError/); }); it('should return `null` if not finding the part', () => { @@ -1582,12 +1777,12 @@ describe('WorkbenchLayout', () => { .addPart(MAIN_AREA) .addView('view.1', {partId: 'main'}); - expect(workbenchLayout.part({by: {partId: 'does-not-exist'}}, {orElse: null})).toBeNull(); - expect(workbenchLayout.part({by: {partId: 'does-not-exist', viewId: 'view.1'}}, {orElse: null})).toBeNull(); - expect(workbenchLayout.part({by: {partId: 'main', viewId: 'view.2'}}, {orElse: null})).toBeNull(); - expect(workbenchLayout.part({grid: 'workbench', by: {partId: 'main', viewId: 'view.1'}}, {orElse: null})).toBeNull(); - expect(workbenchLayout.part({grid: 'workbench', by: {viewId: 'view.1'}}, {orElse: null})).toBeNull(); - expect(workbenchLayout.part({grid: 'workbench', by: {partId: 'main'}}, {orElse: null})).toBeNull(); + expect(workbenchLayout.part({partId: 'does-not-exist'}, {orElse: null})).toBeNull(); + expect(workbenchLayout.part({partId: 'does-not-exist', viewId: 'view.1'}, {orElse: null})).toBeNull(); + expect(workbenchLayout.part({partId: 'main', viewId: 'view.2'}, {orElse: null})).toBeNull(); + expect(workbenchLayout.part({grid: 'workbench', partId: 'main', viewId: 'view.1'}, {orElse: null})).toBeNull(); + expect(workbenchLayout.part({grid: 'workbench', viewId: 'view.1'}, {orElse: null})).toBeNull(); + expect(workbenchLayout.part({grid: 'workbench', partId: 'main'}, {orElse: null})).toBeNull(); }); it('should return whether a part is contained in the main area', () => { @@ -1622,6 +1817,146 @@ describe('WorkbenchLayout', () => { expect(workbenchLayout.views({grid: 'mainArea'}).map(view => view.id)).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2'])); }); + it('should find views by URL segments', () => { + const layout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart(MAIN_AREA) + .addView('view.1', {partId: MAIN_AREA}) + .addView('view.2', {partId: MAIN_AREA}) + .addView('view.3', {partId: MAIN_AREA}) + .navigateView('view.1', ['path', 'to', 'view', '1']) + .navigateView('view.2', ['path', 'to', 'view', '2']) + .navigateView('view.3', ['path', 'to', 'view', '2']); + + expect(layout + .views({segments: new UrlSegmentMatcher(segments(['path']), {matchWildcardPath: false, matchMatrixParams: false})}) + .map(view => view.id), + ).toEqual([]); + + expect(layout + .views({segments: new UrlSegmentMatcher(segments(['path', 'to', 'view']), {matchWildcardPath: false, matchMatrixParams: false})}) + .map(view => view.id), + ).toEqual([]); + + expect(layout + .views({segments: new UrlSegmentMatcher(segments(['path', 'to', 'view', '1']), {matchWildcardPath: false, matchMatrixParams: false})}) + .map(view => view.id), + ).toEqual(['view.1']); + + expect(layout + .views({segments: new UrlSegmentMatcher(segments(['path', 'to', 'view', '2']), {matchWildcardPath: false, matchMatrixParams: false})}) + .map(view => view.id), + ).toEqual(jasmine.arrayWithExactContents(['view.2', 'view.3'])); + + expect(layout + .views({segments: new UrlSegmentMatcher(segments(['path', 'to', 'view', '*']), {matchWildcardPath: false, matchMatrixParams: false})}) + .map(view => view.id), + ).toEqual([]); + + expect(layout + .views({segments: new UrlSegmentMatcher(segments(['path', 'to', 'view', '*']), {matchWildcardPath: true, matchMatrixParams: false})}) + .map(view => view.id), + ).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2', 'view.3'])); + }); + + it('should find views by URL segments (matrix params matching)', () => { + const layout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart(MAIN_AREA) + .addView('view.1', {partId: MAIN_AREA}) + .addView('view.2', {partId: MAIN_AREA}) + .addView('view.3', {partId: MAIN_AREA}) + .navigateView('view.1', ['path', 'to', 'view']) + .navigateView('view.2', ['path', 'to', 'view', {matrixParam: 'A'}]) + .navigateView('view.3', ['path', 'to', 'view', {matrixParam: 'B'}]); + + expect(layout + .views({segments: new UrlSegmentMatcher(segments(['path', 'to', 'view']), {matchWildcardPath: false, matchMatrixParams: false})}) + .map(view => view.id), + ).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2', 'view.3'])); + + expect(layout + .views({segments: new UrlSegmentMatcher(segments(['path', 'to', 'view']), {matchWildcardPath: false, matchMatrixParams: true})}) + .map(view => view.id), + ).toEqual(['view.1']); + + expect(layout + .views({segments: new UrlSegmentMatcher(segments(['path', 'to', 'view', {matrixParam: 'A'}]), {matchWildcardPath: false, matchMatrixParams: false})}) + .map(view => view.id), + ).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2', 'view.3'])); + + expect(layout + .views({segments: new UrlSegmentMatcher(segments(['path', 'to', 'view', {matrixParam: 'A'}]), {matchWildcardPath: false, matchMatrixParams: true})}) + .map(view => view.id), + ).toEqual(['view.2']); + }); + + it('should find views by navigation hint', () => { + const layout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart(MAIN_AREA) + .addView('view.1', {partId: MAIN_AREA}) + .addView('view.2', {partId: MAIN_AREA}) + .addView('view.3', {partId: MAIN_AREA}) + .navigateView('view.1', ['path', 'to', 'view', '1']) + .navigateView('view.2', ['path', 'to', 'view', '2'], {hint: 'hint1'}) + .navigateView('view.3', [], {hint: 'hint2'}); + + expect(layout + .views() + .map(view => view.id), + ).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2', 'view.3'])); + + expect(layout + .views({navigationHint: undefined}) + .map(view => view.id), + ).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2', 'view.3'])); + + expect(layout + .views({navigationHint: ''}) + .map(view => view.id), + ).toEqual([]); + + expect(layout + .views({navigationHint: null}) + .map(view => view.id), + ).toEqual(['view.1']); + + expect(layout + .views({navigationHint: 'hint1'}) + .map(view => view.id), + ).toEqual(['view.2']); + + expect(layout + .views({navigationHint: 'hint2'}) + .map(view => view.id), + ).toEqual(['view.3']); + }); + + it('should find views by part', () => { + const layout = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart('left') + .addPart('right', {align: 'right'}) + .addView('view.1', {partId: 'left'}) + .addView('view.2', {partId: 'left'}) + .addView('view.3', {partId: 'right'}) + .navigateView('view.1', ['path', 'to', 'view']) + .navigateView('view.2', ['path', 'to', 'view']) + .navigateView('view.3', ['path', 'to', 'view']); + + expect(layout + .views({partId: undefined}) + .map(view => view.id), + ).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2', 'view.3'])); + + expect(layout + .views({partId: 'left'}) + .map(view => view.id), + ).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2'])); + + expect(layout + .views({partId: 'right'}) + .map(view => view.id), + ).toEqual(['view.3']); + }); + it('should activate adjacent view', () => { TestBed.overrideProvider(MAIN_AREA_INITIAL_PART_ID, {useValue: 'main'}); @@ -1633,27 +1968,27 @@ describe('WorkbenchLayout', () => { .addView('view.3', {partId: 'part'}); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('main'); - expect(workbenchLayout.part({by: {partId: 'part'}}).activeViewId).toBeUndefined(); + expect(workbenchLayout.part({partId: 'part'}).activeViewId).toBeUndefined(); // Activate adjacent view workbenchLayout = workbenchLayout.activateAdjacentView('view.2'); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('main'); - expect(workbenchLayout.part({by: {partId: 'part'}}).activeViewId).toEqual('view.1'); + expect(workbenchLayout.part({partId: 'part'}).activeViewId).toEqual('view.1'); // Activate adjacent view workbenchLayout = workbenchLayout.activateAdjacentView('view.3'); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('main'); - expect(workbenchLayout.part({by: {partId: 'part'}}).activeViewId).toEqual('view.2'); + expect(workbenchLayout.part({partId: 'part'}).activeViewId).toEqual('view.2'); // Activate adjacent view workbenchLayout = workbenchLayout.activateAdjacentView('view.1'); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('main'); - expect(workbenchLayout.part({by: {partId: 'part'}}).activeViewId).toEqual('view.2'); + expect(workbenchLayout.part({partId: 'part'}).activeViewId).toEqual('view.2'); // Activate adjacent view workbenchLayout = workbenchLayout.activateAdjacentView('view.2', {activatePart: true}); expect(workbenchLayout.activePart({grid: 'mainArea'})!.id).toEqual('part'); - expect(workbenchLayout.part({by: {partId: 'part'}}).activeViewId).toEqual('view.1'); + expect(workbenchLayout.part({partId: 'part'}).activeViewId).toEqual('view.1'); }); it('should allow activating a part', () => { @@ -1709,54 +2044,48 @@ describe('WorkbenchLayout', () => { expect(changedLayout.views().map(view => view.id)).toEqual(jasmine.arrayWithExactContents(['view.10', 'view.2', 'view.3', 'view.4'])); // Rename 'view.1' to 'view.10' [grid=mainArea] - changedLayout = workbenchLayout.renameView('view.1', 'view.10', {grid: 'mainArea'}); + changedLayout = workbenchLayout.renameView('view.1', 'view.10'); expect(changedLayout.views().map(view => view.id)).toEqual(jasmine.arrayWithExactContents(['view.10', 'view.2', 'view.3', 'view.4'])); - // Rename 'view.1' to 'view.10' [grid=workbench] (wrong grid) - expect(() => workbenchLayout.renameView('view.1', 'view.10', {grid: 'workbench'})).toThrowError(/NullPartError/); - // Rename 'view.3' to 'view.30' changedLayout = workbenchLayout.renameView('view.3', 'view.30'); expect(changedLayout.views().map(view => view.id)).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2', 'view.30', 'view.4'])); - // Rename 'view.3' to 'view.30' [grid=mainArea] (wrong grid) - expect(() => workbenchLayout.renameView('view.3', 'view.30', {grid: 'mainArea'})).toThrowError(/NullPartError/); - // Rename 'view.3' to 'view.30' [grid=workbench] - changedLayout = workbenchLayout.renameView('view.3', 'view.30', {grid: 'workbench'}); + changedLayout = workbenchLayout.renameView('view.3', 'view.30'); expect(changedLayout.views().map(view => view.id)).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2', 'view.30', 'view.4'])); // Rename 'view.99' (does not exist) - expect(() => workbenchLayout.renameView('view.99', 'view.999')).toThrowError(/NullPartError/); + expect(() => workbenchLayout.renameView('view.99', 'view.999')).toThrowError(/NullViewError/); // Rename 'view.1' to 'view.2' - expect(() => workbenchLayout.renameView('view.1', 'view.2')).toThrowError(/\[IllegalArgumentError] View id must be unique/); + expect(() => workbenchLayout.renameView('view.1', 'view.2')).toThrowError(/\[ViewRenameError] View id must be unique/); // Rename 'view.2' to 'view.3' - expect(() => workbenchLayout.renameView('view.2', 'view.3')).toThrowError(/\[IllegalArgumentError] View id must be unique/); + expect(() => workbenchLayout.renameView('view.2', 'view.3')).toThrowError(/\[ViewRenameError] View id must be unique/); // Rename 'view.3' to 'view.4' - expect(() => workbenchLayout.renameView('view.3', 'view.4')).toThrowError(/\[IllegalArgumentError] View id must be unique/); + expect(() => workbenchLayout.renameView('view.3', 'view.4')).toThrowError(/\[ViewRenameError] View id must be unique/); // Rename 'view.1' to 'view.10' and expect activated view to be changed. changedLayout = workbenchLayout.renameView('view.1', 'view.10'); expect(changedLayout.views().map(view => view.id)).toEqual(jasmine.arrayWithExactContents(['view.10', 'view.2', 'view.3', 'view.4'])); - expect(changedLayout.part({by: {viewId: 'view.10'}}).activeViewId).toEqual('view.10'); + expect(changedLayout.part({viewId: 'view.10'}).activeViewId).toEqual('view.10'); // Rename 'view.2' to 'view.20' and expect activated view not to be changed. changedLayout = workbenchLayout.renameView('view.2', 'view.20'); expect(changedLayout.views().map(view => view.id)).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.20', 'view.3', 'view.4'])); - expect(changedLayout.part({by: {viewId: 'view.20'}}).activeViewId).toEqual('view.1'); + expect(changedLayout.part({viewId: 'view.20'}).activeViewId).toEqual('view.1'); // Rename 'view.3' to 'view.30' and expect activated view to be changed. changedLayout = workbenchLayout.renameView('view.3', 'view.30'); expect(changedLayout.views().map(view => view.id)).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2', 'view.30', 'view.4'])); - expect(changedLayout.part({by: {viewId: 'view.30'}}).activeViewId).toEqual('view.30'); + expect(changedLayout.part({viewId: 'view.30'}).activeViewId).toEqual('view.30'); // Rename 'view.4' to 'view.40' and expect activated view not to be changed. changedLayout = workbenchLayout.renameView('view.4', 'view.40'); expect(changedLayout.views().map(view => view.id)).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2', 'view.3', 'view.40'])); - expect(changedLayout.part({by: {viewId: 'view.40'}}).activeViewId).toEqual('view.3'); + expect(changedLayout.part({viewId: 'view.40'}).activeViewId).toEqual('view.3'); }); it('should allow setting split ratio', () => { @@ -1774,17 +2103,17 @@ describe('WorkbenchLayout', () => { expect(findParentNode('left').ratio).toEqual(.3); // Expect to error if setting the ratio for a node not contained in the layout. - expect(() => workbenchLayout.setSplitRatio('does-not-exist', .3)).toThrowError(/NullNodeError/); + expect(() => workbenchLayout.setSplitRatio('does-not-exist', .3)).toThrowError(/NullElementError/); // Expect to error if setting an illegal ratio. - expect(() => workbenchLayout.setSplitRatio(findParentNode('left').nodeId, -.1)).toThrowError(/IllegalArgumentError/); - expect(() => workbenchLayout.setSplitRatio(findParentNode('left').nodeId, 0)).not.toThrowError(/IllegalArgumentError/); - expect(() => workbenchLayout.setSplitRatio(findParentNode('left').nodeId, .5)).not.toThrowError(/IllegalArgumentError/); - expect(() => workbenchLayout.setSplitRatio(findParentNode('left').nodeId, 1)).not.toThrowError(/IllegalArgumentError/); - expect(() => workbenchLayout.setSplitRatio(findParentNode('left').nodeId, 1.1)).toThrowError(/IllegalArgumentError/); + expect(() => workbenchLayout.setSplitRatio(findParentNode('left').nodeId, -.1)).toThrowError(/LayoutModifyError/); + expect(() => workbenchLayout.setSplitRatio(findParentNode('left').nodeId, 0)).not.toThrowError(/LayoutModifyError/); + expect(() => workbenchLayout.setSplitRatio(findParentNode('left').nodeId, .5)).not.toThrowError(/LayoutModifyError/); + expect(() => workbenchLayout.setSplitRatio(findParentNode('left').nodeId, 1)).not.toThrowError(/LayoutModifyError/); + expect(() => workbenchLayout.setSplitRatio(findParentNode('left').nodeId, 1.1)).toThrowError(/LayoutModifyError/); function findParentNode(partId: string): MTreeNode { - const parent = workbenchLayout.part({by: {partId}}).parent; + const parent = workbenchLayout.part({partId}).parent; if (!parent) { throw Error(`[MTreeNodeNotFoundError] Parent MTreeNode not found [partId=${partId}].`); } diff --git a/projects/scion/workbench/src/lib/layout/workbench-layout.ts b/projects/scion/workbench/src/lib/layout/workbench-layout.ts index 4927af65f..9128ed9c7 100644 --- a/projects/scion/workbench/src/lib/layout/workbench-layout.ts +++ b/projects/scion/workbench/src/lib/layout/workbench-layout.ts @@ -8,6 +8,9 @@ * SPDX-License-Identifier: EPL-2.0 */ +import {Commands, ViewState} from '../routing/routing.model'; +import {ActivatedRoute} from '@angular/router'; + /** * The workbench layout is a grid of parts. Parts are aligned relative to each other. A part is a stack of views. Content is * displayed in views. @@ -17,7 +20,7 @@ * the user's workflow. Defining a main area is optional and recommended for applications requiring a dedicated and maximizable * area for user interaction. * - * Multiple layouts, called perspectives, are supported. Perspectives can be switched with one perspective active at a time. + * Multiple layouts, called perspectives, are supported. Perspectives can be switched. Only one perspective is active at a time. * Perspectives share the same main area, if any. * * The layout is an immutable object that provides methods to modify the layout. Modifications have no @@ -31,7 +34,7 @@ export interface WorkbenchLayout { * @param id - Unique id of the part. Use {@link MAIN_AREA} to add the main area. * @param relativeTo - Specifies the reference part to lay out the part. * @param options - Controls how to add the part to the layout. - * @property activate - Controls whether to activate the part. If not set, defaults to `false`. + * @param options.activate - Controls whether to activate the part. Default is `false`. * @return a copy of this layout with the part added. */ addPart(id: string | MAIN_AREA, relativeTo: ReferencePart, options?: {activate?: boolean}): WorkbenchLayout; @@ -41,13 +44,44 @@ export interface WorkbenchLayout { * * @param id - The id of the view to add. * @param options - Controls how to add the view to the layout. - * @property partId - References the part to which to add the view. - * @property position - Specifies the position where to insert the view. The position is zero-based. If not set, adds the view at the end. - * @property activateView - Controls whether to activate the view. If not set, defaults to `false`. - * @property activatePart - Controls whether to activate the part that contains the view. If not set, defaults to `false`. + * @param options.partId - References the part to which to add the view. + * @param options.position - Specifies the position where to insert the view. The position is zero-based. Default is `end`. + * @param options.activateView - Controls whether to activate the view. Default is `false`. + * @param options.activatePart - Controls whether to activate the part that contains the view. Default is `false`. * @return a copy of this layout with the view added. */ - addView(id: string, options: {partId: string; position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; activateView?: boolean; activatePart?: boolean}): WorkbenchLayout; + addView(id: string, options: {partId: string; position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; activateView?: boolean; activatePart?: boolean; cssClass?: string | string[]}): WorkbenchLayout; + + /** + * Navigates the specified view based on the provided array of commands and extras. + * + * A command can be a string or an object literal. A string represents a path segment, an object literal associates matrix parameters with the preceding segment. + * Multiple segments can be combined into a single command, separated by a forward slash. + * + * By default, navigation is absolute. Set `relativeTo` in extras for relative navigation. + * + * Usage: + * ``` + * layout.navigateView(viewId, ['path', 'to', 'view', {param1: 'value1', param2: 'value2'}]); + * layout.navigateView(viewId, ['path/to/view', {param1: 'value1', param2: 'value2'}]); + * ``` + * + * @param id - Identifies the view for navigation. + * @param commands - Instructs the router which route to navigate to. + * @param extras - Controls navigation. + * @param extras.hint - Sets a hint to control navigation, e.g., for use in a `CanMatch` guard to differentiate between routes with an identical path. + * For example, views of the initial layout or a perspective are usually navigated to the empty path route to avoid cluttering the URL, + * requiring a navigation hint to differentiate between the routes. See {@link canMatchWorkbenchView} for an example. + * Like the path, a hint affects view resolution. If set, the router will only navigate views with an equivalent hint, or if not set, views without a hint. + * @param extras.relativeTo - Specifies the route for relative navigation, supporting navigational symbols such as '/', './', or '../' in the commands. + * @param extras.state - Associates arbitrary state with a view navigation. + * Navigational state is stored in the browser's session history, supporting back/forward navigation, but is lost on page reload. + * Therefore, a view must be able to restore its state without relying on navigational state. + * Navigational state can be read from {@link WorkbenchView.state} or the browser's session history via `history.state`. + * @param extras.cssClass - Specifies CSS class(es) to add to the view, e.g., to locate the view in tests. + * @return a copy of this layout with the view navigated. + */ + navigateView(id: string, commands: Commands, extras?: {hint?: string; relativeTo?: ActivatedRoute; state?: ViewState; cssClass?: string | string[]}): WorkbenchLayout; /** * Removes given view from the layout. @@ -70,12 +104,25 @@ export interface WorkbenchLayout { */ removePart(id: string): WorkbenchLayout; + /** + * Moves a view to a different part or moves it within a part. + * + * @param id - The id of the view to be moved. + * @param targetPartId - The id of the part to which to move the view. + * @param options - Controls moving of the view. + * @param options.position - Specifies the position where to move the view in the target part. The position is zero-based. Default is `end` when moving the view to a different part. + * @param options.activateView - Controls if to activate the view. Default is `false`. + * @param options.activatePart - Controls if to activate the target part. Default is `false`. + * @return a copy of this layout with the view moved. + */ + moveView(id: string, targetPartId: string, options?: {position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; activateView?: boolean; activatePart?: boolean}): WorkbenchLayout; + /** * Activates the given view. * * @param id - The id of the view which to activate. * @param options - Controls view activation. - * @property activatePart - Controls whether to activate the part that contains the view. If not set, defaults to `false`. + * @param options.activatePart - Controls whether to activate the part that contains the view. Default is `false`. * @return a copy of this layout with the view activated. */ activateView(id: string, options?: {activatePart?: boolean}): WorkbenchLayout; diff --git a/projects/scion/workbench/src/lib/layout/workbench-layouts.util.ts b/projects/scion/workbench/src/lib/layout/workbench-layouts.util.ts index e340b2321..6f0340bd8 100644 --- a/projects/scion/workbench/src/lib/layout/workbench-layouts.util.ts +++ b/projects/scion/workbench/src/lib/layout/workbench-layouts.util.ts @@ -8,6 +8,9 @@ * SPDX-License-Identifier: EPL-2.0 */ import {MPart, MTreeNode} from './workbench-layout.model'; +import {MAIN_AREA} from './workbench-layout'; +import {ViewId} from '../view/workbench-view.model'; +import {VIEW_ID_PREFIX} from '../workbench.constants'; /** * Provides helper functions for operating on a workbench layout. @@ -28,4 +31,42 @@ export const WorkbenchLayouts = { } return parts; }, + + /** + * Tests if the given {@link MTreeNode} or {@link MPart} is visible. + * + * - A part is considered visible if it is the main area part or has at least one view. + * - A node is considered visible if it has at least one visible part in its child hierarchy. + */ + isGridElementVisible: (element: MTreeNode | MPart): boolean => { + if (element instanceof MPart) { + return element.id === MAIN_AREA || element.views.length > 0; + } + return WorkbenchLayouts.isGridElementVisible(element.child1) || WorkbenchLayouts.isGridElementVisible(element.child2); + }, + + /** + * Computes the next available view id. + */ + computeNextViewId: (viewIds: Iterable): ViewId => { + const ids = Array.from(viewIds) + .map(viewId => Number(viewId.substring(VIEW_ID_PREFIX.length))) + .reduce((set, id) => set.add(id), new Set()); + + for (let i = 1; i <= ids.size; i++) { + if (!ids.has(i)) { + return VIEW_ID_PREFIX.concat(`${i}`) as ViewId; + } + } + return VIEW_ID_PREFIX.concat(`${ids.size + 1}`) as ViewId; + }, + + /** + * Tests if the given id matches the format of a view identifier (e.g., `view.1`, `view.2`, etc.). + * + * @see ViewId + */ + isViewId: (viewId: string | undefined | null): viewId is ViewId => { + return viewId?.startsWith(VIEW_ID_PREFIX) ?? false; + }, } as const; diff --git a/projects/scion/workbench/src/lib/layout/workench-layout-serializer.service.ts b/projects/scion/workbench/src/lib/layout/workench-layout-serializer.service.ts index e42fe5ae2..035f95eb6 100644 --- a/projects/scion/workbench/src/lib/layout/workench-layout-serializer.service.ts +++ b/projects/scion/workbench/src/lib/layout/workench-layout-serializer.service.ts @@ -8,10 +8,16 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {Injectable} from '@angular/core'; +import {inject, Injectable} from '@angular/core'; import {MPart, MPartGrid, MTreeNode, ɵMPartGrid} from './workbench-layout.model'; -import {WorkbenchLayoutMigrator} from './migration/workbench-layout-migrator.service'; -import {UUID} from '@scion/toolkit/uuid'; +import {ViewOutlets} from '../routing/routing.model'; +import {UrlSegment} from '@angular/router'; +import {WorkbenchLayoutMigrationV2} from './migration/workbench-layout-migration-v2.service'; +import {WorkbenchLayoutMigrationV3} from './migration/workbench-layout-migration-v3.service'; +import {WorkbenchMigrator} from '../migration/workbench-migrator'; +import {ViewId} from '../view/workbench-view.model'; +import {WorkbenchLayoutMigrationV4} from './migration/workbench-layout-migration-v4.service'; +import {randomUUID} from '../common/uuid.util'; /** * Serializes and deserializes a base64-encoded JSON into a {@link MPartGrid}. @@ -19,19 +25,23 @@ import {UUID} from '@scion/toolkit/uuid'; @Injectable({providedIn: 'root'}) export class WorkbenchLayoutSerializer { - constructor(private _workbenchLayoutMigrator: WorkbenchLayoutMigrator) { - } + private _workbenchLayoutMigrator = new WorkbenchMigrator() + .registerMigration(1, inject(WorkbenchLayoutMigrationV2)) + .registerMigration(2, inject(WorkbenchLayoutMigrationV3)) + .registerMigration(3, inject(WorkbenchLayoutMigrationV4)); /** * Serializes the given grid into a URL-safe base64 string. * * @param grid - Specifies the grid to be serialized. * @param options - Controls the serialization. - * @property includeNodeId - Controls if to include the `nodeId`. By default, if not set, the `nodeId` is excluded from serialization. + * @param options.includeNodeId - Controls if to include the `nodeId`. By default, if not set, the `nodeId` is excluded from serialization. + * @param options.includeUid - Controls if to include the view `uid`. By default, if not set, the `uid` is excluded from serialization. + * @param options.includeMarkRemovedFlag - Controls if to include the `markedForRemoval` flag. By default, if not set, the `markedForRemoval` is excluded from serialization. */ - public serialize(grid: MPartGrid, options?: {includeNodeId?: boolean}): string; - public serialize(grid: MPartGrid | undefined | null, options?: {includeNodeId?: boolean}): null | string; - public serialize(grid: MPartGrid | undefined | null, options?: {includeNodeId?: boolean}): string | null { + public serializeGrid(grid: MPartGrid, options?: {includeNodeId?: boolean; includeUid?: boolean; includeMarkedForRemovalFlag?: boolean}): string; + public serializeGrid(grid: MPartGrid | undefined | null, options?: {includeNodeId?: boolean; includeUid?: boolean; includeMarkedForRemovalFlag?: boolean}): null | string; + public serializeGrid(grid: MPartGrid | undefined | null, options?: {includeNodeId?: boolean; includeUid?: boolean; includeMarkedForRemovalFlag?: boolean}): string | null { if (grid === null || grid === undefined) { return null; } @@ -40,6 +50,12 @@ export class WorkbenchLayoutSerializer { if (!options?.includeNodeId) { transientFields.add('nodeId'); } + if (!options?.includeUid) { + transientFields.add('uid'); + } + if (!options?.includeMarkedForRemovalFlag) { + transientFields.add('markedForRemoval'); + } const json = JSON.stringify(grid, (key, value) => { return transientFields.has(key) ? undefined : value; @@ -48,21 +64,21 @@ export class WorkbenchLayoutSerializer { } /** - * Deserializes the given base64-serialized grid. + * Deserializes the given base64-serialized grid, applying necessary migrations if the serialized grid is outdated. */ - public deserialize(serializedGrid: string): ɵMPartGrid { - const [jsonGrid, jsonGridVersion] = window.atob(serializedGrid).split(VERSION_SEPARATOR, 2); + public deserializeGrid(serialized: string): ɵMPartGrid { + const [jsonGrid, jsonGridVersion] = window.atob(serialized).split(VERSION_SEPARATOR, 2); const gridVersion = Number.isNaN(Number(jsonGridVersion)) ? 1 : Number(jsonGridVersion); - const isGridOutdated = gridVersion < WORKBENCH_LAYOUT_VERSION; - const migratedJsonGrid = isGridOutdated ? this._workbenchLayoutMigrator.migrate(gridVersion, jsonGrid) : jsonGrid; + const migratedJsonGrid = this._workbenchLayoutMigrator.migrate(jsonGrid, {from: gridVersion, to: WORKBENCH_LAYOUT_VERSION}); // Parse the JSON. const grid: MPartGrid = JSON.parse(migratedJsonGrid, (key, value) => { if (MPart.isMPart(value)) { - return new MPart(value); // create a class object from the object literal + const views = value.views.map(view => ({...view, uid: view.uid ?? randomUUID()})); + return new MPart({...value, views}); // create a class object from the object literal } if (MTreeNode.isMTreeNode(value)) { - return new MTreeNode({...value, nodeId: value.nodeId ?? UUID.randomUUID()}); // create a class object from the object literal + return new MTreeNode({...value, nodeId: value.nodeId ?? randomUUID()}); // create a class object from the object literal } return value; }); @@ -77,7 +93,29 @@ export class WorkbenchLayoutSerializer { } })(grid.root, undefined); - return {...grid, migrated: isGridOutdated}; + return (gridVersion < WORKBENCH_LAYOUT_VERSION) ? {...grid, migrated: true} : grid; + } + + /** + * Serializes the given outlets. + */ + public serializeViewOutlets(viewOutlets: ViewOutlets): string { + return JSON.stringify(Object.fromEntries(Object.entries(viewOutlets) + .map(([viewId, segments]: [string, UrlSegment[]]): [string, MUrlSegment[]] => { + return [viewId, segments.map(segment => ({path: segment.path, parameters: segment.parameters}))]; + }))); + } + + /** + * Deserializes the given outlets. + */ + public deserializeViewOutlets(serialized: string): ViewOutlets { + const viewOutlets: {[viewId: ViewId]: MUrlSegment[]} = JSON.parse(serialized); + + return Object.fromEntries(Object.entries(viewOutlets) + .map(([viewId, segments]: [string, MUrlSegment[]]): [string, UrlSegment[]] => { + return [viewId, segments.map(segment => new UrlSegment(segment.path, segment.parameters))]; + })); } } @@ -86,9 +124,10 @@ export class WorkbenchLayoutSerializer { * * Increment this version and write a migrator when introducting a breaking layout model change. * - * @see WorkbenchLayoutMigrator + * @see WorkbenchMigrator */ -const WORKBENCH_LAYOUT_VERSION = 2; +export const WORKBENCH_LAYOUT_VERSION = 4; + /** * Fields not serialized into JSON representation. */ @@ -99,3 +138,13 @@ const TRANSIENT_FIELDS = new Set().add('parent').add('migrated'); * Format: // */ const VERSION_SEPARATOR = '//'; + +/** + * Represents a segment in the URL. + * + * The M-prefix indicates this object is a model object that is serialized and stored, requiring migration on breaking change. + */ +interface MUrlSegment { + path: string; + parameters: {[name: string]: string}; +} diff --git "a/projects/scion/workbench/src/lib/layout/\311\265workbench-layout.factory.ts" "b/projects/scion/workbench/src/lib/layout/\311\265workbench-layout.factory.ts" index dd050630c..20da3184d 100644 --- "a/projects/scion/workbench/src/lib/layout/\311\265workbench-layout.factory.ts" +++ "b/projects/scion/workbench/src/lib/layout/\311\265workbench-layout.factory.ts" @@ -13,6 +13,7 @@ import {MPart, MPartGrid} from './workbench-layout.model'; import {WorkbenchLayoutFactory} from './workbench-layout.factory'; import {EnvironmentInjector, Injectable, Injector, runInInjectionContext} from '@angular/core'; import {MAIN_AREA} from './workbench-layout'; +import {ViewOutlets, ViewStates} from '../routing/routing.model'; /** * @inheritDoc @@ -28,7 +29,7 @@ export class ɵWorkbenchLayoutFactory implements WorkbenchLayoutFactory { */ public addPart(id: string | MAIN_AREA): ɵWorkbenchLayout { return this.create({ - workbenchGrid: {root: new MPart({id, structural: true}), activePartId: id}, + workbenchGrid: {root: new MPart({id, structural: true, views: []}), activePartId: id}, }); } @@ -38,12 +39,15 @@ export class ɵWorkbenchLayoutFactory implements WorkbenchLayoutFactory { * - If not specifying the workbench grid, creates a workbench grid with a main area. * - If not specifying the main area grid, but the workbench grid has a main area part, creates a main area grid with an initial part. * To control the identity of the initial part, pass an injector and set the DI token {@link MAIN_AREA_INITIAL_PART_ID}. + * - Grids and outlets can be passed in serialized or deserialized form. */ - public create(options?: {workbenchGrid?: string | MPartGrid | null; mainAreaGrid?: string | MPartGrid | null; injector?: Injector; maximized?: boolean}): ɵWorkbenchLayout { + public create(options?: {workbenchGrid?: string | MPartGrid | null; mainAreaGrid?: string | MPartGrid | null; viewOutlets?: ViewOutlets | string; viewStates?: ViewStates; injector?: Injector; maximized?: boolean}): ɵWorkbenchLayout { return runInInjectionContext(options?.injector ?? this._environmentInjector, () => new ɵWorkbenchLayout({ workbenchGrid: options?.workbenchGrid, mainAreaGrid: options?.mainAreaGrid, maximized: options?.maximized, + viewOutlets: options?.viewOutlets, + viewStates: options?.viewStates, })); } } diff --git "a/projects/scion/workbench/src/lib/layout/\311\265workbench-layout.ts" "b/projects/scion/workbench/src/lib/layout/\311\265workbench-layout.ts" index 3a128a314..4bfbeef31 100644 --- "a/projects/scion/workbench/src/lib/layout/\311\265workbench-layout.ts" +++ "b/projects/scion/workbench/src/lib/layout/\311\265workbench-layout.ts" @@ -8,14 +8,22 @@ * SPDX-License-Identifier: EPL-2.0 */ import {MPart, MPartGrid, MTreeNode, MView, ɵMPartGrid} from './workbench-layout.model'; -import {VIEW_ID_PREFIX} from '../workbench.constants'; import {assertType} from '../common/asserts.util'; -import {UUID} from '@scion/toolkit/uuid'; +import {randomUUID, UUID} from '../common/uuid.util'; import {MAIN_AREA, ReferencePart, WorkbenchLayout} from './workbench-layout'; import {WorkbenchLayoutSerializer} from './workench-layout-serializer.service'; import {WorkbenchViewRegistry} from '../view/workbench-view.registry'; import {WorkbenchPartRegistry} from '../part/workbench-part.registry'; -import {inject, Injectable, InjectionToken, Injector, runInInjectionContext} from '@angular/core'; +import {inject, Injectable, InjectionToken, Injector, Predicate, runInInjectionContext} from '@angular/core'; +import {RouterUtils} from '../routing/router.util'; +import {Commands, ViewOutlets, ViewState, ViewStates} from '../routing/routing.model'; +import {ActivatedRoute, UrlSegment} from '@angular/router'; +import {ViewId} from '../view/workbench-view.model'; +import {Arrays} from '@scion/toolkit/util'; +import {UrlSegmentMatcher} from '../routing/url-segment-matcher'; +import {Objects} from '../common/objects.util'; +import {WorkbenchLayouts} from './workbench-layouts.util'; +import {Logger} from '../logging'; /** * @inheritDoc @@ -33,6 +41,8 @@ import {inject, Injectable, InjectionToken, Injector, runInInjectionContext} fro export class ɵWorkbenchLayout implements WorkbenchLayout { private readonly _grids: Grids; + private readonly _viewOutlets: Map; + private readonly _viewStates: Map; private readonly _gridNames: Array; private readonly _partActivationInstantProvider = inject(PartActivationInstantProvider); private readonly _viewActivationInstantProvider = inject(ViewActivationInstantProvider); @@ -42,39 +52,86 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { private _maximized: boolean; /** @internal **/ - constructor(config: {workbenchGrid?: string | MPartGrid | null; mainAreaGrid?: string | MPartGrid | null; maximized?: boolean}) { + constructor(config: {workbenchGrid?: string | MPartGrid | null; mainAreaGrid?: string | MPartGrid | null; viewOutlets?: string | ViewOutlets | null; viewStates?: ViewStates | null; maximized?: boolean}) { this._grids = { - workbench: coerceMPartGrid(config.workbenchGrid ?? createDefaultWorkbenchGrid()), + workbench: coerceMPartGrid(config.workbenchGrid, {default: createDefaultWorkbenchGrid}), }; if (this.hasPart(MAIN_AREA, {grid: 'workbench'})) { - this._grids.mainArea = coerceMPartGrid(config.mainAreaGrid ?? createInitialMainAreaGrid()); + this._grids.mainArea = coerceMPartGrid(config.mainAreaGrid, {default: createInitialMainAreaGrid}); } - this._gridNames = Object.keys(this._grids) as Array; + this._gridNames = Objects.keys(this._grids); this._maximized = config.maximized ?? false; + this._viewOutlets = new Map(Objects.entries(coerceViewOutlets(config.viewOutlets))); + this._viewStates = new Map(Objects.entries(config.viewStates ?? {})); this.parts().forEach(part => assertType(part, {toBeOneOf: [MTreeNode, MPart]})); } /** - * Reference to the grid of the workbench. + * Reference to the main workbench grid. */ public get workbenchGrid(): Readonly<ɵMPartGrid> { return this._grids.workbench; } /** - * Reference to the grid of the main area, if any. + * Reference to the main area grid, if any. * - * The main area grid is a sub-grid included by the main area part, if any. It defines the arrangement of parts in the main area. + * The main area grid is a sub-grid included by the {@link MAIN_AREA} part. It defines the arrangement of parts in the main area. */ public get mainAreaGrid(): Readonly<ɵMPartGrid> | null { return this._grids.mainArea ?? null; } /** - * Tests if given part is contained in specified grid. + * Tests if given part is contained in the specified grid. + */ + public hasPart(id: string, options?: {grid?: keyof Grids}): boolean { + return this.part({partId: id, grid: options?.grid}, {orElse: null}) !== null; + } + + /** + * Tests if given view is contained in the specified grid. */ - public hasPart(partId: string, options?: {grid?: keyof Grids}): boolean { - return this.part({by: {partId}, grid: options?.grid}, {orElse: null}) !== null; + public hasView(id: string, options?: {grid?: keyof Grids}): boolean { + return this.views({grid: options?.grid, id: id}).length > 0; + } + + /** + * Finds the URL of views based on the specified filter. + * + * @param findBy - Defines the search scope. + * @param findBy.grid - Searches for views contained in the specified grid. + * @return outlets of views matching the filter criteria. + */ + public viewOutlets(findBy?: {grid?: keyof Grids}): ViewOutlets { + const viewOutletEntries = this.views({grid: findBy?.grid}).map(view => [view.id, this._viewOutlets.get(view.id) ?? []]); + return Object.fromEntries(viewOutletEntries); + } + + /** + * Finds the navigational state of views based on the specified filter. + * + * @param findBy - Defines the search scope. + * @param findBy.grid - Searches for views contained in the specified grid. + * @return view state matching the filter criteria. + */ + public viewStates(findBy?: {grid?: keyof Grids}): ViewStates { + const viewStateEntries = this.views({grid: findBy?.grid}).map(view => [view.id, this._viewStates.get(view.id) ?? {}]); + return Object.fromEntries(viewStateEntries); + } + + /** + * Finds the navigational state of specified view. + */ + public viewState(findBy: {viewId: ViewId}): ViewState { + return this._viewStates.get(findBy.viewId) ?? {}; + } + + /** + * Finds the URL of specified view. + */ + public urlSegments(findBy: {viewId: ViewId}): UrlSegment[] { + return this._viewOutlets.get(findBy.viewId) ?? []; } /** @@ -83,7 +140,9 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { * @return a copy of this layout with the maximization changed. */ public toggleMaximized(): ɵWorkbenchLayout { - return this.workingCopy().__toggleMaximized(); + const workingCopy = this.workingCopy(); + workingCopy.__toggleMaximized(); + return workingCopy; } /** @@ -94,49 +153,48 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { } /** - * Returns parts contained in the specified grid, or parts in any grid if not specifying a search grid. + * Finds parts based on the specified filter. * - * @param find - Search constraints - * @property grid - Limits the search scope. If not specified, all grids are searched. + * @param findBy - Defines the search scope. + * @param findBy.grid - Searches for parts contained in the specified grid. * @return parts matching the filter criteria. */ - public parts(find?: {grid?: keyof Grids}): readonly MPart[] { - return this.findTreeElements((element: MTreeNode | MPart): element is MPart => element instanceof MPart, {grid: find?.grid}); + public parts(findBy?: {grid?: keyof Grids}): readonly MPart[] { + return this.findTreeElements((element: MTreeNode | MPart): element is MPart => element instanceof MPart, {grid: findBy?.grid}); } /** - * Returns the part matching the given criteria. If not found, by default, throws an error unless setting the `orElseNull` option. + * Finds a part based on the specified filter. If not found, by default, throws an error unless setting the `orElseNull` option. * - * @param find - Search constraints - * @property by - * @property partId - If specified, searches the part of given identity. - * @property viewId - If specified, searches the part that contains given view. - * @property grid - Limits the search scope. If not specified, all grids are searched. - * @param options - Search options - * @property orElse - If set, returns `null` instead of throwing an error if no part is found. + * @param findBy - Defines the search scope. + * @param findBy.partId - Searches for a part with the specified id. + * @param findBy.viewId - Searches for a part that contains the specified view. + * @param findBy.grid - Searches for a part contained in the specified grid. + * @param options - Controls the search. + * @param options.orElse - Controls to return `null` instead of throwing an error if no part is found. * @return part matching the filter criteria. */ - public part(find: {by: {partId?: string; viewId?: string}; grid?: keyof Grids}): MPart; - public part(find: {by: {partId?: string; viewId?: string}; grid?: keyof Grids}, options: {orElse: null}): MPart | null; - public part(find: {by: {partId?: string; viewId?: string}; grid?: keyof Grids}, options?: {orElse: null}): MPart | null { - if (!find.by.partId && !find.by.viewId) { - throw Error('[IllegalArgumentError] Missing required argument. Specify either "partId" or "viewId".'); + public part(findBy: {partId?: string; viewId?: string; grid?: keyof Grids}): MPart; + public part(findBy: {partId?: string; viewId?: string; grid?: keyof Grids}, options: {orElse: null}): MPart | null; + public part(findBy: {partId?: string; viewId?: string; grid?: keyof Grids}, options?: {orElse: null}): MPart | null { + if (!findBy.partId && !findBy.viewId) { + throw Error(`[PartFindError] Missing required argument. Specify either 'partId' or 'viewId'.`); } const part = this.findTreeElements((element: MTreeNode | MPart): element is MPart => { if (!(element instanceof MPart)) { return false; } - if (find.by.partId && element.id !== find.by.partId) { + if (findBy.partId !== undefined && element.id !== findBy.partId) { return false; } - if (find.by.viewId && !element.views.some(view => view.id === find.by.viewId)) { + if (findBy.viewId !== undefined && !element.views.some(matchViewById(findBy.viewId!))) { return false; } return true; - }, {findFirst: true, grid: find.grid})[0]; + }, {findFirst: true, grid: findBy.grid})[0]; if (!part && !options) { - throw Error(`[NullPartError] No part found matching "${JSON.stringify(find)}".`); + throw Error(`[NullPartError] No matching part found: [${stringifyFilter(findBy)}]`); } return part ?? null; } @@ -146,19 +204,23 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { * * @param id - @inheritDoc * @param relativeTo - @inheritDoc - * @param options - Controls how to add the part to the layout. - * @property structural - Specifies whether this is a structural part. A structural part is not removed - * from the layout when removing its last view. If not set, defaults to `true`. + * @param options - @inheritDoc + * @param options.activate - @inheritDoc + * @param options.structural - Specifies if this is a structural part. A structural part will not be removed when removing its last view. Default is `true`. */ public addPart(id: string | MAIN_AREA, relativeTo: ReferenceElement, options?: {activate?: boolean; structural?: boolean}): ɵWorkbenchLayout { - return this.workingCopy().__addPart(id, relativeTo, options); + const workingCopy = this.workingCopy(); + workingCopy.__addPart(id, relativeTo, options); + return workingCopy; } /** * @inheritDoc */ public removePart(id: string): ɵWorkbenchLayout { - return this.workingCopy().__removePart(id); + const workingCopy = this.workingCopy(); + workingCopy.__removePart(id); + return workingCopy; } /** @@ -171,149 +233,228 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { if (!grid) { return null; } - return this.part({by: {partId: grid.activePartId}, grid: find.grid}); + return this.part({partId: grid.activePartId, grid: find.grid}); } /** * @inheritDoc */ public activatePart(id: string): ɵWorkbenchLayout { - return this.workingCopy().__activatePart(id); + const workingCopy = this.workingCopy(); + workingCopy.__activatePart(id); + return workingCopy; + } + + /** + * Finds a view based on the specified filter. If not found, by default, throws an error unless setting the `orElseNull` option. + * + * @param findBy - Defines the search scope. + * @param findBy.viewId - Searches for a view with the specified id. + * @param options - Controls the search. + * @param options.orElse - Controls to return `null` instead of throwing an error if no view is found. + * @return view matching the filter criteria. + */ + public view(findBy: {viewId: ViewId}): MView; + public view(findBy: {viewId: ViewId}, options: {orElse: null}): MView | null; + public view(findBy: {viewId: ViewId}, options?: {orElse: null}): MView | null { + const view = this.views({id: findBy.viewId}).at(0); + if (!view && !options) { + throw Error(`[NullViewError] No view found with id '${findBy.viewId}'.`); + } + return view ?? null; } /** - * Returns views contained in the specified grid, or views in any grid if not specifying any. + * Finds views based on the specified filter. * - * @param find - Search constraints - * @property grid - Limits the search scope. If not specified, all grids are searched. - * @return views maching the filter criteria. + * @param findBy - Defines the search scope. + * @param findBy.id - Searches for views with the specified id. + * @param findBy.partId - Searches for views contained in the specified part. + * @param findBy.segments - Searches for views navigated to the specified URL. + * @param findBy.navigationHint - Searches for views navigated with given hint. Passing `null` searches for views navigated without a hint. + * @param findBy.markedForRemoval - Searches for views marked (or not marked) for removal. + * @param findBy.grid - Searches for views contained in the specified grid. + * @param options - Controls the search. + * @param options.orElse - Controls to error if no view is found. + * @return views matching the filter criteria. */ - public views(find?: {grid?: keyof Grids}): readonly MView[] { - return this.parts(find).reduce((views, part) => views.concat(part.views), new Array()); + public views(findBy?: {id?: string; partId?: string; segments?: UrlSegmentMatcher; navigationHint?: string | null; markedForRemoval?: boolean; grid?: keyof Grids}, options?: {orElse: 'throwError'}): readonly MView[] { + const views = this.parts({grid: findBy?.grid}) + .filter(part => { + if (findBy?.partId !== undefined && part.id !== findBy.partId) { + return false; + } + return true; + }) + .flatMap(part => part.views) + .filter(view => { + if (findBy?.id !== undefined && !matchViewById(findBy.id)(view)) { + return false; + } + if (findBy?.segments && !findBy.segments.matches(this.urlSegments({viewId: view.id}))) { + return false; + } + if (findBy?.navigationHint !== undefined && findBy.navigationHint !== (view.navigation?.hint ?? null)) { + return false; + } + if (findBy?.markedForRemoval !== undefined && findBy.markedForRemoval !== (view.markedForRemoval ?? false)) { + return false; + } + return true; + }); + + if (findBy && !views.length && options) { + throw Error(`[NullViewError] No matching view found: [${stringifyFilter(findBy)}]`); + } + return views; } /** * @inheritDoc */ - public addView(id: string, options: {partId: string; position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; activateView?: boolean; activatePart?: boolean}): ɵWorkbenchLayout { - return this.workingCopy().__addView(id, options); + public addView(id: string, options: {partId: string; position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; activateView?: boolean; activatePart?: boolean; cssClass?: string | string[]}): ɵWorkbenchLayout { + const workingCopy = this.workingCopy(); + if (WorkbenchLayouts.isViewId(id)) { + workingCopy.__addView({id, uid: randomUUID()}, options); + } + else { + workingCopy.__addView({id: this.computeNextViewId(), alternativeId: id, uid: randomUUID()}, options); + } + return workingCopy; } /** * @inheritDoc */ - public removeView(id: string, options?: {grid?: keyof Grids}): ɵWorkbenchLayout { - return this.workingCopy().__removeView(id, options); + public navigateView(id: string, commands: Commands, extras?: {hint?: string; relativeTo?: ActivatedRoute | null; state?: ViewState; cssClass?: string | string[]}): ɵWorkbenchLayout { + const workingCopy = this.workingCopy(); + workingCopy.views({id}, {orElse: 'throwError'}).forEach(view => workingCopy.__navigateView(view, commands, extras)); + return workingCopy; } /** - * Moves a view to a different part or moves it within a part. - * - * @param id - The id of the view to be moved. - * @param targetPartId - The id of the part to which to move the view. - * @param options - Controls how to move the view in the layout. - * @property position - Specifies the position where to move the view in the target part. The position is zero-based. If not set and moving the view to a different part, adds it at the end. - * @property activateView - Controls whether to activate the view. If not set, defaults to `false`. - * @property activatePart - Controls whether to activate the target part. If not set, defaults to `false`. + * @inheritDoc * - * @return a copy of this layout with the view moved. + * @param id - @inheritDoc + * @param options - Controls removal of the view. + * @param options.force - Specifies whether to force remove the view, bypassing `CanClose` guard. + */ + public removeView(id: string, options?: {force?: boolean}): ɵWorkbenchLayout { + const workingCopy = this.workingCopy(); + workingCopy.views({id}, {orElse: 'throwError'}).forEach(view => workingCopy.__removeView(view, {force: options?.force})); + return workingCopy; + } + + /** + * Removes views marked for removal, optionally invoking the passed `CanClose` function to decide whether to remove a view. + */ + public async removeViewsMarkedForRemoval(canCloseFn?: (viewUid: UUID) => Promise | boolean): Promise<ɵWorkbenchLayout> { + const workingCopy = this.workingCopy(); + for (const view of workingCopy.views({markedForRemoval: true})) { + if (!canCloseFn || await canCloseFn(view.uid)) { + workingCopy.__removeView(view, {force: true}); + } + } + return workingCopy; + } + + /** + * @inheritDoc */ public moveView(id: string, targetPartId: string, options?: {position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; activateView?: boolean; activatePart?: boolean}): ɵWorkbenchLayout { - return this.workingCopy().__moveView(id, targetPartId, options); + const workingCopy = this.workingCopy(); + workingCopy.views({id}, {orElse: 'throwError'}).forEach(view => workingCopy.__moveView(view, targetPartId, options)); + return workingCopy; } /** * @inheritDoc */ public activateView(id: string, options?: {activatePart?: boolean}): ɵWorkbenchLayout { - return this.workingCopy().__activateView(id, options); + const workingCopy = this.workingCopy(); + workingCopy.views({id}, {orElse: 'throwError'}).forEach(view => workingCopy.__activateView(view, options)); + return workingCopy; } /** * Activates the preceding view if it exists, or the subsequent view otherwise. * - * @param id - The id of the view for which to activate its adjacent view. + * @param id - The id of the view to activate its adjacent view. * @param options - Controls view activation. - * @property activatePart - Controls whether to activate the part. If not set, defaults to `false`. + * @param options.activatePart - Controls if to activate the part. Default is `false`. * @return a copy of this layout with the adjacent view activated. */ public activateAdjacentView(id: string, options?: {activatePart?: boolean}): ɵWorkbenchLayout { - return this.workingCopy().__activateAdjacentView(id, options); + const workingCopy = this.workingCopy(); + workingCopy.views({id}, {orElse: 'throwError'}).forEach(view => workingCopy.__activateAdjacentView(view, options)); + return workingCopy; } /** - * Gives a view a new identity. + * Renames a view. * - * @param id - The id of the view which to give a new identity. + * @param id - The id of the view which to rename. * @param newViewId - The new identity of the view. - * @param options - Controls how to locate the view. - * @property grid - Grid to constrain where to find the view for rename. * @return a copy of this layout with the view renamed. */ - public renameView(id: string, newViewId: string, options?: {grid?: keyof Grids}): ɵWorkbenchLayout { - return this.workingCopy().__renameView(id, newViewId, options); + public renameView(id: ViewId, newViewId: ViewId): ɵWorkbenchLayout { + const workingCopy = this.workingCopy(); + workingCopy.__renameView(workingCopy.view({viewId: id}), newViewId); + return workingCopy; } /** - * Serializes this layout into a URL-safe base64 string. + * Sets the split ratio for the two children of a {@link MTreeNode}. + * + * @param nodeId - The id of the node to set the split ratio for. + * @param ratio - The proportional size between the two children, expressed as closed interval [0,1]. + * Example: To give 1/3 of the space to the first child, set the ratio to `0.3`. + * @return a copy of this layout with the split ratio set. */ - public serialize(): {workbenchGrid: string; mainAreaGrid: string | null} { - const isMainAreaEmpty = (this.mainAreaGrid?.root instanceof MPart && this.mainAreaGrid.root.views.length === 0) ?? true; - return { - workbenchGrid: this._serializer.serialize(this.workbenchGrid), - mainAreaGrid: isMainAreaEmpty ? null : this._serializer.serialize(this._grids.mainArea), - }; + public setSplitRatio(nodeId: string, ratio: number): ɵWorkbenchLayout { + const workingCopy = this.workingCopy(); + workingCopy.__setSplitRatio(nodeId, ratio); + return workingCopy; } /** - * Computes the next available view id to be the target of a primary route. - * - * @see VIEW_ID_PREFIX + * Serializes this layout into a URL-safe base64 string. */ - public computeNextViewId(): string { - const ids = this.views() - .filter(view => view.id.startsWith(VIEW_ID_PREFIX)) - .map(view => Number(view.id.substring(VIEW_ID_PREFIX.length))) - .reduce((set, viewId) => set.add(viewId), new Set()); - - for (let i = 1; i <= ids.size; i++) { - if (!ids.has(i)) { - return VIEW_ID_PREFIX.concat(`${i}`); - } - } - return VIEW_ID_PREFIX.concat(`${ids.size + 1}`); + public serialize(): SerializedWorkbenchLayout { + const isMainAreaEmpty = (this.mainAreaGrid?.root instanceof MPart && this.mainAreaGrid.root.views.length === 0) ?? true; + return { + workbenchGrid: this._serializer.serializeGrid(this.workbenchGrid), + mainAreaGrid: isMainAreaEmpty ? null : this._serializer.serializeGrid(this._grids.mainArea), + workbenchViewOutlets: this._serializer.serializeViewOutlets(this.viewOutlets({grid: 'workbench'})), + mainAreaViewOutlets: this._serializer.serializeViewOutlets(this.viewOutlets({grid: 'mainArea'})), + }; } /** - * Sets the split ratio for the two children of a {@link MTreeNode}. - * - * @param nodeId - The id of the node to set the split ratio for. - * @param ratio - The proportional size between the two children, expressed as closed interval [0,1]. - * Example: To give 1/3 of the space to the first child, set the ratio to `0.3`. - * @return a copy of this layout with the split ratio set. + * Computes the next available view id. */ - public setSplitRatio(nodeId: string, ratio: number): ɵWorkbenchLayout { - return this.workingCopy().__setSplitRatio(nodeId, ratio); + public computeNextViewId(): ViewId { + return WorkbenchLayouts.computeNextViewId(this.views().map(view => view.id)); } /** * Note: This method name begins with underscores, indicating that it does not operate on a working copy, but modifies this layout instead. */ - private __addPart(id: string, relativeTo: ReferenceElement, options?: {activate?: boolean; structural?: boolean}): this { + private __addPart(id: string, relativeTo: ReferenceElement, options?: {activate?: boolean; structural?: boolean}): void { if (this.hasPart(id)) { - throw Error(`[IllegalArgumentError] Part id must be unique. The layout already contains a part with the id '${id}'.`); + throw Error(`[PartAddError] Part id must be unique. The layout already contains a part with the id '${id}'.`); } - const newPart = new MPart({id, structural: options?.structural ?? true}); + const newPart = new MPart({id, structural: options?.structural ?? true, views: []}); // Find the reference element, if specified, or use the layout root as reference otherwise. - const referenceElement = relativeTo.relativeTo ? this.element({by: {id: relativeTo.relativeTo}}) : this.workbenchGrid.root; + const referenceElement = relativeTo.relativeTo ? this.findTreeElement({id: relativeTo.relativeTo}) : this.workbenchGrid.root; const addBefore = relativeTo.align === 'left' || relativeTo.align === 'top'; const ratio = relativeTo.ratio ?? .5; // Create a new tree node. const newTreeNode: MTreeNode = new MTreeNode({ - nodeId: UUID.randomUUID(), + nodeId: randomUUID(), child1: addBefore ? newPart : referenceElement, child2: addBefore ? referenceElement : newPart, direction: relativeTo.align === 'left' || relativeTo.align === 'right' ? 'row' : 'column', @@ -323,7 +464,7 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { // Add the tree node to the layout. if (!referenceElement.parent) { - this.grid({by: {element: referenceElement}}).root = newTreeNode; // top-level node + this.grid({element: referenceElement}).root = newTreeNode; // top-level node } else if (referenceElement.parent.child1 === referenceElement) { referenceElement.parent.child1 = newTreeNode; @@ -338,22 +479,20 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { if (options?.activate) { this.__activatePart(newPart.id); } - - return this; } /** * Note: This method name begins with underscores, indicating that it does not operate on a working copy, but modifies this layout instead. */ - private __removePart(id: string): this { - const part = this.part({by: {partId: id}}); - const grid = this.grid({by: {element: part}}); + private __removePart(id: string): void { + const part = this.part({partId: id}); + const grid = this.grid({element: part}); const gridName = this._gridNames.find(gridName => this._grids[gridName] === grid); // The last part is never removed. const parts = this.parts({grid: gridName}); if (parts.length === 1) { - return this; + return; } // Remove the part. @@ -369,6 +508,12 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { part.parent!.parent.child2 = siblingElement; } + // Remove outlets and states of views contained in the part. + part.views.forEach(view => { + this._viewOutlets.delete(view.id); + this._viewStates.delete(view.id); + }); + // If the removed part was the active part, make the last used part the active part. if (grid.activePartId === id) { const activePart = parts @@ -380,49 +525,77 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { })[0]; grid.activePartId = activePart!.id; } - - return this; } /** * Note: This method name begins with underscores, indicating that it does not operate on a working copy, but modifies this layout instead. */ - private __addView(id: string, options: {partId: string; position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; activateView?: boolean; activatePart?: boolean}): this { - if (this.views().find(view => view.id === id)) { - throw Error(`[IllegalArgumentError] View id must be unique. The layout already contains a view with the id '${id}'.`); + private __addView(view: MView, options: {partId: string; position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; activateView?: boolean; activatePart?: boolean; cssClass?: string | string[]}): void { + if (this.hasView(view.id)) { + throw Error(`[ViewAddError] View id must be unique. The layout already contains a view with the id '${view.id}'.`); } - const part = this.part({by: {partId: options.partId}}); + const part = this.part({partId: options.partId}); const position = coercePosition(options.position ?? 'end', part); - part.views.splice(position, 0, {id}); + part.views.splice(position, 0, view); - // Activate view and part. if (options.activateView) { - this.__activateView(id); + this.__activateView(view); } if (options.activatePart) { this.__activatePart(options.partId); } - return this; + if (options.cssClass) { + view.cssClass = Arrays.coerce(options.cssClass); + } } /** * Note: This method name begins with underscores, indicating that it does not operate on a working copy, but modifies this layout instead. */ - private __moveView(id: string, targetPartId: string, options?: {position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; activateView?: boolean; activatePart?: boolean}): this { - const sourcePart = this.part({by: {viewId: id}}); - const targetPart = this.part({by: {partId: targetPartId}}); + private __navigateView(view: MView, commands: Commands, extras?: {hint?: string; relativeTo?: ActivatedRoute | null; state?: ViewState; cssClass?: string | string[]}): void { + if (!commands.length && !extras?.hint && !extras?.relativeTo) { + throw Error('[NavigateError] Commands, relativeTo or hint must be set.'); + } + + const urlSegments = runInInjectionContext(this._injector, () => RouterUtils.commandsToSegments(commands, {relativeTo: extras?.relativeTo})); + if (urlSegments.length) { + this._viewOutlets.set(view.id, urlSegments); + } + else { + this._viewOutlets.delete(view.id); + } + + if (extras?.state && Objects.keys(extras.state).length) { + this._viewStates.set(view.id, extras.state); + } + else { + this._viewStates.delete(view.id); + } + + view.navigation = Objects.withoutUndefinedEntries({ + hint: extras?.hint, + cssClass: extras?.cssClass ? Arrays.coerce(extras.cssClass) : undefined, + }); + } + + /** + * Note: This method name begins with underscores, indicating that it does not operate on a working copy, but modifies this layout instead. + */ + private __moveView(view: MView, targetPartId: string, options?: {position?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'; activateView?: boolean; activatePart?: boolean}): void { + const sourcePart = this.part({viewId: view.id}); + const targetPart = this.part({partId: targetPartId}); // Move the view. if (sourcePart !== targetPart) { - this.__removeView(id); - this.__addView(id, {partId: targetPartId, position: options?.position}); + this.__removeView(view, {removeOutlet: false, removeState: false, force: true}); + this.__addView(view, {partId: targetPartId, position: options?.position}); } else if (options?.position !== undefined) { const position = coercePosition(options.position, targetPart); const referenceView: MView | undefined = sourcePart.views.at(position); - sourcePart.views.splice(sourcePart.views.findIndex(view => view.id === id), 1); - sourcePart.views.splice(referenceView ? sourcePart.views.indexOf(referenceView) : sourcePart.views.length, 0, {id}); + sourcePart.views.splice(sourcePart.views.indexOf(view), 1); + sourcePart.views.splice(referenceView ? sourcePart.views.indexOf(referenceView) : sourcePart.views.length, 0, view); } // Activate view and part. @@ -430,137 +603,148 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { this.__activatePart(targetPartId); } if (options?.activateView) { - this.__activateView(id); + this.__activateView(view); } - - return this; } /** * Note: This method name begins with underscores, indicating that it does not operate on a working copy, but modifies this layout instead. */ - private __removeView(id: string, options?: {grid?: keyof Grids}): this { - const part = this.part({by: {viewId: id}, grid: options?.grid}); + private __removeView(view: MView, options?: {removeOutlet?: false; removeState?: false; force?: boolean}): void { + if (!options?.force) { + view.markedForRemoval = true; + return; + } - // Remove the view. - const viewIndex = part.views.findIndex(view => view.id === id); - if (viewIndex === -1) { - throw Error(`[IllegalArgumentError] View not found in the part [part=${part.id}, view=${id}]`); + const part = this.part({viewId: view.id}); + + // Remove view. + part.views.splice(part.views.indexOf(view), 1); + + // Remove outlet. + if (options?.removeOutlet ?? true) { + this._viewOutlets.delete(view.id); } - part.views.splice(viewIndex, 1); + // Remove state. + if (options?.removeState ?? true) { + this._viewStates.delete(view.id); + } // Activate the last used view if this view was active. - if (part.activeViewId === id) { + if (part.activeViewId === view.id) { part.activeViewId = part.views .map(view => view.id) .sort((viewId1, viewId2) => { const activationInstantView1 = this._viewActivationInstantProvider.getActivationInstant(viewId1); const activationInstantView2 = this._viewActivationInstantProvider.getActivationInstant(viewId2); return activationInstantView2 - activationInstantView1; - })[0]; + }).at(0); } // Remove the part if this is the last view of the part and not a structural part. if (part.views.length === 0 && !part.structural) { this.__removePart(part.id); } - - return this; } /** * Note: This method name begins with underscores, indicating that it does not operate on a working copy, but modifies this layout instead. */ - private __activateView(id: string, options?: {activatePart?: boolean}): this { + private __activateView(view: MView, options?: {activatePart?: boolean}): void { // Activate the view. - const part = this.part({by: {viewId: id}}); - part.activeViewId = id; + const part = this.part({viewId: view.id}); + part.activeViewId = view.id; // Activate the part. if (options?.activatePart) { this.__activatePart(part.id); } - return this; } /** * Note: This method name begins with underscores, indicating that it does not operate on a working copy, but modifies this layout instead. */ - private __activateAdjacentView(id: string, options?: {activatePart?: boolean}): this { - const part = this.part({by: {viewId: id}}); - const viewIndex = part.views.findIndex(view => view.id === id); + private __activateAdjacentView(view: MView, options?: {activatePart?: boolean}): void { + const part = this.part({viewId: view.id}); + const viewIndex = part.views.indexOf(view); part.activeViewId = (part.views[viewIndex - 1] || part.views[viewIndex + 1])?.id; // is `undefined` if it is the last view of the part // Activate the part. if (options?.activatePart) { this.__activatePart(part.id); } - return this; } /** * Note: This method name begins with underscores, indicating that it does not operate on a working copy, but modifies this layout instead. */ - private __setSplitRatio(nodeId: string, ratio: number): this { + private __setSplitRatio(nodeId: string, ratio: number): void { if (ratio < 0 || ratio > 1) { - throw Error(`[IllegalArgumentError] Ratio for node '${nodeId}' must be in the closed interval [0,1], but was '${ratio}'.`); + throw Error(`[LayoutModifyError] Ratio for node '${nodeId}' must be in the closed interval [0,1], but was '${ratio}'.`); } - this.node({by: {nodeId}}).ratio = ratio; - return this; + this.findTreeElement({id: nodeId}).ratio = ratio; } /** * Note: This method name begins with underscores, indicating that it does not operate on a working copy, but modifies this layout instead. */ - private __activatePart(id: string): this { - const part = this.part({by: {partId: id}}); - this.grid({by: {element: part}}).activePartId = id; - - return this; + private __activatePart(id: string): void { + const part = this.part({partId: id}); + this.grid({element: part}).activePartId = id; } /** * Note: This method name begins with underscores, indicating that it does not operate on a working copy, but modifies this layout instead. */ - private __toggleMaximized(): this { + private __toggleMaximized(): void { this._maximized = !this._maximized; - return this; } /** * Note: This method name begins with underscores, indicating that it does not operate on a working copy, but modifies this layout instead. */ - private __renameView(id: string, newViewId: string, options?: {grid?: keyof Grids}): this { - if (this.views().find(view => view.id === newViewId)) { - throw Error(`[IllegalArgumentError] View id must be unique. The layout already contains a view with the id '${newViewId}'.`); + private __renameView(view: MView, newViewId: ViewId): void { + if (this.hasView(newViewId)) { + throw Error(`[ViewRenameError] View id must be unique. The layout already contains a view with the id '${newViewId}'.`); } - const part = this.part({by: {viewId: id}, grid: options?.grid}); - const viewIndex = part.views.findIndex(view => view.id === id); - part.views[viewIndex] = {...part.views[viewIndex], id: newViewId}; + const part = this.part({viewId: view.id}); + + if (this._viewOutlets.has(view.id)) { + this._viewOutlets.set(newViewId, this._viewOutlets.get(view.id)!); + this._viewOutlets.delete(view.id); + } + if (this._viewStates.has(view.id)) { + this._viewStates.set(newViewId, this._viewStates.get(view.id)!); + this._viewStates.delete(view.id); + } - if (part.activeViewId === id) { + if (part.activeViewId === view.id) { part.activeViewId = newViewId; } - return this; + view.id = newViewId; } /** - * Returns the grid that contains the given element. If not found, throws an error. + * Finds a grid based on the specified filter. If not found, throws an error. + * + * @param findBy - Defines the search scope. + * @param findBy.element - Searches for a grid that contains the specified element. + * @return Grid matching the filter criteria. */ - private grid(find: {by: {element: MPart | MTreeNode}}): MPartGrid { + private grid(findBy: {element: MPart | MTreeNode}): MPartGrid { const gridName = this._gridNames.find(gridName => { - return this.findTreeElements((element: MTreeNode | MPart): element is MPart | MTreeNode => element === find.by.element, {findFirst: true, grid: gridName}).length > 0; + return this.findTreeElements((element: MTreeNode | MPart): element is MPart | MTreeNode => element === findBy.element, {findFirst: true, grid: gridName}).length > 0; }); if (!gridName) { - if (find.by.element instanceof MPart) { - throw Error(`[NullGridError] No grid found that contains the part '${find.by.element.id}'".`); + if (findBy.element instanceof MPart) { + throw Error(`[NullGridError] No grid found that contains the part '${findBy.element.id}'".`); } else { - throw Error(`[NullGridError] No grid found that contains the node '${find.by.element.nodeId}'".`); + throw Error(`[NullGridError] No grid found that contains the node '${findBy.element.nodeId}'".`); } } @@ -568,47 +752,31 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { } /** - * Returns the element of given id. If not found, by default, throws an error unless setting the `orElseNull` option. + * Traverses the tree to find an element that matches the given predicate. * - * @param find - Search constraints - * @property by - * @property id - Specifies the identity of the element. - * @property grid - Limits the search scope. If not specified, all grids are searched. - * @param options - Search options - * @property orElse - If set, returns `null` instead of throwing an error if no element is found. - * @return part maching the filter criteria. + * @param findBy - Defines the search scope. + * @param findBy.id - Searches for an element with the specified id. + * @return Element matching the filter criteria. */ - private element(find: {by: {id: string}; grid?: keyof Grids}): MPart | MTreeNode; - private element(find: {by: {id: string}; grid?: keyof Grids}, options: {orElse: null}): MPart | MTreeNode | null; - private element(find: {by: {id: string}; grid?: keyof Grids}, options?: {orElse: null}): MPart | MTreeNode | null { - const element = this.findTreeElements((element: MTreeNode | MPart): element is MPart | MTreeNode => { - return element instanceof MPart ? element.id === find.by.id : element.nodeId === find.by.id; - }, {findFirst: true, grid: find.grid})[0]; + private findTreeElement(findBy: {id: string}): T { + const element = this.findTreeElements((element: MTreeNode | MPart): element is T => { + return element instanceof MPart ? element.id === findBy.id : element.nodeId === findBy.id; + }, {findFirst: true}).at(0); - if (!element && !options) { - throw Error(`[NullElementError] Element with id '${find.by.id}' not found in the layout.`); + if (!element) { + throw Error(`[NullElementError] No element found with id '${findBy.id}'.`); } - return element ?? null; - } - - /** - * Returns the node of given id. If not found, throws an error. - */ - private node(find: {by: {nodeId: string}}): MTreeNode { - const node = this.findTreeElements((element: MTreeNode | MPart): element is MTreeNode => element instanceof MTreeNode && element.nodeId === find.by.nodeId, {findFirst: true})[0]; - if (!node) { - throw Error(`[NullNodeError] Node '${find.by.nodeId}' not found.`); - } - return node; + return element; } /** * Traverses the tree to find elements that match the given predicate. * * @param predicateFn - Predicate function to match. - * @param options - Search options - * @property findFirst - If specified, stops traversing on first match. If not set, defaults to `false`. - * @property grid - Limits the search scope. If not specified, all grids are searched. + * @param options - Defines search scope and options. + * @param options.findFirst - If specified, stops traversing on first match. If not set, defaults to `false`. + * @param options.grid - Searches for an element contained in the specified grid. + * @return Elements matching the filter criteria. */ private findTreeElements(predicateFn: (element: MTreeNode | MPart) => element is T, options?: {findFirst?: boolean; grid?: keyof Grids}): T[] { if (options?.grid && !this._grids[options.grid]) { @@ -650,8 +818,10 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { */ private workingCopy(): ɵWorkbenchLayout { return runInInjectionContext(this._injector, () => new ɵWorkbenchLayout({ - workbenchGrid: this._serializer.serialize(this.workbenchGrid, {includeNodeId: true}), - mainAreaGrid: this._serializer.serialize(this._grids.mainArea, {includeNodeId: true}), + workbenchGrid: this._serializer.serializeGrid(this.workbenchGrid, {includeNodeId: true, includeUid: true, includeMarkedForRemovalFlag: true}), + mainAreaGrid: this._serializer.serializeGrid(this._grids.mainArea, {includeNodeId: true, includeUid: true, includeMarkedForRemovalFlag: true}), + viewOutlets: Object.fromEntries(this._viewOutlets), + viewStates: Object.fromEntries(this._viewStates), maximized: this._maximized, })); } @@ -662,7 +832,7 @@ export class ɵWorkbenchLayout implements WorkbenchLayout { */ function createDefaultWorkbenchGrid(): MPartGrid { return { - root: new MPart({id: MAIN_AREA, structural: true}), + root: new MPart({id: MAIN_AREA, structural: true, views: []}), activePartId: MAIN_AREA, }; } @@ -674,7 +844,7 @@ function createDefaultWorkbenchGrid(): MPartGrid { */ function createInitialMainAreaGrid(): MPartGrid { return { - root: new MPart({id: inject(MAIN_AREA_INITIAL_PART_ID)}), + root: new MPart({id: inject(MAIN_AREA_INITIAL_PART_ID), structural: false, views: []}), activePartId: inject(MAIN_AREA_INITIAL_PART_ID), }; } @@ -682,11 +852,41 @@ function createInitialMainAreaGrid(): MPartGrid { /** * Coerces {@link MPartGrid}, applying necessary migrations if the serialized grid is outdated. */ -function coerceMPartGrid(grid: string | MPartGrid): ɵMPartGrid { - if (typeof grid === 'string') { - return {...inject(WorkbenchLayoutSerializer).deserialize(grid)}; +function coerceMPartGrid(grid: string | MPartGrid | null | undefined, options: {default: () => MPartGrid}): ɵMPartGrid { + grid ??= options.default(); + + if (typeof grid === 'object') { + return grid; + } + + try { + return inject(WorkbenchLayoutSerializer).deserializeGrid(grid); + } + catch (error) { + inject(Logger).error('[SerializeError] Failed to deserialize workbench layout. Please clear your browser storage and reload the application.', error); + return {...options.default(), migrated: true}; + } +} + +/** + * Coerces {@link ViewOutlets}, applying necessary migrations if the serialized outlets are outdated. + */ +function coerceViewOutlets(viewOutlets: string | ViewOutlets | null | undefined): ViewOutlets { + if (!viewOutlets) { + return {}; + } + + if (typeof viewOutlets === 'object') { + return viewOutlets; + } + + try { + return inject(WorkbenchLayoutSerializer).deserializeViewOutlets(viewOutlets); + } + catch (error) { + inject(Logger).error('[SerializeError] Failed to deserialize view outlets. Please clear your browser storage and reload the application.', error); + return {}; } - return {...grid, migrated: false}; } /** @@ -705,19 +905,6 @@ interface Grids { mainArea?: ɵMPartGrid; } -/** - * Tests if the given {@link MTreeNode} or {@link MPart} is visible. - * - * - A part is considered visible if it is the main area part or has at least one view. - * - A node is considered visible if it has at least one visible part in its child hierarchy. - */ -export function isGridElementVisible(element: MTreeNode | MPart): boolean { - if (element instanceof MPart) { - return element.id === MAIN_AREA || element.views.length > 0; - } - return isGridElementVisible(element.child1) || isGridElementVisible(element.child2); -} - /** * Returns the position if a number, or computes it from the given literal otherwise. */ @@ -767,7 +954,7 @@ export interface ReferenceElement extends ReferencePart { */ export const MAIN_AREA_INITIAL_PART_ID = new InjectionToken('MAIN_AREA_INITIAL_PART_ID', { providedIn: 'root', - factory: () => UUID.randomUUID(), + factory: () => randomUUID(), }); /** @@ -809,7 +996,36 @@ export class ViewActivationInstantProvider { /** * Returns the instant when the specified view was last activated. */ - public getActivationInstant(viewId: string): number { + public getActivationInstant(viewId: ViewId): number { return this._viewRegistry.get(viewId, {orElse: null})?.activationInstant ?? 0; } } + +/** + * Creates a predicate to match a view by its primary or alternative id, depending on the type of the passed id. + */ +function matchViewById(id: string): Predicate { + if (WorkbenchLayouts.isViewId(id)) { + return view => view.id === id; + } + else { + return view => view.alternativeId === id; + } +} + +/** + * Stringifies the given filter to be used in error messages. + */ +function stringifyFilter(filter: {[property: string]: unknown}): string { + return Object.entries(filter).map(([key, value]) => `${key}=${value}`).join(', '); +} + +/** + * Serialized artifacts of the workbench layout. + */ +export interface SerializedWorkbenchLayout { + workbenchGrid: string; + workbenchViewOutlets: string; + mainAreaGrid: string | null; + mainAreaViewOutlets: string; +} diff --git a/projects/scion/workbench/src/lib/logging/logging-support.ts b/projects/scion/workbench/src/lib/logging/logging-support.ts index 85e331347..1ddcbf21b 100644 --- a/projects/scion/workbench/src/lib/logging/logging-support.ts +++ b/projects/scion/workbench/src/lib/logging/logging-support.ts @@ -9,19 +9,19 @@ */ import {EnvironmentProviders, makeEnvironmentProviders, Provider, Type} from '@angular/core'; -import {WorkbenchModuleConfig} from '../workbench-module-config'; +import {WorkbenchConfig} from '../workbench-config'; import {LogAppender, LogLevel} from './logging.model'; import {ConsoleAppender} from './console-appender.service'; /** * Provides a set of DI providers for installing workbench logging. */ -export function provideLogging(workbenchModuleConfig: WorkbenchModuleConfig): EnvironmentProviders { - const logAppenders: Type[] = workbenchModuleConfig.logging?.logAppenders || [ConsoleAppender]; +export function provideLogging(workbenchConfig: WorkbenchConfig): EnvironmentProviders { + const logAppenders: Type[] = workbenchConfig.logging?.logAppenders || [ConsoleAppender]; return makeEnvironmentProviders([ { provide: LogLevel, - useValue: workbenchModuleConfig.logging?.logLevel ?? LogLevel.INFO, + useValue: workbenchConfig.logging?.logLevel ?? LogLevel.INFO, }, logAppenders.map((logAppender: Type): Provider => { return { diff --git a/projects/scion/workbench/src/lib/message-box/message-box-footer/message-box-footer.component.ts b/projects/scion/workbench/src/lib/message-box/message-box-footer/message-box-footer.component.ts index 69a17b374..9b47ba5d0 100644 --- a/projects/scion/workbench/src/lib/message-box/message-box-footer/message-box-footer.component.ts +++ b/projects/scion/workbench/src/lib/message-box/message-box-footer/message-box-footer.component.ts @@ -7,12 +7,11 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {Component, ElementRef, EventEmitter, HostBinding, inject, Input, NgZone, Output, QueryList, ViewChildren} from '@angular/core'; +import {Component, ElementRef, EventEmitter, HostBinding, inject, Input, Output, QueryList, ViewChildren} from '@angular/core'; import {KeyValuePipe} from '@angular/common'; -import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {observeOn} from 'rxjs/operators'; +import {animationFrameScheduler, firstValueFrom} from 'rxjs'; import {fromDimension$} from '@scion/toolkit/observable'; -import {take, tap} from 'rxjs/operators'; -import {observeInside} from '@scion/toolkit/operators'; @Component({ selector: 'wb-message-box-footer', @@ -42,7 +41,7 @@ export class MessageBoxFooterComponent { public preferredSizeChange = new EventEmitter(); constructor() { - this.emitPreferredSize(); + this.emitPreferredSize().then(); } protected insertionSortOrderFn = (): number => 0; @@ -58,22 +57,15 @@ export class MessageBoxFooterComponent { actionButtons[((newIndex + actionButtonCount) % actionButtonCount)].nativeElement.focus(); } - private emitPreferredSize(): void { - const host = inject(ElementRef).nativeElement; - const zone = inject(NgZone); - - fromDimension$(host) - .pipe( - observeInside(fn => zone.run(fn)), /* fromDimension$ emits outside the Angular zone */ - tap({ - subscribe: () => host.classList.add('calculating-min-width'), - finalize: () => host.classList.remove('calculating-min-width'), - }), - take(1), - takeUntilDestroyed(), - ) - .subscribe(dimension => { - this.preferredSizeChange.emit(dimension.offsetWidth); - }); + private async emitPreferredSize(): Promise { + const host = inject(ElementRef).nativeElement; + host.classList.add('calculating-min-width'); + try { + const initialSize = await firstValueFrom(fromDimension$(host).pipe(observeOn(animationFrameScheduler))); + this.preferredSizeChange.emit(initialSize.offsetWidth); + } + finally { + host.classList.remove('calculating-min-width'); + } } } diff --git a/projects/scion/workbench/src/lib/message-box/workbench-message-box.options.ts b/projects/scion/workbench/src/lib/message-box/workbench-message-box.options.ts index d6f790cc0..e51547bcf 100644 --- a/projects/scion/workbench/src/lib/message-box/workbench-message-box.options.ts +++ b/projects/scion/workbench/src/lib/message-box/workbench-message-box.options.ts @@ -8,6 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 */ import {Injector} from '@angular/core'; +import {ViewId} from '../view/workbench-view.model'; /** * Controls the appearance and behavior of a message box. @@ -47,7 +48,7 @@ export interface WorkbenchMessageBoxOptions { * Controls which area of the application to block by the message box. * * - **Application-modal:** - * Use to block the workbench, or the browser's viewport if configured in {@link WorkbenchModuleConfig.dialog.modalityScope}. + * Use to block the workbench, or the browser's viewport if configured in {@link WorkbenchConfig.dialog.modalityScope}. * * - **View-modal:** * Use to block only the contextual view of the message box, allowing the user to interact with other views. @@ -92,7 +93,7 @@ export interface WorkbenchMessageBoxOptions { injector?: Injector; /** - * Specifies CSS class(es) to be added to the message box, useful in end-to-end tests for locating the message box. + * Specifies CSS class(es) to add to the message box, e.g., to locate the message box in tests. */ cssClass?: string | string[]; @@ -105,6 +106,6 @@ export interface WorkbenchMessageBoxOptions { * * By default, if opening the message box in the context of a view, that view is used as the contextual view. */ - viewId?: string; + viewId?: ViewId; }; } diff --git a/projects/scion/workbench/src/lib/message-box/workbench-message-box.service.ts b/projects/scion/workbench/src/lib/message-box/workbench-message-box.service.ts index 652bf0637..144299322 100644 --- a/projects/scion/workbench/src/lib/message-box/workbench-message-box.service.ts +++ b/projects/scion/workbench/src/lib/message-box/workbench-message-box.service.ts @@ -22,7 +22,7 @@ import {ɵWorkbenchMessageBoxService} from './ɵworkbench-message-box.service'; * * A message box can be view-modal or application-modal. A view-modal message box blocks only a specific view, * allowing the user to interact with other views. An application-modal message box blocks the workbench, - * or the browser's viewport if configured in {@link WorkbenchModuleConfig.dialog.modalityScope}. + * or the browser's viewport if configured in {@link WorkbenchConfig.dialog.modalityScope}. * * ## Stacking * Multiple message boxes are stacked, and only the topmost message box in each modality stack can be interacted with. diff --git "a/projects/scion/workbench/src/lib/message-box/\311\265workbench-message-box.service.ts" "b/projects/scion/workbench/src/lib/message-box/\311\265workbench-message-box.service.ts" index 020159c49..8d4222e1e 100644 --- "a/projects/scion/workbench/src/lib/message-box/\311\265workbench-message-box.service.ts" +++ "b/projects/scion/workbench/src/lib/message-box/\311\265workbench-message-box.service.ts" @@ -27,7 +27,7 @@ export class ɵWorkbenchMessageBoxService implements WorkbenchMessageBoxService public async open(message: string | ComponentType, options?: WorkbenchMessageBoxOptions): Promise { // Ensure to run in Angular zone to display the message box even if called from outside the Angular zone, e.g. from an error handler. if (!NgZone.isInAngularZone()) { - return this._zone.run(() => this.open(message)); + return this._zone.run(() => this.open(message, options)); } return (await this._workbenchDialogService.open(WorkbenchMessageBoxComponent, { diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/initialization/microfrontend-platform-initializer.service.spec.ts b/projects/scion/workbench/src/lib/microfrontend-platform/initialization/microfrontend-platform-initializer.service.spec.ts index 3d065c7cc..096d0882d 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/initialization/microfrontend-platform-initializer.service.spec.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/initialization/microfrontend-platform-initializer.service.spec.ts @@ -14,9 +14,9 @@ import {MicrofrontendViewIntentHandler} from '../routing/microfrontend-view-inte import {MicrofrontendPopupIntentHandler} from '../microfrontend-popup/microfrontend-popup-intent-handler.interceptor'; import {MICROFRONTEND_PLATFORM_PRE_STARTUP, WorkbenchInitializer} from '../../startup/workbench-initializer'; import {TestBed} from '@angular/core/testing'; -import {WorkbenchTestingModule} from '../../testing/workbench-testing.module'; -import {RouterTestingModule} from '@angular/router/testing'; import {WorkbenchLauncher} from '../../startup/workbench-launcher.service'; +import {provideRouter} from '@angular/router'; +import {provideWorkbenchForTest} from '../../testing/workbench.provider'; describe('Microfrontend Platform Initializer', () => { @@ -37,11 +37,9 @@ describe('Microfrontend Platform Initializer', () => { // Configure and start the SCION Workbench. TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({microfrontendPlatform: {applications: []}}), - RouterTestingModule.withRoutes([]), - ], providers: [ + provideWorkbenchForTest({microfrontendPlatform: {applications: []}}), + provideRouter([]), {provide: MICROFRONTEND_PLATFORM_PRE_STARTUP, multi: true, useClass: CustomIntentInterceptorRegisterer}, ], }); diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-host-dialog/microfrontend-host-dialog.component.ts b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-host-dialog/microfrontend-host-dialog.component.ts index 5e0c25ab1..e96fdd545 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-host-dialog/microfrontend-host-dialog.component.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-host-dialog/microfrontend-host-dialog.component.ts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {Component, inject, Injector, Input, OnDestroy, OnInit, StaticProvider} from '@angular/core'; +import {Component, inject, Injector, Input, OnDestroy, OnInit, runInInjectionContext, StaticProvider} from '@angular/core'; import {WorkbenchDialog as WorkbenchClientDialog, WorkbenchDialogCapability} from '@scion/workbench-client'; import {RouterUtils} from '../../routing/router.util'; import {Commands} from '../../routing/routing.model'; @@ -72,7 +72,7 @@ export class MicrofrontendHostDialogComponent implements OnDestroy, OnInit { private navigate(path: string | null, extras?: {params?: Map}): Promise { path = Microfrontends.substituteNamedParameters(path, extras?.params); - const outletCommands: Commands | null = (path !== null ? RouterUtils.segmentsToCommands(RouterUtils.parsePath(this._router, path)) : null); + const outletCommands: Commands | null = (path !== null ? runInInjectionContext(this._injector, () => RouterUtils.pathToCommands(path!)) : null); const commands: Commands = [{outlets: {[this.outletName]: outletCommands}}]; return this._router.navigate(commands, {skipLocationChange: true, queryParamsHandling: 'preserve'}); } diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-host-popup/microfrontend-host-popup.component.ts b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-host-popup/microfrontend-host-popup.component.ts index 6ba8bab53..257282b3f 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-host-popup/microfrontend-host-popup.component.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-host-popup/microfrontend-host-popup.component.ts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {Component, inject, Injector, OnDestroy, StaticProvider} from '@angular/core'; +import {Component, inject, Injector, OnDestroy, runInInjectionContext, StaticProvider} from '@angular/core'; import {WorkbenchPopup, ɵPopupContext} from '@scion/workbench-client'; import {RouterUtils} from '../../routing/router.util'; import {Commands} from '../../routing/routing.model'; @@ -41,7 +41,7 @@ export class MicrofrontendHostPopupComponent implements OnDestroy { public readonly outletInjector: Injector; constructor(popup: Popup<ɵPopupContext>, - injector: Injector, + private _injector: Injector, private _router: Router) { const popupContext = popup.input!; const capability = popupContext.capability; @@ -49,7 +49,7 @@ export class MicrofrontendHostPopupComponent implements OnDestroy { const params = popupContext.params; this.outletName = POPUP_ID_PREFIX.concat(popupContext.popupId); this.outletInjector = Injector.create({ - parent: injector, + parent: this._injector, providers: [provideWorkbenchPopupHandle(popupContext)], }); @@ -67,7 +67,7 @@ export class MicrofrontendHostPopupComponent implements OnDestroy { private navigate(path: string | null, extras: {outletName: string; params?: Map}): Promise { path = Microfrontends.substituteNamedParameters(path, extras.params); - const outletCommands: Commands | null = (path !== null ? RouterUtils.segmentsToCommands(RouterUtils.parsePath(this._router, path)) : null); + const outletCommands: Commands | null = (path !== null ? runInInjectionContext(this._injector, () => RouterUtils.pathToCommands(path!)) : null); const commands: Commands = [{outlets: {[extras.outletName]: outletCommands}}]; return this._router.navigate(commands, {skipLocationChange: true, queryParamsHandling: 'preserve'}); } diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-platform-lifecycle.spec.ts b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-platform-lifecycle.spec.ts index 2779d3a84..7536b4dbc 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-platform-lifecycle.spec.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-platform-lifecycle.spec.ts @@ -11,20 +11,18 @@ import {TestBed} from '@angular/core/testing'; import {PlatformRef} from '@angular/core'; import {MicrofrontendPlatform, PlatformState} from '@scion/microfrontend-platform'; -import {WorkbenchTestingModule} from '../testing/workbench-testing.module'; -import {RouterTestingModule} from '@angular/router/testing'; import {WorkbenchLauncher} from '../startup/workbench-launcher.service'; import {waitForInitialWorkbenchLayout} from '../testing/testing.util'; +import {provideWorkbenchForTest} from '../testing/workbench.provider'; +import {provideRouter} from '@angular/router'; describe('Microfrontend Platform Lifecycle', () => { it('should destroy SCION Microfrontend Platform when destroying the Angular platform', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({ - microfrontendPlatform: {applications: []}, - }), - RouterTestingModule.withRoutes([]), + providers: [ + provideWorkbenchForTest({microfrontendPlatform: {applications: []}}), + provideRouter([]), ], }); diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view-command-handler.service.ts b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view-command-handler.service.ts index 64caa4454..322320f4e 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view-command-handler.service.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view-command-handler.service.ts @@ -11,7 +11,7 @@ import {Injectable, OnDestroy} from '@angular/core'; import {Message, MessageClient, MessageHeaders} from '@scion/microfrontend-platform'; import {Logger} from '../../logging'; -import {WorkbenchView} from '../../view/workbench-view.model'; +import {ViewId, WorkbenchView} from '../../view/workbench-view.model'; import {WorkbenchViewRegistry} from '../../view/workbench-view.registry'; import {map, switchMap} from 'rxjs/operators'; import {ɵWorkbenchCommands} from '@scion/workbench-client'; @@ -61,7 +61,7 @@ export class MicrofrontendViewCommandHandler implements OnDestroy { */ private installViewTitleCommandHandler(): Subscription { return this._messageClient.onMessage(ɵWorkbenchCommands.viewTitleTopic(':viewId'), message => { - const viewId = message.params!.get('viewId')!; + const viewId = message.params!.get('viewId') as ViewId; this.runIfPrivileged(viewId, message, view => { view.title = message.body; }); @@ -73,7 +73,7 @@ export class MicrofrontendViewCommandHandler implements OnDestroy { */ private installViewHeadingCommandHandler(): Subscription { return this._messageClient.onMessage(ɵWorkbenchCommands.viewHeadingTopic(':viewId'), message => { - const viewId = message.params!.get('viewId')!; + const viewId = message.params!.get('viewId') as ViewId; this.runIfPrivileged(viewId, message, view => { view.heading = message.body; }); @@ -85,7 +85,7 @@ export class MicrofrontendViewCommandHandler implements OnDestroy { */ private installViewDirtyCommandHandler(): Subscription { return this._messageClient.onMessage(ɵWorkbenchCommands.viewDirtyTopic(':viewId'), message => { - const viewId = message.params!.get('viewId')!; + const viewId = message.params!.get('viewId') as ViewId; this.runIfPrivileged(viewId, message, view => { view.dirty = message.body; }); @@ -97,7 +97,7 @@ export class MicrofrontendViewCommandHandler implements OnDestroy { */ private installViewClosableCommandHandler(): Subscription { return this._messageClient.onMessage(ɵWorkbenchCommands.viewClosableTopic(':viewId'), message => { - const viewId = message.params!.get('viewId')!; + const viewId = message.params!.get('viewId') as ViewId; this.runIfPrivileged(viewId, message, view => { view.closable = message.body; }); @@ -109,7 +109,7 @@ export class MicrofrontendViewCommandHandler implements OnDestroy { */ private installViewCloseCommandHandler(): Subscription { return this._messageClient.onMessage(ɵWorkbenchCommands.viewCloseTopic(':viewId'), message => { - const viewId = message.params!.get('viewId')!; + const viewId = message.params!.get('viewId') as ViewId; this.runIfPrivileged(viewId, message, view => { view.close().then(); }); @@ -120,7 +120,7 @@ export class MicrofrontendViewCommandHandler implements OnDestroy { * Runs the given runnable only if the microfrontend displayed in the view is actually provided by the sender, * thus preventing other apps from updating other apps' views. */ - private runIfPrivileged(viewId: string, message: Message, runnable: (view: WorkbenchView) => void): void { + private runIfPrivileged(viewId: ViewId, message: Message, runnable: (view: WorkbenchView) => void): void { const view = this._viewRegistry.get(viewId); const sender = message.headers.get(MessageHeaders.AppSymbolicName); if (view.adapt(MicrofrontendWorkbenchView)?.capability.metadata!.appSymbolicName === sender) { diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.ts b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.ts index 0b3b68505..5d2305830 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/microfrontend-view/microfrontend-view.component.ts @@ -10,13 +10,13 @@ import {ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, DestroyRef, ElementRef, inject, Inject, Injector, OnDestroy, OnInit, Provider, runInInjectionContext, ViewChild} from '@angular/core'; import {ActivatedRoute, Params} from '@angular/router'; -import {combineLatest, EMPTY, firstValueFrom, Observable, of, Subject, switchMap} from 'rxjs'; -import {catchError, first, map, takeUntil} from 'rxjs/operators'; +import {combineLatest, firstValueFrom, Observable, of, Subject, switchMap} from 'rxjs'; +import {first, map, takeUntil} from 'rxjs/operators'; import {ManifestService, mapToBody, MessageClient, MessageHeaders, MicrofrontendPlatformConfig, OutletRouter, ResponseStatusCodes, SciRouterOutletElement, TopicMessage} from '@scion/microfrontend-platform'; import {WorkbenchViewCapability, ɵMicrofrontendRouteParams, ɵVIEW_ID_CONTEXT_KEY, ɵViewParamsUpdateCommand, ɵWorkbenchCommands} from '@scion/workbench-client'; import {Dictionaries, Maps} from '@scion/toolkit/util'; import {Logger, LoggerNames} from '../../logging'; -import {WorkbenchViewPreDestroy} from '../../workbench.model'; +import {CanClose} from '../../workbench.model'; import {IFRAME_HOST, ViewContainerReference} from '../../content-projection/view-container.reference'; import {serializeExecution} from '../../common/operators'; import {ɵWorkbenchView} from '../../view/ɵworkbench-view.model'; @@ -34,6 +34,7 @@ import {MicrofrontendSplashComponent} from '../microfrontend-splash/microfronten import {GLASS_PANE_BLOCKABLE, GlassPaneDirective} from '../../glass-pane/glass-pane.directive'; import {MicrofrontendWorkbenchView} from './microfrontend-workbench-view.model'; import {Microfrontends} from '../common/microfrontend.util'; +import {Objects} from '../../common/objects.util'; /** * Embeds the microfrontend of a view capability. @@ -55,7 +56,7 @@ import {Microfrontends} from '../common/microfrontend.util'; ], schemas: [CUSTOM_ELEMENTS_SCHEMA], // required because is a custom element }) -export class MicrofrontendViewComponent implements OnInit, OnDestroy, WorkbenchViewPreDestroy { +export class MicrofrontendViewComponent implements OnInit, OnDestroy, CanClose { private _unsubscribeParamsUpdater$ = new Subject(); private _universalKeystrokes = [ @@ -196,7 +197,7 @@ export class MicrofrontendViewComponent implements OnInit, OnDestroy, WorkbenchV const replyTo = request.headers.get(MessageHeaders.ReplyTo); try { - const success = await this._workbenchRouter.ɵnavigate(layout => { + const success = await this._workbenchRouter.navigate(layout => { // Cancel pending navigation if the subscription was closed, e.g., because closed the view or navigated to another capability if (subscription.closed) { return null; @@ -205,15 +206,16 @@ export class MicrofrontendViewComponent implements OnInit, OnDestroy, WorkbenchV const paramsHandling = request.body!.paramsHandling; const currentParams = this._route.snapshot.params; const newParams = Dictionaries.coerce(request.body!.params); // coerce params for backward compatibility - const mergedParams = Dictionaries.withoutUndefinedEntries(paramsHandling === 'merge' ? {...currentParams, ...newParams} : newParams); + const mergedParams = Objects.withoutUndefinedEntries(paramsHandling === 'merge' ? {...currentParams, ...newParams} : newParams); const {urlParams, transientParams} = MicrofrontendViewRoutes.splitParams(mergedParams, viewCapability); - return { - layout, - viewOutlets: {[this.view.id]: [urlParams]}, - viewStates: {[this.view.id]: {[MicrofrontendViewRoutes.STATE_TRANSIENT_PARAMS]: transientParams}}, - }; - }, {relativeTo: this._route}); + return layout.navigateView(this.view.id, [urlParams], { + relativeTo: this._route, + state: Objects.withoutUndefinedEntries({ + [MicrofrontendViewRoutes.STATE_TRANSIENT_PARAMS]: transientParams, + }), + }); + }); await this._messageClient.publish(replyTo, success, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); } @@ -246,23 +248,18 @@ export class MicrofrontendViewComponent implements OnInit, OnDestroy, WorkbenchV await firstValueFrom(viewParams$); } - /** - * Method invoked just before closing this view. - * - * If the embedded microfrontend has a listener installed to be notified when closing this view, - * initiates a request-reply communication, allowing the microfrontend to prevent this view from closing. - */ - public async onWorkbenchViewPreDestroy(): Promise { - const closingTopic = ɵWorkbenchCommands.viewClosingTopic(this.view.id); + /** @inheritDoc */ + public async canClose(): Promise { + const canCloseTopic = ɵWorkbenchCommands.canCloseTopic(this.view.id); + const legacyCanCloseTopic = ɵWorkbenchCommands.viewClosingTopic(this.view.id); - // Allow the microfrontend to prevent this view from closing. - const count = await firstValueFrom(this._messageClient.subscriberCount$(closingTopic)); - if (count === 0) { + // Initiate a request-response communication only if the embedded microfrontend implements `CanClose` guard. + const hasCanCloseGuard = await firstValueFrom(this._messageClient.subscriberCount$(canCloseTopic)) > 0; + const hasLegacyCanCloseGuard = await firstValueFrom(this._messageClient.subscriberCount$(legacyCanCloseTopic)) > 0; + if (!hasCanCloseGuard && !hasLegacyCanCloseGuard) { return true; } - - const doit = this._messageClient.request$(closingTopic).pipe(mapToBody(), catchError(() => EMPTY)); - return firstValueFrom(doit, {defaultValue: true}); + return firstValueFrom(this._messageClient.request$(hasCanCloseGuard ? canCloseTopic : legacyCanCloseTopic).pipe(mapToBody()), {defaultValue: true}); } public onFocusWithin(event: Event): void { diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-intent-handler.interceptor.ts b/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-intent-handler.interceptor.ts index f1c88f976..affb059bf 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-intent-handler.interceptor.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-intent-handler.interceptor.ts @@ -18,6 +18,7 @@ import {Beans} from '@scion/toolkit/bean-manager'; import {Arrays, Dictionaries} from '@scion/toolkit/util'; import {WorkbenchViewRegistry} from '../../view/workbench-view.registry'; import {MicrofrontendWorkbenchView} from '../microfrontend-view/microfrontend-workbench-view.model'; +import {Objects} from '../../common/objects.util'; /** * Handles microfrontend view intents, instructing the workbench to navigate to the microfrontend of the resolved capability. @@ -54,22 +55,23 @@ export class MicrofrontendViewIntentHandler implements IntentInterceptor { private async navigate(message: IntentMessage): Promise { const viewCapability = message.capability as WorkbenchViewCapability; const intent = message.intent; - const extras: WorkbenchNavigationExtras = message.body ?? {}; + // TODO [Angular 20] remove backward compatibility for property 'blankInsertionIndex' + const extras: WorkbenchNavigationExtras & {blankInsertionIndex?: number | 'start' | 'end' | 'before-active-view' | 'after-active-view'} = message.body ?? {}; - const intentParams = Dictionaries.withoutUndefinedEntries(Dictionaries.coerce(intent.params)); + const intentParams = Objects.withoutUndefinedEntries(Dictionaries.coerce(intent.params)); const {urlParams, transientParams} = MicrofrontendViewRoutes.splitParams(intentParams, viewCapability); const targets = this.resolveTargets(message, extras); - const routerNavigateCommand = extras.close ? [] : MicrofrontendViewRoutes.createMicrofrontendNavigateCommands(viewCapability.metadata!.id, urlParams); + const commands = extras.close ? [] : MicrofrontendViewRoutes.createMicrofrontendNavigateCommands(viewCapability.metadata!.id, urlParams); - this._logger.debug(() => `Navigating to: ${viewCapability.properties.path}`, LoggerNames.MICROFRONTEND_ROUTING, routerNavigateCommand, viewCapability, transientParams); + this._logger.debug(() => `Navigating to: ${viewCapability.properties.path}`, LoggerNames.MICROFRONTEND_ROUTING, commands, viewCapability, transientParams); const navigations = await Promise.all(Arrays.coerce(targets).map(target => { - return this._workbenchRouter.navigate(routerNavigateCommand, { + return this._workbenchRouter.navigate(commands, { target, activate: extras.activate, close: extras.close, - blankInsertionIndex: extras.blankInsertionIndex, + position: extras.position ?? extras.blankInsertionIndex, cssClass: extras.cssClass, - state: Dictionaries.withoutUndefinedEntries({ + state: Objects.withoutUndefinedEntries({ [MicrofrontendViewRoutes.STATE_TRANSIENT_PARAMS]: transientParams, }), }); @@ -83,7 +85,7 @@ export class MicrofrontendViewIntentHandler implements IntentInterceptor { private resolveTargets(intentMessage: IntentMessage, extras: WorkbenchNavigationExtras): string | string[] { // Closing a microfrontend view by viewId is not allowed, as this would violate the concept of intent-based view navigation. if (extras.close && extras.target) { - throw Error(`[WorkbenchRouterError][IllegalArgumentError] The target must be empty if closing a view [target=${(extras.target)}]`); + throw Error(`[NavigateError] The target must be empty if closing a view [target=${(extras.target)}]`); } if (extras.close) { return this.resolvePresentViewIds(intentMessage, {matchWildcardParams: true}) ?? []; diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-routes.ts b/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-routes.ts index a01fb8a45..eb8e008f5 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-routes.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/routing/microfrontend-view-routes.ts @@ -8,12 +8,13 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {Params, PRIMARY_OUTLET, Route, UrlMatcher, UrlMatchResult, UrlSegment, UrlSegmentGroup} from '@angular/router'; +import {Params, Route, UrlMatcher, UrlMatchResult, UrlSegment, UrlSegmentGroup} from '@angular/router'; import {WorkbenchViewCapability, ɵMicrofrontendRouteParams} from '@scion/workbench-client'; -import {inject, Injector, runInInjectionContext} from '@angular/core'; -import {RouterUtils} from '../../routing/router.util'; -import {WorkbenchNavigationalStates} from '../../routing/workbench-navigational-states'; +import {inject, Injector} from '@angular/core'; import {Commands} from '../../routing/routing.model'; +import {ɵWorkbenchRouter} from '../../routing/ɵworkbench-router.service'; +import {WorkbenchLayouts} from '../../layout/workbench-layouts.util'; +import {WorkbenchRouteData} from '../../routing/workbench-route-data'; /** * Provides functions and constants specific to microfrontend routes. @@ -47,14 +48,20 @@ export const MicrofrontendViewRoutes = { const injector = inject(Injector); return (segments: UrlSegment[], group: UrlSegmentGroup, route: Route): UrlMatchResult | null => { - if (!RouterUtils.isPrimaryRouteTarget(route.outlet ?? PRIMARY_OUTLET)) { + // Test if the path matches. + if (!MicrofrontendViewRoutes.isMicrofrontendRoute(segments)) { return null; } - if (!MicrofrontendViewRoutes.isMicrofrontendRoute(segments)) { + + // Test if navigating a view. + const outlet = route.data?.[WorkbenchRouteData.ɵoutlet]; + if (!WorkbenchLayouts.isViewId(outlet)) { return null; } - const viewState = runInInjectionContext(injector, () => WorkbenchNavigationalStates.resolveViewState(route.outlet!)); - const transientParams = viewState?.[MicrofrontendViewRoutes.STATE_TRANSIENT_PARAMS] ?? {}; + + const {layout} = injector.get(ɵWorkbenchRouter).getCurrentNavigationContext(); + const viewState = layout.viewState({viewId: outlet}); + const transientParams = viewState[MicrofrontendViewRoutes.STATE_TRANSIENT_PARAMS] ?? {}; const posParams = Object.entries(transientParams).map(([name, value]) => [name, new UrlSegment(value, {})]); return { diff --git a/projects/scion/workbench/src/lib/microfrontend-platform/workbench-microfrontend-support.ts b/projects/scion/workbench/src/lib/microfrontend-platform/workbench-microfrontend-support.ts index c789fa525..f3bdf4991 100644 --- a/projects/scion/workbench/src/lib/microfrontend-platform/workbench-microfrontend-support.ts +++ b/projects/scion/workbench/src/lib/microfrontend-platform/workbench-microfrontend-support.ts @@ -16,7 +16,7 @@ import {MICROFRONTEND_PLATFORM_POST_STARTUP, WORKBENCH_STARTUP} from '../startup import {Beans} from '@scion/toolkit/bean-manager'; import {WorkbenchDialogService, WorkbenchMessageBoxService, WorkbenchNotificationService, WorkbenchPopupService, WorkbenchRouter} from '@scion/workbench-client'; import {NgZoneObservableDecorator} from './initialization/ng-zone-observable-decorator'; -import {WorkbenchModuleConfig} from '../workbench-module-config'; +import {WorkbenchConfig} from '../workbench-config'; import {MicrofrontendViewCommandHandler} from './microfrontend-view/microfrontend-view-command-handler.service'; import {MicrofrontendMessageBoxIntentHandler} from './microfrontend-message-box/microfrontend-message-box-intent-handler.service'; import {MicrofrontendNotificationIntentHandler} from './microfrontend-notification/microfrontend-notification-intent-handler.service'; @@ -32,13 +32,14 @@ import {MicrofrontendPopupCapabilityValidator} from './microfrontend-popup/micro import {MicrofrontendDialogIntentHandler} from './microfrontend-dialog/microfrontend-dialog-intent-handler.interceptor'; import {MicrofrontendDialogCapabilityValidator} from './microfrontend-dialog/microfrontend-dialog-capability-validator.interceptor'; import {Defined} from '@scion/toolkit/util'; +import {canMatchWorkbenchView} from '../view/workbench-view-route-guards'; import './microfrontend-platform.config'; // DO NOT REMOVE to augment `MicrofrontendPlatformConfig` with `splash` property. /** * Provides a set of DI providers to set up microfrontend support in the workbench. */ -export function provideWorkbenchMicrofrontendSupport(workbenchModuleConfig: WorkbenchModuleConfig): EnvironmentProviders | [] { - if (!workbenchModuleConfig.microfrontendPlatform) { +export function provideWorkbenchMicrofrontendSupport(workbenchConfig: WorkbenchConfig): EnvironmentProviders | [] { + if (!workbenchConfig.microfrontendPlatform) { return []; } @@ -50,7 +51,7 @@ export function provideWorkbenchMicrofrontendSupport(workbenchModuleConfig: Work }, { provide: MicrofrontendPlatformConfigLoader, - useClass: typeof workbenchModuleConfig.microfrontendPlatform === 'function' ? workbenchModuleConfig.microfrontendPlatform : StaticMicrofrontendPlatformConfigLoader, + useClass: typeof workbenchConfig.microfrontendPlatform === 'function' ? workbenchConfig.microfrontendPlatform : StaticMicrofrontendPlatformConfigLoader, }, { provide: MICROFRONTEND_PLATFORM_POST_STARTUP, @@ -87,16 +88,16 @@ export function provideWorkbenchMicrofrontendSupport(workbenchModuleConfig: Work } /** - * Provides {@link WorkbenchModuleConfig.microfrontendPlatform} config as passed to {@link WorkbenchModule.forRoot}. + * Provides {@link WorkbenchConfig.microfrontendPlatform} config as passed to {@link provideWorkbench}. */ @Injectable(/* DO NOT PROVIDE via 'providedIn' metadata as registered under `MicrofrontendPlatformConfigLoader` DI token. */) class StaticMicrofrontendPlatformConfigLoader implements MicrofrontendPlatformConfigLoader { - constructor(private _workbenchModuleConfig: WorkbenchModuleConfig) { + constructor(private _workbenchConfig: WorkbenchConfig) { } public async load(): Promise { - return this._workbenchModuleConfig.microfrontendPlatform! as MicrofrontendPlatformConfig; + return this._workbenchConfig.microfrontendPlatform! as MicrofrontendPlatformConfig; } } @@ -138,6 +139,7 @@ function provideMicrofrontendRoute(): EnvironmentProviders { useFactory: (): Route => ({ matcher: MicrofrontendViewRoutes.provideMicrofrontendRouteMatcher(), component: MicrofrontendViewComponent, + canMatch: [canMatchWorkbenchView(true)], }), }, ]); diff --git a/projects/scion/workbench/src/lib/migration/workbench-migration.ts b/projects/scion/workbench/src/lib/migration/workbench-migration.ts new file mode 100644 index 000000000..46cff7a97 --- /dev/null +++ b/projects/scion/workbench/src/lib/migration/workbench-migration.ts @@ -0,0 +1,20 @@ +/* + * 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 + */ + +/** + * Represents a migration to migrate a workbench object to the next version. + */ +export interface WorkbenchMigration { + + /** + * Migrates serialized workbench data to the next version. + */ + migrate(json: string): string; +} diff --git a/projects/scion/workbench/src/lib/migration/workbench-migrator.ts b/projects/scion/workbench/src/lib/migration/workbench-migrator.ts new file mode 100644 index 000000000..088a69c25 --- /dev/null +++ b/projects/scion/workbench/src/lib/migration/workbench-migrator.ts @@ -0,0 +1,41 @@ +/* + * 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 + */ + +import {WorkbenchMigration} from './workbench-migration'; + +/** + * Migrates serialized workbench data to the latest version. + */ +export class WorkbenchMigrator { + + private _migrators = new Map(); + + /** + * Registers a migration from a specific version to the next version. + */ + public registerMigration(fromVersion: number, migration: WorkbenchMigration): this { + this._migrators.set(fromVersion, migration); + return this; + } + + /** + * Migrates serialized workbench data to the latest version. + */ + public migrate(json: string, version: {from: number; to: number}): string { + for (let v = version.from; v < version.to; v++) { + const migrator = this._migrators.get(v); + if (!migrator) { + throw Error(`[NullMigrationError] Cannot perform workbench data migration. No migration registered for version ${v}.`); + } + json = migrator.migrate(json); + } + return json; + } +} diff --git a/projects/scion/workbench/src/lib/notification/notification.config.ts b/projects/scion/workbench/src/lib/notification/notification.config.ts index 73e8c5ad6..ff43e6d7e 100644 --- a/projects/scion/workbench/src/lib/notification/notification.config.ts +++ b/projects/scion/workbench/src/lib/notification/notification.config.ts @@ -101,7 +101,7 @@ export interface NotificationConfig { groupInputReduceFn?: (prevInput: any, currInput: any) => any; /** - * Specifies CSS class(es) to be added to the notification, useful in end-to-end tests for locating the notification. + * Specifies CSS class(es) to add to the notification, e.g., to locate the notification in tests. */ cssClass?: string | string[]; } diff --git a/projects/scion/workbench/src/lib/notification/notification.service.ts b/projects/scion/workbench/src/lib/notification/notification.service.ts index a9515eabf..4bef13328 100644 --- a/projects/scion/workbench/src/lib/notification/notification.service.ts +++ b/projects/scion/workbench/src/lib/notification/notification.service.ts @@ -67,15 +67,15 @@ export class NotificationService { private addNotification(config: NotificationConfig): void { const notifications = [...this.notifications]; - const {insertionIndex, notification} = this.constructNotification(config, notifications); - notifications.splice(insertionIndex, 1, notification); + const {index, notification} = this.constructNotification(config, notifications); + notifications.splice(index, 1, notification); this._notifications$.next(notifications); } /** * Constructs the notification based on the given config and computes its insertion index. */ - private constructNotification(config: NotificationConfig, notifications: ɵNotification[]): {notification: ɵNotification; insertionIndex: number} { + private constructNotification(config: NotificationConfig, notifications: ɵNotification[]): {notification: ɵNotification; index: number} { config = {...config}; // Check whether the notification belongs to a group. If so, replace any present notification of that group. @@ -83,7 +83,7 @@ export class NotificationService { if (!group) { return { notification: new ɵNotification(config), - insertionIndex: notifications.length, + index: notifications.length, }; } @@ -92,7 +92,7 @@ export class NotificationService { if (index === -1) { return { notification: new ɵNotification(config), - insertionIndex: notifications.length, + index: notifications.length, }; } @@ -103,7 +103,7 @@ export class NotificationService { return { notification: new ɵNotification(config), - insertionIndex: index, + index: index, }; } diff --git a/projects/scion/workbench/src/lib/notification/notification.ts b/projects/scion/workbench/src/lib/notification/notification.ts index 19c2005a9..de3352a64 100644 --- a/projects/scion/workbench/src/lib/notification/notification.ts +++ b/projects/scion/workbench/src/lib/notification/notification.ts @@ -41,9 +41,7 @@ export abstract class Notification { public abstract setDuration(duration: 'short' | 'medium' | 'long' | 'infinite' | number): void; /** - * Specifies CSS class(es) to be added to the notification, useful in end-to-end tests for locating the notification. - * - * This operation is additive, that is, it does not override CSS classes set by the notification opener. + * Specifies CSS class(es) to add to the notification, e.g., to locate the notification in tests. */ public abstract setCssClass(cssClass: string | string[]): void; } diff --git a/projects/scion/workbench/src/lib/page-not-found/format-url.pipe.ts b/projects/scion/workbench/src/lib/page-not-found/format-url.pipe.ts new file mode 100644 index 000000000..dc3ba4af8 --- /dev/null +++ b/projects/scion/workbench/src/lib/page-not-found/format-url.pipe.ts @@ -0,0 +1,23 @@ +/* + * 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 + */ + +import {Pipe, PipeTransform} from '@angular/core'; +import {UrlSegment} from '@angular/router'; + +/** + * Formats given URL. + */ +@Pipe({name: 'appFormatUrl', standalone: true}) +export class FormatUrlPipe implements PipeTransform { + + public transform(url: UrlSegment[]): string { + return url.map(segment => segment.path).join('/'); + } +} diff --git a/projects/scion/workbench/src/lib/page-not-found/page-not-found.component.html b/projects/scion/workbench/src/lib/page-not-found/page-not-found.component.html new file mode 100644 index 000000000..c6992b333 --- /dev/null +++ b/projects/scion/workbench/src/lib/page-not-found/page-not-found.component.html @@ -0,0 +1,15 @@ +
Page Not Found
+ +
+ The requested page {{view.urlSegments | appFormatUrl}} was not found. +
+ The URL may have changed. Try to open the view again. +
+ + + +@if (isDevMode) { +
+ You can create a custom "Page Not Found" component and register it in the workbench configuration to personalize this page. +
+} diff --git a/projects/scion/workbench/src/lib/page-not-found/page-not-found.component.scss b/projects/scion/workbench/src/lib/page-not-found/page-not-found.component.scss new file mode 100644 index 000000000..5ada9eaf2 --- /dev/null +++ b/projects/scion/workbench/src/lib/page-not-found/page-not-found.component.scss @@ -0,0 +1,49 @@ +:host { + display: flex; + flex-direction: column; + gap: 2em; + padding: 1em; + align-items: center; + + > header { + font-weight: bold; + font-size: 1.3rem; + } + + > section.message { + text-align: center; + line-height: 1.75; + + > span.url { + font-weight: bold; + } + } + + > 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; + } + + > button { + all: unset; + cursor: pointer; + 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; + + &:focus, &:active { + border-color: transparent; + outline: 1px solid var(--sci-color-accent); + color: var(--sci-color-accent-inverse); + } + } +} diff --git a/projects/scion/workbench/src/lib/page-not-found/page-not-found.component.ts b/projects/scion/workbench/src/lib/page-not-found/page-not-found.component.ts new file mode 100644 index 000000000..683a6f073 --- /dev/null +++ b/projects/scion/workbench/src/lib/page-not-found/page-not-found.component.ts @@ -0,0 +1,28 @@ +/* + * 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 + */ + +import {Component, inject, isDevMode} from '@angular/core'; +import {WorkbenchView} from '../view/workbench-view.model'; +import {FormatUrlPipe} from './format-url.pipe'; + +@Component({ + selector: 'wb-page-not-found', + templateUrl: './page-not-found.component.html', + styleUrls: ['./page-not-found.component.scss'], + standalone: true, + imports: [ + FormatUrlPipe, + ], +}) +export default class PageNotFoundComponent { + + protected isDevMode = isDevMode(); + protected view = inject(WorkbenchView); +} diff --git a/projects/scion/workbench/src/lib/part/part-action-bar/part-action.directive.ts b/projects/scion/workbench/src/lib/part/part-action-bar/part-action.directive.ts index 2b0715ed5..cf1de8a0a 100644 --- a/projects/scion/workbench/src/lib/part/part-action-bar/part-action.directive.ts +++ b/projects/scion/workbench/src/lib/part/part-action-bar/part-action.directive.ts @@ -60,7 +60,7 @@ export class WorkbenchPartActionDirective implements OnInit, OnDestroy { public canMatch?: CanMatchPartFn; /** - * Specifies CSS class(es) to be associated with the action, useful in end-to-end tests for locating it. + * Specifies CSS class(es) to add to the action, e.g., to locate the action in tests. */ @Input() public cssClass?: string | string[] | undefined; diff --git a/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.ts b/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.ts index 975d6ef87..99c947de3 100644 --- a/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.ts +++ b/projects/scion/workbench/src/lib/part/part-bar/part-bar.component.ts @@ -19,7 +19,7 @@ import {getCssTranslation, setCssClass, setCssVariable, unsetCssClass, unsetCssV import {ɵWorkbenchPart} from '../ɵworkbench-part.model'; import {filterArray, mapArray, observeInside, subscribeInside} from '@scion/toolkit/operators'; import {SciViewportComponent} from '@scion/components/viewport'; -import {WorkbenchRouter} from '../../routing/workbench-router.service'; +import {ɵWorkbenchRouter} from '../../routing/ɵworkbench-router.service'; import {SciDimensionModule} from '@scion/components/dimension'; import {AsyncPipe, NgFor, NgIf} from '@angular/common'; import {PartActionBarComponent} from '../part-action-bar/part-action-bar.component'; @@ -27,6 +27,7 @@ import {ViewListButtonComponent} from '../view-list-button/view-list-button.comp import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {fromDimension$} from '@scion/toolkit/observable'; import {WORKBENCH_ID} from '../../workbench-id'; +import {ViewId} from '../../view/workbench-view.model'; /** * Renders view tabs and actions of a {@link WorkbenchPart}. @@ -139,7 +140,7 @@ export class PartBarComponent implements OnInit { constructor(host: ElementRef, @Inject(WORKBENCH_ID) private _workbenchId: string, private _workbenchLayoutService: WorkbenchLayoutService, - private _router: WorkbenchRouter, + private _router: ɵWorkbenchRouter, private _viewTabDragImageRenderer: ViewTabDragImageRenderer, private _part: ɵWorkbenchPart, private _viewDragService: ViewDragService, @@ -160,12 +161,12 @@ export class PartBarComponent implements OnInit { @HostListener('dblclick', ['$event']) public onDoubleClick(event: MouseEvent): void { if (this._part.isInMainArea) { - this._router.ɵnavigate(layout => layout.toggleMaximized()).then(); + this._router.navigate(layout => layout.toggleMaximized()).then(); } event.stopPropagation(); } - public get viewIds$(): Observable { + public get viewIds$(): Observable { return this._part.viewIds$; } @@ -276,7 +277,7 @@ export class PartBarComponent implements OnInit { // Activate the view next to the view being dragged out of this tabbar. But, do not push the navigation into browsing history stack. if (this.dragSourceViewTab && this.dragSourceViewTab.active) { - this._router.ɵnavigate(layout => layout.activateAdjacentView(this.dragSourceViewTab!.viewId), {skipLocationChange: true}).then(); + this._router.navigate(layout => layout.activateAdjacentView(this.dragSourceViewTab!.viewId), {skipLocationChange: true}).then(); } } @@ -303,7 +304,9 @@ export class PartBarComponent implements OnInit { workbenchId: this._dragData!.workbenchId, partId: this._dragData!.partId, viewId: this._dragData!.viewId, + alternativeViewId: this._dragData!.alternativeViewId, viewUrlSegments: this._dragData!.viewUrlSegments, + navigationHint: this._dragData!.navigationHint, classList: this._dragData!.classList, }, target: { diff --git a/projects/scion/workbench/src/lib/part/part.component.ts b/projects/scion/workbench/src/lib/part/part.component.ts index 63c8c20b0..3158a09f3 100644 --- a/projects/scion/workbench/src/lib/part/part.component.ts +++ b/projects/scion/workbench/src/lib/part/part.component.ts @@ -93,7 +93,9 @@ export class PartComponent implements OnInit, OnDestroy { workbenchId: event.dragData.workbenchId, partId: event.dragData.partId, viewId: event.dragData.viewId, + alternativeViewId: event.dragData.alternativeViewId, viewUrlSegments: event.dragData.viewUrlSegments, + navigationHint: event.dragData.navigationHint, classList: event.dragData.classList, }, target: { diff --git a/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.directive.ts b/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.directive.ts index 507eadab6..fb21f1a1e 100644 --- a/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.directive.ts +++ b/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.directive.ts @@ -49,7 +49,7 @@ export class WorkbenchViewMenuItemDirective implements OnDestroy { public disabled = false; /** - * Specifies CSS class(es) to be added to the menu item, useful in end-to-end tests for locating the menu item. + * Specifies CSS class(es) to add to the menu item, e.g., to locate the menu item in tests. */ @Input() public cssClass?: string | string[] | undefined; diff --git a/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.service.ts b/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.service.ts index e5da46cc4..af23088f6 100644 --- a/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.service.ts +++ b/projects/scion/workbench/src/lib/part/view-context-menu/view-menu.service.ts @@ -18,11 +18,11 @@ import {filter, map, switchMap, takeUntil} from 'rxjs/operators'; import {firstValueFrom, fromEvent, Observable, Subject, TeardownLogic} from 'rxjs'; import {coerceElement} from '@angular/cdk/coercion'; import {TEXT, TextComponent} from '../view-context-menu/text.component'; -import {MenuItemConfig, WorkbenchModuleConfig} from '../../workbench-module-config'; +import {MenuItemConfig, WorkbenchConfig} from '../../workbench-config'; import {WorkbenchService} from '../../workbench.service'; import {filterArray, observeInside, subscribeInside} from '@scion/toolkit/operators'; import {ɵWorkbenchView} from '../../view/ɵworkbench-view.model'; -import {WorkbenchView} from '../../view/workbench-view.model'; +import {ViewId, WorkbenchView} from '../../view/workbench-view.model'; import {provideViewContext} from '../../view/view-context-provider'; import {Arrays} from '@scion/toolkit/util'; @@ -42,7 +42,7 @@ export class ViewMenuService { private _zone: NgZone, private _viewRegistry: WorkbenchViewRegistry, private _workbenchService: WorkbenchService, - private _workbenchModuleConfig: WorkbenchModuleConfig) { + private _workbenchConfig: WorkbenchConfig) { // Registers built-in menu items added to the context menu of every view tab. this.registerCloseViewMenuItem(); this.registerCloseOtherViewsMenuItem(); @@ -61,7 +61,7 @@ export class ViewMenuService { * * @see {@link WorkbenchView.registerViewMenuItem} */ - public async showMenu(location: Point, viewId: string): Promise { + public async showMenu(location: Point, viewId: ViewId): Promise { const view = this._viewRegistry.get(viewId); const menuItems = await firstValueFrom(view.menuItems$); @@ -147,7 +147,7 @@ export class ViewMenuService { private registerCloseViewMenuItem(): void { const defaults: MenuItemConfig = {visible: true, text: 'Close tab', group: 'close', accelerator: ['ctrl', 'k'], cssClass: 'e2e-close-tab'}; - const appConfig: MenuItemConfig | undefined = this._workbenchModuleConfig.viewMenuItems?.close; + const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.close; const config = {...defaults, ...appConfig}; config.visible && this._workbenchService.registerViewMenuItem((view: WorkbenchView): WorkbenchMenuItem => { @@ -168,7 +168,7 @@ export class ViewMenuService { private registerCloseOtherViewsMenuItem(): void { const defaults: MenuItemConfig = {visible: true, text: 'Close other tabs', group: 'close', accelerator: ['ctrl', 'shift', 'k']}; - const appConfig: MenuItemConfig | undefined = this._workbenchModuleConfig.viewMenuItems?.closeOthers; + const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.closeOthers; const config = {...defaults, ...appConfig}; config.visible && this._workbenchService.registerViewMenuItem((view: WorkbenchView): WorkbenchMenuItem => { @@ -189,7 +189,7 @@ export class ViewMenuService { private registerCloseAllViewsMenuItem(): void { const defaults: MenuItemConfig = {visible: true, text: 'Close all tabs', group: 'close', accelerator: ['ctrl', 'shift', 'alt', 'k'], cssClass: 'e2e-close-all-tabs'}; - const appConfig: MenuItemConfig | undefined = this._workbenchModuleConfig.viewMenuItems?.closeAll; + const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.closeAll; const config = {...defaults, ...appConfig}; config.visible && this._workbenchService.registerViewMenuItem((view: WorkbenchView): WorkbenchMenuItem => { @@ -209,7 +209,7 @@ export class ViewMenuService { private registerCloseViewsToTheRightMenuItem(): void { const defaults: MenuItemConfig = {visible: true, text: 'Close tabs to the right', group: 'close'}; - const appConfig: MenuItemConfig | undefined = this._workbenchModuleConfig.viewMenuItems?.closeToTheRight; + const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.closeToTheRight; const config = {...defaults, ...appConfig}; config.visible && this._workbenchService.registerViewMenuItem((view: WorkbenchView): WorkbenchMenuItem => { @@ -230,7 +230,7 @@ export class ViewMenuService { private registerCloseViewsToTheLeftMenuItem(): void { const defaults: MenuItemConfig = {visible: true, text: 'Close tabs to the left', group: 'close'}; - const appConfig: MenuItemConfig | undefined = this._workbenchModuleConfig.viewMenuItems?.closeToTheLeft; + const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.closeToTheLeft; const config = {...defaults, ...appConfig}; config.visible && this._workbenchService.registerViewMenuItem((view: WorkbenchView): WorkbenchMenuItem => { @@ -251,7 +251,7 @@ export class ViewMenuService { private registerMoveRightMenuItem(): void { const defaults: MenuItemConfig = {visible: true, text: 'Move right', group: 'move', accelerator: ['ctrl', 'alt', 'end']}; - const appConfig: MenuItemConfig | undefined = this._workbenchModuleConfig.viewMenuItems?.moveRight; + const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.moveRight; const config = {...defaults, ...appConfig}; config.visible && this._workbenchService.registerViewMenuItem((view: WorkbenchView): WorkbenchMenuItem => { @@ -272,7 +272,7 @@ export class ViewMenuService { private registerMoveLeftMenuItem(): void { const defaults: MenuItemConfig = {visible: true, text: 'Move left', group: 'move'}; - const appConfig: MenuItemConfig | undefined = this._workbenchModuleConfig.viewMenuItems?.moveLeft; + const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.moveLeft; const config = {...defaults, ...appConfig}; config.visible && this._workbenchService.registerViewMenuItem((view: WorkbenchView): WorkbenchMenuItem => { @@ -293,7 +293,7 @@ export class ViewMenuService { private registerMoveUpMenuItem(): void { const defaults: MenuItemConfig = {visible: true, text: 'Move up', group: 'move'}; - const appConfig: MenuItemConfig | undefined = this._workbenchModuleConfig.viewMenuItems?.moveUp; + const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.moveUp; const config = {...defaults, ...appConfig}; config.visible && this._workbenchService.registerViewMenuItem((view: WorkbenchView): WorkbenchMenuItem => { @@ -314,7 +314,7 @@ export class ViewMenuService { private registerMoveDownMenuItem(): void { const defaults: MenuItemConfig = {visible: true, text: 'Move down', group: 'move'}; - const appConfig: MenuItemConfig | undefined = this._workbenchModuleConfig.viewMenuItems?.moveDown; + const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.moveDown; const config = {...defaults, ...appConfig}; config.visible && this._workbenchService.registerViewMenuItem((view: WorkbenchView): WorkbenchMenuItem => { @@ -335,7 +335,7 @@ export class ViewMenuService { private registerMoveToNewWindowMenuItem(): void { const defaults: MenuItemConfig = {visible: true, text: 'Move to new window', group: 'open', cssClass: 'e2e-move-to-new-window'}; - const appConfig: MenuItemConfig | undefined = this._workbenchModuleConfig.viewMenuItems?.moveToNewWindow; + const appConfig: MenuItemConfig | undefined = this._workbenchConfig.viewMenuItems?.moveToNewWindow; const config = {...defaults, ...appConfig}; config.visible && this._workbenchService.registerViewMenuItem((view: WorkbenchView): WorkbenchMenuItem => { diff --git a/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.ts b/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.ts index 594243a7f..132792095 100644 --- a/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.ts +++ b/projects/scion/workbench/src/lib/part/view-list-item/view-list-item.component.ts @@ -14,8 +14,8 @@ import {ɵWorkbenchView} from '../../view/ɵworkbench-view.model'; import {WorkbenchViewRegistry} from '../../view/workbench-view.registry'; import {ViewTabContentComponent} from '../view-tab-content/view-tab-content.component'; import {NgIf} from '@angular/common'; -import {WorkbenchModuleConfig} from '../../workbench-module-config'; -import {WorkbenchView} from '../../view/workbench-view.model'; +import {WorkbenchConfig} from '../../workbench-config'; +import {ViewId, WorkbenchView} from '../../view/workbench-view.model'; import {VIEW_TAB_RENDERING_CONTEXT, ViewTabRenderingContext} from '../../workbench.constants'; @Component({ @@ -35,13 +35,13 @@ export class ViewListItemComponent { public viewTabContentPortal!: ComponentPortal; @Input({required: true}) - public set viewId(viewId: string) { + public set viewId(viewId: ViewId) { this.view = this._viewRegistry.get(viewId); this.viewTabContentPortal = this.createViewTabContentPortal(); } constructor(private _viewRegistry: WorkbenchViewRegistry, - private _workbenchModuleConfig: WorkbenchModuleConfig, + private _workbenchConfig: WorkbenchConfig, private _injector: Injector) { } @@ -55,7 +55,7 @@ export class ViewListItemComponent { } private createViewTabContentPortal(): ComponentPortal { - const componentType = this._workbenchModuleConfig.viewTabComponent || ViewTabContentComponent; + const componentType = this._workbenchConfig.viewTabComponent || ViewTabContentComponent; return new ComponentPortal(componentType, null, Injector.create({ parent: this._injector, providers: [ diff --git a/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.html b/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.html index 99561ec7a..08d0bed44 100644 --- a/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.html +++ b/projects/scion/workbench/src/lib/part/view-tab-content/view-tab-content.component.html @@ -1,5 +1,5 @@ - dirty + dirty {{view.title}} diff --git a/projects/scion/workbench/src/lib/part/view-tab-drag-image/view-tab-drag-image.component.ts b/projects/scion/workbench/src/lib/part/view-tab-drag-image/view-tab-drag-image.component.ts index 9dbba6fdd..fe41d4fe9 100644 --- a/projects/scion/workbench/src/lib/part/view-tab-drag-image/view-tab-drag-image.component.ts +++ b/projects/scion/workbench/src/lib/part/view-tab-drag-image/view-tab-drag-image.component.ts @@ -11,7 +11,7 @@ import {Component, HostBinding, inject, Injector} from '@angular/core'; import {ViewDragService} from '../../view-dnd/view-drag.service'; import {ComponentPortal, PortalModule} from '@angular/cdk/portal'; -import {WorkbenchModuleConfig} from '../../workbench-module-config'; +import {WorkbenchConfig} from '../../workbench-config'; import {ViewTabContentComponent} from '../view-tab-content/view-tab-content.component'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {DOCUMENT, NgIf} from '@angular/common'; @@ -56,7 +56,7 @@ export class ViewTabDragImageComponent { public isDragOverPeripheralTabbar = false; constructor(public view: WorkbenchView, - private _workbenchModuleConfig: WorkbenchModuleConfig, + private _workbenchConfig: WorkbenchConfig, private _viewDragService: ViewDragService, private _injector: Injector) { this.installDragOverTabbarDetector(); @@ -68,7 +68,7 @@ export class ViewTabDragImageComponent { } private createViewTabContentPortal(): ComponentPortal { - const componentType = this._workbenchModuleConfig.viewTabComponent || ViewTabContentComponent; + const componentType = this._workbenchConfig.viewTabComponent || ViewTabContentComponent; return new ComponentPortal(componentType, null, this._injector); } diff --git a/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.ts b/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.ts index 6c5eb65a2..5eb021be9 100644 --- a/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.ts +++ b/projects/scion/workbench/src/lib/part/view-tab/view-tab.component.ts @@ -16,12 +16,12 @@ import {VIEW_DRAG_TRANSFER_TYPE, ViewDragService} from '../../view-dnd/view-drag import {createElement} from '../../common/dom.util'; import {ComponentPortal, PortalModule} from '@angular/cdk/portal'; import {VIEW_TAB_RENDERING_CONTEXT, ViewTabRenderingContext} from '../../workbench.constants'; -import {WorkbenchModuleConfig} from '../../workbench-module-config'; +import {WorkbenchConfig} from '../../workbench-config'; import {ViewTabContentComponent} from '../view-tab-content/view-tab-content.component'; import {ViewMenuService} from '../view-context-menu/view-menu.service'; import {ɵWorkbenchView} from '../../view/ɵworkbench-view.model'; -import {WorkbenchView} from '../../view/workbench-view.model'; -import {WorkbenchRouter} from '../../routing/workbench-router.service'; +import {ViewId, WorkbenchView} from '../../view/workbench-view.model'; +import {ɵWorkbenchRouter} from '../../routing/ɵworkbench-router.service'; import {subscribeInside} from '@scion/toolkit/operators'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {NgIf} from '@angular/common'; @@ -52,7 +52,7 @@ export class ViewTabComponent implements OnChanges { @Input({required: true}) @HostBinding('attr.data-viewid') - public viewId!: string; + public viewId!: ViewId; @HostBinding('attr.draggable') public draggable = true; @@ -70,9 +70,9 @@ export class ViewTabComponent implements OnChanges { constructor(host: ElementRef, @Inject(WORKBENCH_ID) private _workbenchId: string, - private _workbenchModuleConfig: WorkbenchModuleConfig, + private _workbenchConfig: WorkbenchConfig, private _viewRegistry: WorkbenchViewRegistry, - private _router: WorkbenchRouter, + private _router: ɵWorkbenchRouter, private _viewDragService: ViewDragService, private _differs: IterableDiffers, private _viewContextMenuService: ViewMenuService, @@ -149,6 +149,8 @@ export class ViewTabComponent implements OnChanges { viewClosable: this.view.closable, viewDirty: this.view.dirty, viewUrlSegments: this.view.urlSegments, + alternativeViewId: this.view.alternativeId, + navigationHint: this.view.navigationHint, partId: this.view.part.id, viewTabPointerOffsetX: event.offsetX, viewTabPointerOffsetY: event.offsetY, @@ -191,7 +193,7 @@ export class ViewTabComponent implements OnChanges { .subscribe(([event, enabled]) => { event.stopPropagation(); // prevent `PartBarComponent` handling the dblclick event which would undo maximization/minimization if (enabled && this.view.part.isInMainArea) { - this._router.ɵnavigate(layout => layout.toggleMaximized()).then(); + this._router.navigate(layout => layout.toggleMaximized()).then(); } }); } @@ -225,7 +227,7 @@ export class ViewTabComponent implements OnChanges { } private createViewTabContentPortal(): ComponentPortal { - const componentType = this._workbenchModuleConfig.viewTabComponent || ViewTabContentComponent; + const componentType = this._workbenchConfig.viewTabComponent || ViewTabContentComponent; return new ComponentPortal(componentType, null, Injector.create({ parent: this._injector, providers: [ diff --git a/projects/scion/workbench/src/lib/part/workbench-part.model.ts b/projects/scion/workbench/src/lib/part/workbench-part.model.ts index 13feffe51..24b8800f8 100644 --- a/projects/scion/workbench/src/lib/part/workbench-part.model.ts +++ b/projects/scion/workbench/src/lib/part/workbench-part.model.ts @@ -1,5 +1,6 @@ import {Observable} from 'rxjs'; import {WorkbenchPartAction} from '../workbench.model'; +import {ViewId} from '../view/workbench-view.model'; /** * Represents a part of the workbench layout. @@ -35,24 +36,24 @@ export abstract class WorkbenchPart { /** * Emits the currently active view in this part. */ - public abstract readonly activeViewId$: Observable; + public abstract readonly activeViewId$: Observable; /** * The currently active view, if any. */ - public abstract readonly activeViewId: string | null; + public abstract readonly activeViewId: ViewId | null; /** * Emits the views opened in this part. * * Upon subscription, emits the current views, and then each time the views change. The observable never completes. */ - public abstract readonly viewIds$: Observable; + public abstract readonly viewIds$: Observable; /** * The currently opened views in this part. */ - public abstract readonly viewIds: string[]; + public abstract readonly viewIds: ViewId[]; /** * Emits actions associated with this part. diff --git a/projects/scion/workbench/src/lib/part/workbench-part.registry.spec.ts b/projects/scion/workbench/src/lib/part/workbench-part.registry.spec.ts index 94f4bd7ba..c7fcaeb89 100644 --- a/projects/scion/workbench/src/lib/part/workbench-part.registry.spec.ts +++ b/projects/scion/workbench/src/lib/part/workbench-part.registry.spec.ts @@ -16,18 +16,16 @@ import {WorkbenchLayoutService} from '../layout/workbench-layout.service'; import {PartComponent} from './part.component'; import {ɵWorkbenchPart} from './ɵworkbench-part.model'; import {WorkbenchUrlObserver} from '../routing/workbench-url-observer.service'; -import {WorkbenchTestingModule} from '../testing/workbench-testing.module'; import {WorkbenchPart} from './workbench-part.model'; import {ɵWorkbenchLayoutFactory} from '../layout/ɵworkbench-layout.factory'; +import {provideWorkbenchForTest} from '../testing/workbench.provider'; describe('WorkbenchPartRegistry', () => { it('should delay emitting parts until the next layout change', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest(), - ], providers: [ + provideWorkbenchForTest(), {provide: WorkbenchUrlObserver, useValue: null}, // disable WorkbenchUrlObserver ], }); diff --git a/projects/scion/workbench/src/lib/part/workbench-part.spec.ts b/projects/scion/workbench/src/lib/part/workbench-part.spec.ts index ae66448ae..299f0a128 100644 --- a/projects/scion/workbench/src/lib/part/workbench-part.spec.ts +++ b/projects/scion/workbench/src/lib/part/workbench-part.spec.ts @@ -9,31 +9,32 @@ */ import {TestBed} from '@angular/core/testing'; -import {WorkbenchTestingModule} from '../testing/workbench-testing.module'; -import {RouterTestingModule} from '@angular/router/testing'; import {TestComponent} from '../testing/test.component'; import {styleFixture, waitForInitialWorkbenchLayout} from '../testing/testing.util'; import {WorkbenchComponent} from '../workbench.component'; import {WorkbenchViewRegistry} from '../view/workbench-view.registry'; import {expect} from '../testing/jasmine/matcher/custom-matchers.definition'; import {WorkbenchPartRegistry} from './workbench-part.registry'; +import {provideRouter} from '@angular/router'; +import {provideWorkbenchForTest} from '../testing/workbench.provider'; describe('WorkbenchPart', () => { it('should activate part even if view is already active', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({ + providers: [ + provideWorkbenchForTest({ layout: factory => factory .addPart('left-top') .addPart('left-bottom', {relativeTo: 'left-top', align: 'bottom'}) - .addView('view-1', {partId: 'left-top', activateView: true}) - .addView('view-2', {partId: 'left-bottom', activateView: true}) + .addView('view.101', {partId: 'left-top', activateView: true}) + .addView('view.102', {partId: 'left-bottom', activateView: true}) + .navigateView('view.101', ['test-page']) + .navigateView('view.102', ['test-page']) .activatePart('left-top'), }), - RouterTestingModule.withRoutes([ - {path: '', outlet: 'view-1', component: TestComponent}, - {path: '', outlet: 'view-2', component: TestComponent}, + provideRouter([ + {path: 'test-page', component: TestComponent}, ]), ], }); @@ -45,7 +46,7 @@ describe('WorkbenchPart', () => { expect(TestBed.inject(WorkbenchPartRegistry).get('left-bottom').active).toBeFalse(); // WHEN activating already active view - await TestBed.inject(WorkbenchViewRegistry).get('view-2').activate(); + await TestBed.inject(WorkbenchViewRegistry).get('view.102').activate(); // THEN expect part to be activated. expect(TestBed.inject(WorkbenchPartRegistry).get('left-top').active).toBeFalse(); diff --git "a/projects/scion/workbench/src/lib/part/\311\265workbench-part.model.ts" "b/projects/scion/workbench/src/lib/part/\311\265workbench-part.model.ts" index 43be7ccb8..00eae66c3 100644 --- "a/projects/scion/workbench/src/lib/part/\311\265workbench-part.model.ts" +++ "b/projects/scion/workbench/src/lib/part/\311\265workbench-part.model.ts" @@ -21,6 +21,7 @@ import {filterArray} from '@scion/toolkit/operators'; import {distinctUntilChanged, filter, map, takeUntil} from 'rxjs/operators'; import {ɵWorkbenchLayout} from '../layout/ɵworkbench-layout'; import {WorkbenchLayoutService} from '../layout/workbench-layout.service'; +import {ViewId} from '../view/workbench-view.model'; export class ɵWorkbenchPart implements WorkbenchPart { @@ -35,8 +36,8 @@ export class ɵWorkbenchPart implements WorkbenchPart { private readonly _destroy$ = new Subject(); public readonly active$ = new BehaviorSubject(false); - public readonly viewIds$ = new BehaviorSubject([]); - public readonly activeViewId$ = new BehaviorSubject(null); + public readonly viewIds$ = new BehaviorSubject([]); + public readonly activeViewId$ = new BehaviorSubject(null); public readonly actions$: Observable; private _isInMainArea: boolean | undefined; @@ -66,7 +67,7 @@ export class ɵWorkbenchPart implements WorkbenchPart { */ public onLayoutChange(layout: ɵWorkbenchLayout): void { this._isInMainArea ??= layout.hasPart(this.id, {grid: 'mainArea'}); - const part = layout.part({by: {partId: this.id}}); + const part = layout.part({partId: this.id}); const active = layout.activePart({grid: this._isInMainArea ? 'mainArea' : 'workbench'})?.id === this.id; const prevViewIds = this.viewIds$.value; const currViewIds = part.views.map(view => view.id); @@ -87,11 +88,11 @@ export class ɵWorkbenchPart implements WorkbenchPart { } } - public get viewIds(): string[] { + public get viewIds(): ViewId[] { return this.viewIds$.value; } - public get activeViewId(): string | null { + public get activeViewId(): ViewId | null { return this.activeViewId$.value; } @@ -106,7 +107,7 @@ export class ɵWorkbenchPart implements WorkbenchPart { } const currentLayout = this._workbenchLayoutService.layout; - return this._workbenchRouter.ɵnavigate( + return this._workbenchRouter.navigate( layout => currentLayout === layout ? layout.activatePart(this.id) : null, // cancel navigation if the layout has become stale {skipLocationChange: true}, // do not add part activation into browser history stack ); diff --git a/projects/scion/workbench/src/lib/perspective/migration/model/workbench-perspective-migration-v1.model.ts b/projects/scion/workbench/src/lib/perspective/migration/model/workbench-perspective-migration-v1.model.ts new file mode 100644 index 000000000..7cff9fe69 --- /dev/null +++ b/projects/scion/workbench/src/lib/perspective/migration/model/workbench-perspective-migration-v1.model.ts @@ -0,0 +1,17 @@ +/* + * 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 + */ + +import {Commands} from '../../../routing/routing.model'; + +export interface PerspectiveDataV1 { + initialWorkbenchGrid: string; + workbenchGrid: string; + viewOutlets: {[viewId: string]: Commands}; +} diff --git a/projects/scion/workbench/src/lib/perspective/migration/model/workbench-perspective-migration-v2.model.ts b/projects/scion/workbench/src/lib/perspective/migration/model/workbench-perspective-migration-v2.model.ts new file mode 100644 index 000000000..157ed1096 --- /dev/null +++ b/projects/scion/workbench/src/lib/perspective/migration/model/workbench-perspective-migration-v2.model.ts @@ -0,0 +1,20 @@ +/* + * 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 + */ + +export interface MPerspectiveLayoutV2 { + referenceLayout: { + workbenchGrid: string; + viewOutlets: string; + }; + userLayout: { + workbenchGrid: string; + viewOutlets: string; + }; +} diff --git a/projects/scion/workbench/src/lib/perspective/migration/workbench-perspective-migration-v2.service.ts b/projects/scion/workbench/src/lib/perspective/migration/workbench-perspective-migration-v2.service.ts new file mode 100644 index 000000000..9f0f12e5e --- /dev/null +++ b/projects/scion/workbench/src/lib/perspective/migration/workbench-perspective-migration-v2.service.ts @@ -0,0 +1,67 @@ +/* + * 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 + */ + +import {Injectable} from '@angular/core'; +import {PerspectiveDataV1} from './model/workbench-perspective-migration-v1.model'; +import {MPerspectiveLayoutV2} from './model/workbench-perspective-migration-v2.model'; +import {Commands} from '../../routing/routing.model'; +import {WorkbenchMigration} from '../../migration/workbench-migration'; + +/** + * Migrates the perspective layout from version 1 to version 2. + * + * TODO [Angular 20] Remove migrator. + */ +@Injectable({providedIn: 'root'}) +export class WorkbenchPerspectiveMigrationV2 implements WorkbenchMigration { + + public migrate(json: string): string { + const perspectiveDataV1: PerspectiveDataV1 = JSON.parse(json); + const perspectiveLayoutV2: MPerspectiveLayoutV2 = { + userLayout: { + workbenchGrid: perspectiveDataV1.workbenchGrid, + viewOutlets: this.migrateViewOutlets(perspectiveDataV1.viewOutlets), + }, + referenceLayout: { + workbenchGrid: perspectiveDataV1.initialWorkbenchGrid, + viewOutlets: JSON.stringify({}), + }, + }; + return JSON.stringify(perspectiveLayoutV2); + } + + private migrateViewOutlets(viewOutlets: {[viewId: string]: Commands}): string { + return JSON.stringify(Object.fromEntries(Object.entries(viewOutlets) + .map(([viewId, commands]: [string, Commands]): [string, MUrlSegmentV2[]] => { + return [viewId, commandsToSegments(commands)]; + }), + )); + } +} + +function commandsToSegments(commands: Commands): MUrlSegmentV2[] { + const segments = new Array(); + + commands.forEach(command => { + if (typeof command === 'string') { + segments.push({path: command, parameters: {}}); + } + else { + segments.at(-1)!.parameters = command; + } + }); + + return segments; +} + +interface MUrlSegmentV2 { + path: string; + parameters: {[name: string]: string}; +} diff --git a/projects/scion/workbench/src/lib/perspective/workbench-grid-merger.service.spec.ts b/projects/scion/workbench/src/lib/perspective/workbench-grid-merger.service.spec.ts index 39b5faec9..2151e5e68 100644 --- a/projects/scion/workbench/src/lib/perspective/workbench-grid-merger.service.spec.ts +++ b/projects/scion/workbench/src/lib/perspective/workbench-grid-merger.service.spec.ts @@ -9,58 +9,51 @@ */ import {TestBed} from '@angular/core/testing'; -import {WorkbenchTestingModule} from '../testing/workbench-testing.module'; -import {RouterTestingModule} from '@angular/router/testing'; -import {toEqualWorkbenchLayoutCustomMatcher} from '../testing/jasmine/matcher/to-equal-workbench-layout.matcher'; -import {ɵWorkbenchLayout} from '../layout/ɵworkbench-layout'; +import {MPart, MTreeNode, toEqualWorkbenchLayoutCustomMatcher} from '../testing/jasmine/matcher/to-equal-workbench-layout.matcher'; import {MAIN_AREA} from '../layout/workbench-layout'; import {WorkbenchGridMerger} from './workbench-grid-merger.service'; -import {MPart, MTreeNode} from '../layout/workbench-layout.model'; import {expect} from '../testing/jasmine/matcher/custom-matchers.definition'; import {ɵWorkbenchLayoutFactory} from '../layout/ɵworkbench-layout.factory'; +import {segments} from '../testing/testing.util'; +import {provideRouter} from '@angular/router'; +import {provideWorkbenchForTest} from '../testing/workbench.provider'; describe('WorkbenchGridMerger', () => { - let workbenchGridMerger: WorkbenchGridMerger; - - let local: ɵWorkbenchLayout; - let base: ɵWorkbenchLayout; - let remote: ɵWorkbenchLayout; - beforeEach(() => { jasmine.addMatchers(toEqualWorkbenchLayoutCustomMatcher); TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest(), - RouterTestingModule.withRoutes([]), + providers: [ + provideWorkbenchForTest(), + provideRouter([]), ], }); + }); - workbenchGridMerger = TestBed.inject(WorkbenchGridMerger); - - local = TestBed.inject(ɵWorkbenchLayoutFactory) + it('should preserve local changes when no diff between base and remote', () => { + const base = TestBed.inject(ɵWorkbenchLayoutFactory) .addPart(MAIN_AREA) .addPart('topLeft', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) .addPart('bottomLeft', {relativeTo: 'topLeft', align: 'bottom', ratio: .5}) .addView('view.1', {partId: 'topLeft'}) .addView('view.2', {partId: 'topLeft'}) - .addView('view.3', {partId: 'topLeft'}) - .addView('view.4', {partId: 'bottomLeft'}) - .addView('view.5', {partId: 'bottomLeft'}) - .addView('view.6', {partId: 'bottomLeft'}); - base = local; - remote = local; - }); + .addView('view.3', {partId: 'bottomLeft'}) + .navigateView('view.1', ['path/to/view/1']) + .navigateView('view.2', ['path/to/view/2']) + .navigateView('view.3', [], {hint: 'hint-3'}); - it('should do nothing if no diff between remote and base', () => { - const mergedGrid = TestBed.inject(ɵWorkbenchLayoutFactory).create({ - workbenchGrid: workbenchGridMerger.merge({ - local: local.workbenchGrid, - base: base.workbenchGrid, - remote: remote.workbenchGrid, - }), + const mergedLayout = TestBed.inject(WorkbenchGridMerger).merge({ + local: base + .removeView('view.2', {force: true}) + .addView('view.100', {partId: 'topLeft'}) + .navigateView('view.100', ['path/to/view/100']) + .navigateView('view.1', ['PATH/TO/VIEW/1']), + base, + remote: base, }); - expect(mergedGrid).toEqualWorkbenchLayout({ + + // Expect local changes not to be discarded. + expect(mergedLayout).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ direction: 'row', @@ -68,49 +61,67 @@ describe('WorkbenchGridMerger', () => { child1: new MTreeNode({ direction: 'column', ratio: .5, - child1: new MPart({id: 'topLeft', views: [{id: 'view.1'}, {id: 'view.2'}, {id: 'view.3'}]}), - child2: new MPart({id: 'bottomLeft', views: [{id: 'view.4'}, {id: 'view.5'}, {id: 'view.6'}]}), + child1: new MPart({ + id: 'topLeft', + views: [ + {id: 'view.1', navigation: {}}, // additional assertion below to assert the hint not to be present + {id: 'view.100', navigation: {}}, // additional assertion below to assert the hint not to be present + ], + }), + child2: new MPart({ + id: 'bottomLeft', + views: [ + {id: 'view.3', navigation: {hint: 'hint-3'}}, + ], + }), }), child2: new MPart({id: MAIN_AREA}), }), }, }); - }); - it('should add views that are added to the remote', () => { - const mergedGrid = TestBed.inject(ɵWorkbenchLayoutFactory).create({ - workbenchGrid: workbenchGridMerger.merge({ - local: local.workbenchGrid, - base: base.workbenchGrid, - remote: remote.addView('view.7', {partId: 'topLeft'}).workbenchGrid, - }), - }); - expect(mergedGrid).toEqualWorkbenchLayout({ - workbenchGrid: { - root: new MTreeNode({ - direction: 'row', - ratio: .25, - child1: new MTreeNode({ - direction: 'column', - ratio: .5, - child1: new MPart({id: 'topLeft', views: [{id: 'view.1'}, {id: 'view.2'}, {id: 'view.3'}, {id: 'view.7'}]}), - child2: new MPart({id: 'bottomLeft', views: [{id: 'view.4'}, {id: 'view.5'}, {id: 'view.6'}]}), - }), - child2: new MPart({id: MAIN_AREA}), - }), - }, + // Expect hint not to be present. + expect(mergedLayout.view({viewId: 'view.1'}).navigation).toEqual({}); + expect(mergedLayout.view({viewId: 'view.3'}).navigation).toEqual({hint: 'hint-3'}); + expect(mergedLayout.view({viewId: 'view.100'}).navigation).toEqual({}); + + expect(mergedLayout.viewOutlets()).toEqual({ + 'view.1': segments(['PATH/TO/VIEW/1']), + 'view.3': [], + 'view.100': segments(['path/to/view/100']), }); }); - it('should remove views that are removed from the remote', () => { - const mergedGrid = TestBed.inject(ɵWorkbenchLayoutFactory).create({ - workbenchGrid: workbenchGridMerger.merge({ - local: local.workbenchGrid, - base: base.workbenchGrid, - remote: remote.removeView('view.4').workbenchGrid, - }), + /** + * TODO [#452] The current implementation of 'WorkbenchGridMerger' discards local changes when a new layout is available. + */ + it('should discard local changes when diff between base and remote grids', () => { + const base = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart(MAIN_AREA) + .addPart('topLeft', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) + .addPart('bottomLeft', {relativeTo: 'topLeft', align: 'bottom', ratio: .5}) + .addView('view.1', {partId: 'topLeft'}) + .addView('view.2', {partId: 'topLeft'}) + .addView('view.3', {partId: 'bottomLeft'}) + .navigateView('view.1', ['path/to/view/1']) + .navigateView('view.2', ['path/to/view/2']) + .navigateView('view.3', [], {hint: 'hint-3'}); + + const mergedLayout = TestBed.inject(WorkbenchGridMerger).merge({ + local: base + .removeView('view.2', {force: true}) + .addView('view.100', {partId: 'topLeft'}) + .navigateView('view.100', ['path/to/view/100']) + .navigateView('view.3', ['path/to/view/3']), + base, + remote: base + .removeView('view.1', {force: true}) + .addView('view.100', {partId: 'bottomLeft'}) + .navigateView('view.100', ['PATH/TO/VIEW/100']), }); - expect(mergedGrid).toEqualWorkbenchLayout({ + + // Expect local changes to be discarded. + expect(mergedLayout).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ direction: 'row', @@ -118,49 +129,58 @@ describe('WorkbenchGridMerger', () => { child1: new MTreeNode({ direction: 'column', ratio: .5, - child1: new MPart({id: 'topLeft', views: [{id: 'view.1'}, {id: 'view.2'}, {id: 'view.3'}]}), - child2: new MPart({id: 'bottomLeft', views: [{id: 'view.5'}, {id: 'view.6'}]}), + child1: new MPart({ + id: 'topLeft', + views: [ + {id: 'view.2', navigation: {}}, // additional assertion below to assert the hint not to be present + ], + }), + child2: new MPart({ + id: 'bottomLeft', + views: [ + {id: 'view.3', navigation: {hint: 'hint-3'}}, + {id: 'view.100', navigation: {}}, // additional assertion below to assert the hint not to be present + ], + }), }), child2: new MPart({id: MAIN_AREA}), }), }, }); - }); - it('should not remove views that are added to the local', () => { - const mergedGrid = TestBed.inject(ɵWorkbenchLayoutFactory).create({ - workbenchGrid: workbenchGridMerger.merge({ - local: local.addView('view.7', {partId: 'topLeft'}).workbenchGrid, - base: base.workbenchGrid, - remote: remote.workbenchGrid, - }), - }); - expect(mergedGrid).toEqualWorkbenchLayout({ - workbenchGrid: { - root: new MTreeNode({ - direction: 'row', - ratio: .25, - child1: new MTreeNode({ - direction: 'column', - ratio: .5, - child1: new MPart({id: 'topLeft', views: [{id: 'view.1'}, {id: 'view.2'}, {id: 'view.3'}, {id: 'view.7'}]}), - child2: new MPart({id: 'bottomLeft', views: [{id: 'view.4'}, {id: 'view.5'}, {id: 'view.6'}]}), - }), - child2: new MPart({id: MAIN_AREA}), - }), - }, + // Expect hint not to be present. + expect(mergedLayout.view({viewId: 'view.2'}).navigation).toEqual({}); + expect(mergedLayout.view({viewId: 'view.3'}).navigation).toEqual({hint: 'hint-3'}); + expect(mergedLayout.view({viewId: 'view.100'}).navigation).toEqual({}); + + expect(mergedLayout.viewOutlets()).toEqual({ + 'view.2': segments(['path/to/view/2']), + 'view.3': [], + 'view.100': segments(['PATH/TO/VIEW/100']), }); }); - it('should not re-add views that are removed from the local (1)', () => { - const mergedGrid = TestBed.inject(ɵWorkbenchLayoutFactory).create({ - workbenchGrid: workbenchGridMerger.merge({ - local: local.removeView('view.1').workbenchGrid, - base: base.workbenchGrid, - remote: remote.workbenchGrid, - }), + /** + * TODO [#452] The current implementation of 'WorkbenchGridMerger' discards local changes when a new layout is available. + */ + it('should discard local changes when diff between base and remote paths', () => { + const base = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart(MAIN_AREA) + .addPart('topLeft', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) + .addPart('bottomLeft', {relativeTo: 'topLeft', align: 'bottom', ratio: .5}) + .addView('view.1', {partId: 'topLeft'}) + .addView('view.2', {partId: 'bottomLeft'}) + .navigateView('view.1', ['path/to/view/1']) + .navigateView('view.2', ['path/to/view/2']); + + const mergedLayout = TestBed.inject(WorkbenchGridMerger).merge({ + local: base.navigateView('view.2', ['path/to/view/2a']), + base, + remote: base.navigateView('view.2', ['path/to/view/2b']), }); - expect(mergedGrid).toEqualWorkbenchLayout({ + + // Expect local changes to be discarded. + expect(mergedLayout).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ direction: 'row', @@ -168,66 +188,55 @@ describe('WorkbenchGridMerger', () => { child1: new MTreeNode({ direction: 'column', ratio: .5, - child1: new MPart({id: 'topLeft', views: [{id: 'view.2'}, {id: 'view.3'}]}), - child2: new MPart({id: 'bottomLeft', views: [{id: 'view.4'}, {id: 'view.5'}, {id: 'view.6'}]}), + child1: new MPart({ + id: 'topLeft', + views: [ + {id: 'view.1', navigation: {}}, // additional assertion below to assert the hint not to be present + ], + }), + child2: new MPart({ + id: 'bottomLeft', + views: [ + {id: 'view.2', navigation: {}}, // additional assertion below to assert the hint not to be present + ], + }), }), child2: new MPart({id: MAIN_AREA}), }), }, }); - }); - it('should not re-add views that are removed from the local (2)', () => { - const mergedGrid = TestBed.inject(ɵWorkbenchLayoutFactory).create({ - workbenchGrid: workbenchGridMerger.merge({ - local: {root: new MPart({id: MAIN_AREA}), activePartId: MAIN_AREA}, - base: remote.workbenchGrid, - remote: remote.workbenchGrid, - }), - }); - expect(mergedGrid).toEqualWorkbenchLayout({ - workbenchGrid: { - root: new MPart({id: MAIN_AREA}), - }, - }); - }); + // Expect hint not to be present. + expect(mergedLayout.view({viewId: 'view.1'}).navigation).toEqual({}); + expect(mergedLayout.view({viewId: 'view.2'}).navigation).toEqual({}); - it('should not re-add views that are moved in the local', () => { - const mergedGrid = TestBed.inject(ɵWorkbenchLayoutFactory).create({ - workbenchGrid: workbenchGridMerger.merge({ - local: local.moveView('view.1', 'bottomLeft').workbenchGrid, - base: base.workbenchGrid, - remote: remote.workbenchGrid, - }), - }); - expect(mergedGrid).toEqualWorkbenchLayout({ - workbenchGrid: { - root: new MTreeNode({ - direction: 'row', - ratio: .25, - child1: new MTreeNode({ - direction: 'column', - ratio: .5, - child1: new MPart({id: 'topLeft', views: [{id: 'view.2'}, {id: 'view.3'}]}), - child2: new MPart({id: 'bottomLeft', views: [{id: 'view.4'}, {id: 'view.5'}, {id: 'view.6'}, {id: 'view.1'}]}), - }), - child2: new MPart({id: MAIN_AREA}), - }), - }, + expect(mergedLayout.viewOutlets()).toEqual({ + 'view.1': segments(['path/to/view/1']), + 'view.2': segments(['path/to/view/2b']), }); }); - it('should add views of new remote parts to "any" local part (1)', () => { - // This test is not very useful and should be removed when implemented issue #452. - // TODO [#452]: Support for merging newly added or moved parts into the user's layout) - const mergedGrid = TestBed.inject(ɵWorkbenchLayoutFactory).create({ - workbenchGrid: workbenchGridMerger.merge({ - local: local.workbenchGrid, - base: base.workbenchGrid, - remote: remote.addPart('right', {relativeTo: MAIN_AREA, align: 'right', ratio: .25}).addView('view.7', {partId: 'right'}).workbenchGrid, - }), + /** + * TODO [#452] The current implementation of 'WorkbenchGridMerger' discards local changes when a new layout is available. + */ + it('should discard local changes when diff between base and remote hints', () => { + const base = TestBed.inject(ɵWorkbenchLayoutFactory) + .addPart(MAIN_AREA) + .addPart('topLeft', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) + .addPart('bottomLeft', {relativeTo: 'topLeft', align: 'bottom', ratio: .5}) + .addView('view.1', {partId: 'topLeft'}) + .addView('view.2', {partId: 'bottomLeft'}) + .navigateView('view.1', ['path/to/view/1']) + .navigateView('view.2', [], {hint: 'hint-2'}); + + const mergedLayout = TestBed.inject(WorkbenchGridMerger).merge({ + local: base.navigateView('view.2', [], {hint: 'hint-2a'}), + base, + remote: base.navigateView('view.2', [], {hint: 'hint-2b'}), }); - expect(mergedGrid).toEqualWorkbenchLayout({ + + // Expect local changes to be discarded. + expect(mergedLayout).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ direction: 'row', @@ -235,34 +244,31 @@ describe('WorkbenchGridMerger', () => { child1: new MTreeNode({ direction: 'column', ratio: .5, - child1: new MPart({id: 'topLeft', views: [{id: 'view.1'}, {id: 'view.2'}, {id: 'view.3'}, {id: 'view.7'}]}), - child2: new MPart({id: 'bottomLeft', views: [{id: 'view.4'}, {id: 'view.5'}, {id: 'view.6'}]}), + child1: new MPart({ + id: 'topLeft', + views: [ + {id: 'view.1', navigation: {}}, // additional assertion below to assert the hint not to be present + ], + }), + child2: new MPart({ + id: 'bottomLeft', + views: [ + {id: 'view.2', navigation: {hint: 'hint-2b'}}, + ], + }), }), child2: new MPart({id: MAIN_AREA}), }), }, }); - }); - it('should add views of new remote parts to "any" local part (2)', () => { - // This test is not very useful and should be removed when implemented issue #452. - // TODO [#452]: Support for merging newly added or moved parts into the user's layout) - const mergedGrid = TestBed.inject(ɵWorkbenchLayoutFactory).create({ - workbenchGrid: workbenchGridMerger.merge({ - local: {root: new MPart({id: MAIN_AREA}), activePartId: MAIN_AREA}, - base: {root: new MPart({id: MAIN_AREA}), activePartId: MAIN_AREA}, - remote: remote.workbenchGrid, - }), - }); - expect(mergedGrid).toEqualWorkbenchLayout({ - workbenchGrid: { - root: new MTreeNode({ - direction: 'row', - ratio: .5, - child1: new MPart({id: 'topLeft', views: [{id: 'view.1'}, {id: 'view.2'}, {id: 'view.3'}, {id: 'view.4'}, {id: 'view.5'}, {id: 'view.6'}]}), - child2: new MPart({id: MAIN_AREA}), - }), - }, + // Expect hint not to be present. + expect(mergedLayout.view({viewId: 'view.1'}).navigation).toEqual({}); + expect(mergedLayout.view({viewId: 'view.2'}).navigation).toEqual({hint: 'hint-2b'}); + + expect(mergedLayout.viewOutlets()).toEqual({ + 'view.1': segments(['path/to/view/1']), + 'view.2': [], }); }); }); diff --git a/projects/scion/workbench/src/lib/perspective/workbench-grid-merger.service.ts b/projects/scion/workbench/src/lib/perspective/workbench-grid-merger.service.ts index ab52ba380..d4907a0b8 100644 --- a/projects/scion/workbench/src/lib/perspective/workbench-grid-merger.service.ts +++ b/projects/scion/workbench/src/lib/perspective/workbench-grid-merger.service.ts @@ -7,68 +7,30 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {inject, Injectable, IterableChanges, IterableDiffers} from '@angular/core'; -import {MPartGrid, MView} from '../layout/workbench-layout.model'; +import {Injectable} from '@angular/core'; import {ɵWorkbenchLayout} from '../layout/ɵworkbench-layout'; -import {MAIN_AREA} from '../layout/workbench-layout'; -import {ɵWorkbenchLayoutFactory} from '../layout/ɵworkbench-layout.factory'; /** - * Performs a three-way merge of the changes from the local and remote grid, using the base grid (common ancestor) as the base of the merge operation. + * Performs a three-way merge of the local and remote layouts, using the base layout (common ancestor) as the base of the merge operation. + * + * TODO [#452] This implementation discards local changes when a new layout is available. */ @Injectable({providedIn: 'root'}) export class WorkbenchGridMerger { - private _differs = inject(IterableDiffers).find([]); - - constructor(private _workbenchLayoutFactory: ɵWorkbenchLayoutFactory, iterableDiffers: IterableDiffers) { - this._differs = iterableDiffers.find([]); - } - - /** - * Performs a merge of given local and remote grids, using the base grid as the common ancestor. - */ - public merge(grids: {local: MPartGrid; remote: MPartGrid; base: MPartGrid}): MPartGrid { - const localLayout = this._workbenchLayoutFactory.create({workbenchGrid: grids.local}); - const baseLayout = this._workbenchLayoutFactory.create({workbenchGrid: grids.base}); - const remoteLayout = this._workbenchLayoutFactory.create({workbenchGrid: grids.remote}); - - let mergedLayout: ɵWorkbenchLayout = localLayout; - const viewsChanges = this.viewsDiff(baseLayout, remoteLayout); - - viewsChanges?.forEachAddedItem(({item: addedView}) => { - // If the local grid contains the part, add the view to that part. - const part = remoteLayout.part({by: {viewId: addedView.id}}); - if (mergedLayout.hasPart(part.id)) { - mergedLayout = mergedLayout.addView(addedView.id, {partId: part.id}); - } - // If the local grid does not contain the part, add the part to an existing part or create a new part. - else { - const existingPart = mergedLayout.parts({grid: 'workbench'}).filter(part => part.id !== MAIN_AREA)[0]; - if (existingPart) { - mergedLayout = mergedLayout.addView(addedView.id, {partId: existingPart.id}); - } - else { - mergedLayout = mergedLayout - .addPart(part.id, {align: 'left'}) - .addView(addedView.id, {partId: part.id}); - } - } - }); - - viewsChanges?.forEachRemovedItem(({item: removedView}) => { - mergedLayout = mergedLayout.removeView(removedView.id); - }); - - return mergedLayout.workbenchGrid; - } - /** - * Computes the diff of views added or removed in layout 2. + * Performs a merge of given local and remote layouts, using the base layout as the common ancestor. */ - private viewsDiff(layout1: ɵWorkbenchLayout, layout2: ɵWorkbenchLayout): IterableChanges | null { - const differ = this._differs.create((index, view) => view.id); - differ.diff(layout1.views({grid: 'workbench'})); - return differ.diff(layout2.views({grid: 'workbench'})); + public merge(grids: {local: ɵWorkbenchLayout; remote: ɵWorkbenchLayout; base: ɵWorkbenchLayout}): ɵWorkbenchLayout { + const serializedBaseLayout = grids.base.serialize(); + const serializedRemoteLayout = grids.remote.serialize(); + + if (serializedBaseLayout.workbenchGrid !== serializedRemoteLayout.workbenchGrid) { + return grids.remote; + } + if (serializedBaseLayout.workbenchViewOutlets !== serializedRemoteLayout.workbenchViewOutlets) { + return grids.remote; + } + return grids.local; } } diff --git a/projects/scion/workbench/src/lib/perspective/workbench-perspective-storage.service.ts b/projects/scion/workbench/src/lib/perspective/workbench-perspective-storage.service.ts index 3c54a9037..13972c9db 100644 --- a/projects/scion/workbench/src/lib/perspective/workbench-perspective-storage.service.ts +++ b/projects/scion/workbench/src/lib/perspective/workbench-perspective-storage.service.ts @@ -10,7 +10,9 @@ import {Injectable} from '@angular/core'; import {WorkbenchStorage} from '../storage/workbench-storage'; -import {Commands} from '../routing/routing.model'; +import {MPerspectiveLayout} from './workbench-perspective.model'; +import {WorkbenchPerspectiveSerializer} from './workench-perspective-serializer.service'; +import {Logger} from '../logging'; /** * Provides API to read/write perspective data from/to {@link WorkbenchStorage}. @@ -18,29 +20,32 @@ import {Commands} from '../routing/routing.model'; @Injectable({providedIn: 'root'}) export class WorkbenchPerspectiveStorageService { - constructor(private _storage: WorkbenchStorage) { + constructor(private _storage: WorkbenchStorage, + private _workbenchPerspectiveSerializer: WorkbenchPerspectiveSerializer, + private _logger: Logger) { } /** - * Reads perspective data for a given perspective from storage. + * Reads the layout of given perspective from storage, applying necessary migrations if the serialized layout is outdated. */ - public async loadPerspectiveData(perspectiveId: string): Promise { - const storageKey = storageKeys.perspectiveData(perspectiveId); + public async loadPerspectiveLayout(perspectiveId: string): Promise { + const storageKey = storageKeys.perspectiveLayout(perspectiveId); const serialized = await this._storage.load(storageKey); - if (!serialized?.length) { + try { + return this._workbenchPerspectiveSerializer.deserialize(serialized); + } + catch (error) { + this._logger.error(`[SerializeError] Failed to deserialize perspective '${perspectiveId}'. Please clear your browser storage and reload the application.`, error); return null; } - const json = window.atob(serialized); - return JSON.parse(json); } /** - * Writes perspective data for a given perspective to storage. + * Writes the layout of a perspective to storage. */ - public async storePerspectiveData(perspectiveId: string, data: PerspectiveData): Promise { - const storageKey = storageKeys.perspectiveData(perspectiveId); - const json = JSON.stringify(data); - const serialized = window.btoa(json); + public async storePerspectiveLayout(perspectiveId: string, data: MPerspectiveLayout): Promise { + const serialized = this._workbenchPerspectiveSerializer.serialize(data); + const storageKey = storageKeys.perspectiveLayout(perspectiveId); await this._storage.store(storageKey, serialized); } @@ -63,27 +68,6 @@ export class WorkbenchPerspectiveStorageService { * Represents keys to associate data in the storage. */ const storageKeys = { - perspectiveData: (perspectiveId: string): string => `scion.workbench.perspectives.${perspectiveId}`, + perspectiveLayout: (perspectiveId: string): string => `scion.workbench.perspectives.${perspectiveId}`, activePerspectiveId: 'scion.workbench.perspective', }; - -/** - * Perspective data stored in persistent storage. - */ -export interface PerspectiveData { - /** - * The actual workbench grid. - * - * When activated the perspective for the first time, this grid is identical to the {@link initialWorkbenchGrid}, - * but changes when the user customizes the layout. - */ - workbenchGrid: string | null; - /** - * The initial definition used to create the workbench grid. - */ - initialWorkbenchGrid: string | null; - /** - * Commands of views contained in the workbench grid. - */ - viewOutlets: {[viewId: string]: Commands}; -} diff --git a/projects/scion/workbench/src/lib/perspective/workbench-perspective-storage.spec.ts b/projects/scion/workbench/src/lib/perspective/workbench-perspective-storage.spec.ts index 90f33a142..5a9f30b56 100644 --- a/projects/scion/workbench/src/lib/perspective/workbench-perspective-storage.spec.ts +++ b/projects/scion/workbench/src/lib/perspective/workbench-perspective-storage.spec.ts @@ -10,19 +10,17 @@ import {TestBed} from '@angular/core/testing'; import {expect} from '../testing/jasmine/matcher/custom-matchers.definition'; -import {toEqualWorkbenchLayoutCustomMatcher} from '../testing/jasmine/matcher/to-equal-workbench-layout.matcher'; +import {MPart, MTreeNode, toEqualWorkbenchLayoutCustomMatcher} from '../testing/jasmine/matcher/to-equal-workbench-layout.matcher'; import {TestComponent} from '../testing/test.component'; import {WorkbenchRouter} from '../routing/workbench-router.service'; import {MAIN_AREA, WorkbenchLayout} from '../layout/workbench-layout'; import {styleFixture, waitForInitialWorkbenchLayout, waitUntilStable} from '../testing/testing.util'; -import {WorkbenchTestingModule} from '../testing/workbench-testing.module'; -import {RouterTestingModule} from '@angular/router/testing'; import {WorkbenchLayoutComponent} from '../layout/workbench-layout.component'; -import {MPart, MTreeNode} from '../layout/workbench-layout.model'; import {WorkbenchService} from '../workbench.service'; - -import {PerspectiveData} from './workbench-perspective-storage.service'; import {ɵWorkbenchLayoutFactory} from '../layout/ɵworkbench-layout.factory'; +import {WorkbenchPerspectiveStorageService} from './workbench-perspective-storage.service'; +import {provideRouter} from '@angular/router'; +import {provideWorkbenchForTest} from '../testing/workbench.provider'; describe('WorkbenchPerspectiveStorage', () => { @@ -33,8 +31,8 @@ describe('WorkbenchPerspectiveStorage', () => { it('should write the layout to storage on every layout change', async () => { // GIVEN: Single perspective with two parts TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({ + providers: [ + provideWorkbenchForTest({ layout: { perspectives: [ { @@ -48,7 +46,7 @@ describe('WorkbenchPerspectiveStorage', () => { }, startup: {launcher: 'APP_INITIALIZER'}, }), - RouterTestingModule.withRoutes([ + provideRouter([ {path: 'view', component: TestComponent}, ]), ], @@ -57,11 +55,11 @@ describe('WorkbenchPerspectiveStorage', () => { await waitForInitialWorkbenchLayout(); // WHEN: Opening view.1 in part 'left-top' - await TestBed.inject(WorkbenchRouter).navigate(['view'], {blankPartId: 'left-top', target: 'view.1'}); + await TestBed.inject(WorkbenchRouter).navigate(['view'], {partId: 'left-top', target: 'view.1'}); await waitUntilStable(); // THEN: Expect the layout to be stored. - expect(deserializePerspectiveData(localStorage.getItem('scion.workbench.perspectives.perspective'))).toEqualWorkbenchLayout({ + expect(await loadPerspectiveLayoutFromStorage('perspective')).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ child1: new MTreeNode({ @@ -74,11 +72,11 @@ describe('WorkbenchPerspectiveStorage', () => { }); // WHEN: Opening view.2 in part 'left-bottom' - await TestBed.inject(WorkbenchRouter).navigate(['view'], {blankPartId: 'left-bottom', target: 'view.2'}); + await TestBed.inject(WorkbenchRouter).navigate(['view'], {partId: 'left-bottom', target: 'view.2'}); await waitUntilStable(); // THEN: Expect the layout to be stored. - expect(deserializePerspectiveData(localStorage.getItem('scion.workbench.perspectives.perspective'))).toEqualWorkbenchLayout({ + expect(await loadPerspectiveLayoutFromStorage('perspective')).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ child1: new MTreeNode({ @@ -94,8 +92,8 @@ describe('WorkbenchPerspectiveStorage', () => { it('should load the layout from storage when activating the perspective', async () => { // GIVEN: Two perspectives with a part TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({ + providers: [ + provideWorkbenchForTest({ layout: { perspectives: [ { @@ -111,7 +109,7 @@ describe('WorkbenchPerspectiveStorage', () => { }, startup: {launcher: 'APP_INITIALIZER'}, }), - RouterTestingModule.withRoutes([ + provideRouter([ {path: 'view', component: TestComponent}, ]), ], @@ -120,7 +118,7 @@ describe('WorkbenchPerspectiveStorage', () => { await waitForInitialWorkbenchLayout(); // Open view.1 in perspective-1. - await TestBed.inject(WorkbenchRouter).navigate(['view'], {blankPartId: 'left', target: 'view.1'}); + await TestBed.inject(WorkbenchRouter).navigate(['view'], {partId: 'left', target: 'view.1'}); await waitUntilStable(); expect(fixture).toEqualWorkbenchLayout({ @@ -136,7 +134,7 @@ describe('WorkbenchPerspectiveStorage', () => { const layout = localStorage.getItem('scion.workbench.perspectives.perspective-1')!; // Open view.2 in perspective-1. - await TestBed.inject(WorkbenchRouter).navigate(['view'], {blankPartId: 'left', target: 'view.2'}); + await TestBed.inject(WorkbenchRouter).navigate(['view'], {partId: 'left', target: 'view.2'}); await waitUntilStable(); expect(fixture).toEqualWorkbenchLayout({ @@ -173,8 +171,8 @@ describe('WorkbenchPerspectiveStorage', () => { it('should only write the layout of the active perspective to storage', async () => { // GIVEN: Two perspectives with a part TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({ + providers: [ + provideWorkbenchForTest({ layout: { perspectives: [ { @@ -190,8 +188,8 @@ describe('WorkbenchPerspectiveStorage', () => { }, startup: {launcher: 'APP_INITIALIZER'}, }), - RouterTestingModule.withRoutes([ - {path: 'view', component: TestComponent}, + provideRouter([ + {path: 'path/to/view', component: TestComponent}, ]), ], }); @@ -199,11 +197,11 @@ describe('WorkbenchPerspectiveStorage', () => { await waitForInitialWorkbenchLayout(); // WHEN: Opening view.1 in perspective-1 - await TestBed.inject(WorkbenchRouter).navigate(['view'], {blankPartId: 'left', target: 'view.1'}); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {partId: 'left', target: 'view.1'}); await waitUntilStable(); // THEN: Expect the layout of perspective-1 to be stored. - expect(deserializePerspectiveData(localStorage.getItem('scion.workbench.perspectives.perspective-1'))).toEqualWorkbenchLayout({ + expect(await loadPerspectiveLayoutFromStorage('perspective-1')).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ child1: new MPart({id: 'left', views: [{id: 'view.1'}], activeViewId: 'view.1'}), @@ -212,7 +210,7 @@ describe('WorkbenchPerspectiveStorage', () => { }, }); // THEN: Expect the layout of perspective-2 not to be stored. - expect(deserializePerspectiveData(localStorage.getItem('scion.workbench.perspectives.perspective-2'))).toBeNull(); + expect(await loadPerspectiveLayoutFromStorage('perspective-2')).toBeNull(); // Switch to perspective-2. await TestBed.inject(WorkbenchService).switchPerspective('perspective-2'); @@ -222,11 +220,11 @@ describe('WorkbenchPerspectiveStorage', () => { localStorage.clear(); // WHEN: Opening view.1 in perspective-2 - await TestBed.inject(WorkbenchRouter).navigate(['view'], {blankPartId: 'left', target: 'view.1'}); + await TestBed.inject(WorkbenchRouter).navigate(['path/to/view'], {partId: 'left', target: 'view.1'}); await waitUntilStable(); // THEN: Expect the layout of perspective-2 to be stored. - expect(deserializePerspectiveData(localStorage.getItem('scion.workbench.perspectives.perspective-2'))).toEqualWorkbenchLayout({ + expect(await loadPerspectiveLayoutFromStorage('perspective-2')).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ child1: new MPart({id: 'left', views: [{id: 'view.1'}], activeViewId: 'view.1'}), @@ -235,17 +233,18 @@ describe('WorkbenchPerspectiveStorage', () => { }, }); // THEN: Expect the layout of perspective-1 not to be stored. - expect(deserializePerspectiveData(localStorage.getItem('scion.workbench.perspectives.perspective-1'))).toBeNull(); + expect(await loadPerspectiveLayoutFromStorage('perspective-1')).toBeNull(); }); }); -/** - * Deserializes given perspective data. - */ -function deserializePerspectiveData(serializedPerspectiveData: string | null): WorkbenchLayout | null { - if (!serializedPerspectiveData) { +async function loadPerspectiveLayoutFromStorage(perspectiveId: string): Promise { + const perspectiveLayout = await TestBed.inject(WorkbenchPerspectiveStorageService).loadPerspectiveLayout(perspectiveId); + if (!perspectiveLayout) { return null; } - const perspectiveData: PerspectiveData = JSON.parse(window.atob(serializedPerspectiveData)); - return TestBed.inject(ɵWorkbenchLayoutFactory).create({workbenchGrid: perspectiveData.workbenchGrid}); + + return TestBed.inject(ɵWorkbenchLayoutFactory).create({ + workbenchGrid: perspectiveLayout.userLayout.workbenchGrid, + viewOutlets: perspectiveLayout.userLayout.viewOutlets, + }); } diff --git a/projects/scion/workbench/src/lib/perspective/workbench-perspective-view-conflict-resolver.service.ts b/projects/scion/workbench/src/lib/perspective/workbench-perspective-view-conflict-resolver.service.ts index 62c75d555..743d4f87a 100644 --- a/projects/scion/workbench/src/lib/perspective/workbench-perspective-view-conflict-resolver.service.ts +++ b/projects/scion/workbench/src/lib/perspective/workbench-perspective-view-conflict-resolver.service.ts @@ -8,61 +8,43 @@ * SPDX-License-Identifier: EPL-2.0 */ import {Injectable} from '@angular/core'; -import {MPartGrid} from '../layout/workbench-layout.model'; -import {Arrays, Dictionaries, Maps} from '@scion/toolkit/util'; -import {ɵWorkbenchLayoutFactory} from '../layout/ɵworkbench-layout.factory'; -import {RouterUtils} from '../routing/router.util'; -import {Commands} from '../routing/routing.model'; +import {Arrays} from '@scion/toolkit/util'; +import {ViewId} from '../view/workbench-view.model'; +import {WorkbenchLayouts} from '../layout/workbench-layouts.util'; +import {ɵWorkbenchLayout} from '../layout/ɵworkbench-layout'; /** - * Detects and resolves name conflicts of view names, that may occur when switching between perspectives. + * Detects and resolves conflicting view ids, that may occur when switching between perspectives. */ @Injectable({providedIn: 'root'}) export class WorkbenchPerspectiveViewConflictResolver { - constructor(private _workbenchLayoutFactory: ɵWorkbenchLayoutFactory) { - } - /** - * Detects and resolves name clashes between views defined by the perspective and views in the main area. - * - * Conflict resolution for views defined by the perspective: - * - Assigns views a new identity if target of a primary route. The id of such views begin with the view prefix. - * - Removes views if target of a secondary route. The id of such views does not begin with the view prefix. + * Detects and resolves id clashes between views defined by the perspective and views contained in the main area, + * assigning views of the perspective a new identity. * - * @param mainAreaGrid - The grid of the main area. - * @param perspective - The workbench grid and views of the perspective. - * @return workbench grid and views of the provided perspective with conflicts resolved, if any. + * @param currentLayout - The current workbench layout. + * @param perspectiveLayout - The layout of the perspective to activate. + * @return layout of the perspective with conflicts resolved. */ - public resolve(mainAreaGrid: MPartGrid, perspective: {workbenchGrid: MPartGrid; viewOutlets: {[viewId: string]: Commands}}): {workbenchGrid: MPartGrid; viewOutlets: {[viewId: string]: Commands}} { - const conflictingLayout = this._workbenchLayoutFactory.create({mainAreaGrid, workbenchGrid: perspective.workbenchGrid}); - const conflictingViewIds = Arrays.intersect( - conflictingLayout.views({grid: 'workbench'}).map(view => view.id), - conflictingLayout.views({grid: 'mainArea'}).map(view => view.id), - ); + public resolve(currentLayout: ɵWorkbenchLayout, perspectiveLayout: ɵWorkbenchLayout): ɵWorkbenchLayout { + const perspectiveViewIds = perspectiveLayout.views({grid: 'workbench'}).map(view => view.id); + const mainAreaViewIds = currentLayout.views({grid: 'mainArea'}).map(view => view.id); + + // Test if there are conflicts. + const conflictingViewIds = Arrays.intersect(perspectiveViewIds, mainAreaViewIds); if (!conflictingViewIds.length) { - return perspective; + return perspectiveLayout; } - const viewOutlets = Maps.coerce(perspective.viewOutlets); - const resolvedLayout = conflictingViewIds.reduce((layout, conflictingViewId) => { - if (RouterUtils.isPrimaryRouteTarget(conflictingViewId)) { - const newViewId = layout.computeNextViewId(); - const path = viewOutlets.get(conflictingViewId)!; - viewOutlets.delete(conflictingViewId); - - // Rename view in the perspective grid. - viewOutlets.set(newViewId, path); - return layout.renameView(conflictingViewId, newViewId, {grid: 'workbench'}); - } - else { - return layout.removeView(conflictingViewId, {grid: 'workbench'}); - } - }, conflictingLayout); + // Rename conflicting views. + const usedViewIds = new Set(perspectiveViewIds.concat(mainAreaViewIds)); + conflictingViewIds.forEach(conflictingViewId => { + const newViewId = WorkbenchLayouts.computeNextViewId(usedViewIds); + perspectiveLayout = perspectiveLayout.renameView(conflictingViewId, newViewId); + usedViewIds.add(newViewId); + }); - return { - workbenchGrid: resolvedLayout.workbenchGrid, - viewOutlets: Dictionaries.coerce(viewOutlets), - }; + return perspectiveLayout; } } diff --git a/projects/scion/workbench/src/lib/perspective/workbench-perspective-view-conflict-resolver.spec.ts b/projects/scion/workbench/src/lib/perspective/workbench-perspective-view-conflict-resolver.spec.ts index 3b0283766..1b6068d47 100644 --- a/projects/scion/workbench/src/lib/perspective/workbench-perspective-view-conflict-resolver.spec.ts +++ b/projects/scion/workbench/src/lib/perspective/workbench-perspective-view-conflict-resolver.spec.ts @@ -10,17 +10,16 @@ import {TestBed} from '@angular/core/testing'; import {expect} from '../testing/jasmine/matcher/custom-matchers.definition'; -import {toEqualWorkbenchLayoutCustomMatcher} from '../testing/jasmine/matcher/to-equal-workbench-layout.matcher'; +import {MPart, MTreeNode, toEqualWorkbenchLayoutCustomMatcher} from '../testing/jasmine/matcher/to-equal-workbench-layout.matcher'; import {By} from '@angular/platform-browser'; import {TestComponent, withComponentContent} from '../testing/test.component'; import {WorkbenchRouter} from '../routing/workbench-router.service'; import {WorkbenchService} from '../workbench.service'; import {MAIN_AREA} from '../layout/workbench-layout'; import {styleFixture, waitForInitialWorkbenchLayout, waitUntilStable} from '../testing/testing.util'; -import {WorkbenchTestingModule} from '../testing/workbench-testing.module'; -import {RouterTestingModule} from '@angular/router/testing'; import {WorkbenchLayoutComponent} from '../layout/workbench-layout.component'; -import {MPart, MTreeNode} from '../layout/workbench-layout.model'; +import {provideRouter} from '@angular/router'; +import {provideWorkbenchForTest} from '../testing/workbench.provider'; describe('WorkbenchPerspectiveViewConflictResolver', () => { @@ -30,8 +29,8 @@ describe('WorkbenchPerspectiveViewConflictResolver', () => { it('should resolve view conflicts when switching perspective', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({ + providers: [ + provideWorkbenchForTest({ layout: { perspectives: [ {id: 'perspective-1', layout: factory => factory.addPart(MAIN_AREA).addPart('left', {align: 'left'})}, @@ -41,7 +40,7 @@ describe('WorkbenchPerspectiveViewConflictResolver', () => { }, startup: {launcher: 'APP_INITIALIZER'}, }), - RouterTestingModule.withRoutes([ + provideRouter([ {path: 'view-1', component: TestComponent, providers: [withComponentContent('a')]}, {path: 'view-2', component: TestComponent, providers: [withComponentContent('b')]}, ]), @@ -51,7 +50,7 @@ describe('WorkbenchPerspectiveViewConflictResolver', () => { await waitForInitialWorkbenchLayout(); // Open view.1 in perspective-1 - await TestBed.inject(WorkbenchRouter).navigate(['view-1'], {blankPartId: 'left', target: 'view.1'}); + await TestBed.inject(WorkbenchRouter).navigate(['view-1'], {partId: 'left', target: 'view.1'}); await waitUntilStable(); // Switch to perspective-2 diff --git a/projects/scion/workbench/src/lib/perspective/workbench-perspective.model.ts b/projects/scion/workbench/src/lib/perspective/workbench-perspective.model.ts index db0875900..22808acd4 100644 --- a/projects/scion/workbench/src/lib/perspective/workbench-perspective.model.ts +++ b/projects/scion/workbench/src/lib/perspective/workbench-perspective.model.ts @@ -72,6 +72,8 @@ export interface WorkbenchPerspectiveDefinition { id: string; /** * Function to create the initial layout for the perspective. The function can call `inject` to get any required dependencies. + * + * See {@link WorkbenchLayoutFn} for more information and an example. */ layout: WorkbenchLayoutFn; /** @@ -91,27 +93,95 @@ export interface WorkbenchPerspectiveDefinition { /** * Signature of a function to provide a workbench layout. * - * The function is passed a factory to create the layout. The layout has methods to modify it. - * Each modification creates a new layout instance that can be used for further modifications. - * - * The layout is an immutable object, i.e., modifications have no side effects. + * The workbench will invoke this function with a factory to create the layout. The layout is immutable, so each modification creates a new instance. + * Use the instance for further modifications and finally return it. * * The function can call `inject` to get any required dependencies. * + * ## Workbench Layout + * The workbench layout is a grid of parts. Parts are aligned relative to each other. A part is a stack of views. Content is displayed in views. + * + * The layout can be divided into a main and a peripheral area, with the main area as the primary place for opening views. + * The peripheral area arranges parts around the main area to provide navigation or context-sensitive assistance to support + * the user's workflow. Defining a main area is optional and recommended for applications requiring a dedicated and maximizable + * area for user interaction. + * + * ## Steps to create the layout + * Start by adding the first part. From there, you can gradually add more parts and align them relative to each other. + * Next, add views to the layout, specifying to which part to add the views. + * The final step is to navigate the views. A view can be navigated to any route. + * + * To avoid cluttering the initial URL, we recommend navigating the views of the initial layout to empty path routes and using a navigation hint to differentiate. + * + * ## Example + * The following example defines a layout with a main area and three parts in the peripheral area: + * + * ```plain + * +--------+----------------+ + * | top | main area | + * | left | | + * |--------+ | + * | bottom | | + * | left | | + * +--------+----------------+ + * | bottom | + * +-------------------------+ + * ``` + * * ```ts * function defineLayout(factory: WorkbenchLayoutFactory): WorkbenchLayout { - * return factory.addPart(MAIN_AREA) - * .addPart('topLeft', {align: 'left', ratio: .25}) - * .addPart('bottomLeft', {relativeTo: 'topLeft', align: 'bottom', ratio: .5}) - * .addPart('bottom', {align: 'bottom', ratio: .3}) - * .addView('navigator', {partId: 'topLeft', activateView: true}) - * .addView('explorer', {partId: 'topLeft'}) - * .addView('repositories', {partId: 'bottomLeft', activateView: true}) - * .addView('console', {partId: 'bottom', activateView: true}) - * .addView('problems', {partId: 'bottom'}) - * .addView('search', {partId: 'bottom'}); + * return factory + * // Add parts to the layout. + * .addPart(MAIN_AREA) + * .addPart('topLeft', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) + * .addPart('bottomLeft', {relativeTo: 'topLeft', align: 'bottom', ratio: .5}) + * .addPart('bottom', {align: 'bottom', ratio: .3}) + * + * // Add views to the layout. + * .addView('navigator', {partId: 'topLeft'}) + * .addView('explorer', {partId: 'bottomLeft'}) + * .addView('console', {partId: 'bottom'}) + * .addView('problems', {partId: 'bottom'}) + * .addView('search', {partId: 'bottom'}) + * + * // Navigate views. + * .navigateView('navigator', ['path/to/navigator']) + * .navigateView('explorer', ['path/to/explorer']) + * .navigateView('console', [], {hint: 'console'}) // Set hint to differentiate between routes with an empty path. + * .navigateView('problems', [], {hint: 'problems'}) // Set hint to differentiate between routes with an empty path. + * .navigateView('search', ['path/to/search']) + * + * // Decide which views to activate. + * .activateView('navigator') + * .activateView('explorer') + * .activateView('console'); * } * ``` + * + * The layout requires the following routes. + * + * ```ts + * import {bootstrapApplication} from '@angular/platform-browser'; + * import {provideRouter} from '@angular/router'; + * import {canMatchWorkbenchView} from '@scion/workbench'; + * + * bootstrapApplication(AppComponent, { + * providers: [ + * provideRouter([ + * // Navigator View + * {path: 'path/to/navigator', loadComponent: () => import('./navigator/navigator.component')}, + * // Explorer View + * {path: 'path/to/explorer', loadComponent: () => import('./explorer/explorer.component')}, + * // Search view + * {path: 'path/to/search', loadComponent: () => import('./search/search.component')}, + * // Console view + * {path: '', canMatch: [canMatchWorkbenchView('console')], loadComponent: () => import('./console/console.component')}, + * // Problems view + * {path: '', canMatch: [canMatchWorkbenchView('problems')], loadComponent: () => import('./problems/problems.component')}, + * ]), + * ], + * }); + * ``` */ export type WorkbenchLayoutFn = (factory: WorkbenchLayoutFactory) => Promise | WorkbenchLayout; @@ -121,3 +191,43 @@ export type WorkbenchLayoutFn = (factory: WorkbenchLayoutFactory) => Promise Promise | WorkbenchPerspective | null; + +/** + * Contains different versions of a perspective layout. + * + * The M-prefix indicates this object is a model object that is serialized and stored, requiring migration on breaking change. + * + * @see WORKBENCH_PERSPECTIVE_MODEL_VERSION + */ +export interface MPerspectiveLayout { + /** + * Layout before any user personalization (initial layout). + */ + referenceLayout: { + /** + * @see WorkbenchLayoutSerializer.serializeGrid + * @see WorkbenchLayoutSerializer.deserializeGrid + */ + workbenchGrid: string; + /** + * @see WorkbenchLayoutSerializer.serializeViewOutlets + * @see WorkbenchLayoutSerializer.deserializeViewOutlets + */ + viewOutlets: string; + }; + /** + * Layout personalized by the user. + */ + userLayout: { + /** + * @see WorkbenchLayoutSerializer.serializeGrid + * @see WorkbenchLayoutSerializer.deserializeGrid + */ + workbenchGrid: string; + /** + * @see WorkbenchLayoutSerializer.serializeViewOutlets + * @see WorkbenchLayoutSerializer.deserializeViewOutlets + */ + viewOutlets: string; + }; +} diff --git a/projects/scion/workbench/src/lib/perspective/workbench-perspective.service.ts b/projects/scion/workbench/src/lib/perspective/workbench-perspective.service.ts index f0605096a..335dfc5bb 100644 --- a/projects/scion/workbench/src/lib/perspective/workbench-perspective.service.ts +++ b/projects/scion/workbench/src/lib/perspective/workbench-perspective.service.ts @@ -37,14 +37,14 @@ export class WorkbenchPerspectiveService implements WorkbenchInitializer { } public async init(): Promise { - await this.registerPerspectivesFromModuleConfig(); + await this.registerPerspectivesFromConfig(); await this.registerAnonymousPerspectiveFromWindowName(); } /** - * Registers perspectives configured in {@link WorkbenchModuleConfig}. + * Registers perspectives configured in {@link WorkbenchConfig}. */ - private async registerPerspectivesFromModuleConfig(): Promise { + private async registerPerspectivesFromConfig(): Promise { // Register perspective either from function or object config. if (typeof this._layoutConfig === 'function') { await this.registerPerspective({id: DEFAULT_WORKBENCH_PERSPECTIVE_ID, layout: this._layoutConfig}); @@ -90,13 +90,18 @@ export class WorkbenchPerspectiveService implements WorkbenchInitializer { /** * Switches to the specified perspective. The main area will not change, if any. + * + * @param id - Specifies the id of the perspective to activate. + * @param options - Controls activation of the perspective. + * @param options.storePerspectiveAsActive - Controls if to store the perspective as the active perspective. Default is `true`. + * @return `true` if activated the perspective, otherwise `false`. */ - public async switchPerspective(id: string): Promise { + public async switchPerspective(id: string, options?: {storePerspectiveAsActive?: boolean}): Promise { if (this.activePerspective?.id === id) { return true; } const activated = await this._perspectiveRegistry.get(id).activate(); - if (activated) { + if (activated && (options?.storePerspectiveAsActive ?? true)) { await this._workbenchPerspectiveStorageService.storeActivePerspectiveId(id); window.name = generatePerspectiveWindowName(id); } @@ -144,7 +149,7 @@ export class WorkbenchPerspectiveService implements WorkbenchInitializer { // Select initial perspective. if (initialPerspectiveId) { - await this.switchPerspective(initialPerspectiveId); + await this.switchPerspective(initialPerspectiveId, {storePerspectiveAsActive: false}); } } diff --git a/projects/scion/workbench/src/lib/perspective/workbench-perspective.spec.ts b/projects/scion/workbench/src/lib/perspective/workbench-perspective.spec.ts index d664da061..4f2f466e2 100644 --- a/projects/scion/workbench/src/lib/perspective/workbench-perspective.spec.ts +++ b/projects/scion/workbench/src/lib/perspective/workbench-perspective.spec.ts @@ -9,8 +9,6 @@ */ import {TestBed} from '@angular/core/testing'; -import {WorkbenchTestingModule} from '../testing/workbench-testing.module'; -import {RouterTestingModule} from '@angular/router/testing'; import {styleFixture, waitForInitialWorkbenchLayout, waitUntilStable} from '../testing/testing.util'; import {WorkbenchComponent} from '../workbench.component'; import {WorkbenchService} from '../workbench.service'; @@ -19,12 +17,14 @@ import {By} from '@angular/platform-browser'; import {inject} from '@angular/core'; import {expect} from '../testing/jasmine/matcher/custom-matchers.definition'; import {WorkbenchLayoutComponent} from '../layout/workbench-layout.component'; -import {MPart, MTreeNode} from '../layout/workbench-layout.model'; import {delay, of} from 'rxjs'; -import {toEqualWorkbenchLayoutCustomMatcher} from '../testing/jasmine/matcher/to-equal-workbench-layout.matcher'; +import {MPart, MTreeNode, toEqualWorkbenchLayoutCustomMatcher} from '../testing/jasmine/matcher/to-equal-workbench-layout.matcher'; import {MAIN_AREA} from '../layout/workbench-layout'; import {WorkbenchLayoutFactory} from '../layout/workbench-layout.factory'; import {WorkbenchRouter} from '../routing/workbench-router.service'; +import {provideRouter} from '@angular/router'; +import {provideWorkbenchForTest} from '../testing/workbench.provider'; +import {canMatchWorkbenchView} from '../view/workbench-view-route-guards'; describe('Workbench Perspective', () => { @@ -32,8 +32,8 @@ describe('Workbench Perspective', () => { it('should support configuring different start page per perspective', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({ + providers: [ + provideWorkbenchForTest({ layout: { perspectives: [ {id: 'perspective-1', layout: (factory: WorkbenchLayoutFactory) => factory.addPart(MAIN_AREA)}, @@ -41,7 +41,7 @@ describe('Workbench Perspective', () => { ], }, }), - RouterTestingModule.withRoutes([ + provideRouter([ { path: '', loadComponent: () => import('../testing/test.component'), @@ -78,17 +78,18 @@ describe('Workbench Perspective', () => { */ it('should display the perspective also for asynchronous/slow initial navigation', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({ + providers: [ + provideWorkbenchForTest({ layout: factory => factory .addPart(MAIN_AREA) .addPart('left', {relativeTo: MAIN_AREA, align: 'left', ratio: .25}) - .addView('navigator', {partId: 'left', activateView: true}), + .addView('view.101', {partId: 'left', activateView: true}) + .navigateView('view.101', [], {hint: 'navigator'}), }), - RouterTestingModule.withRoutes([ + provideRouter([ { path: '', - outlet: 'navigator', + canMatch: [canMatchWorkbenchView('navigator')], component: TestComponent, canActivate: [ () => of(true).pipe(delay(1000)), // simulate slow initial navigation @@ -104,7 +105,7 @@ describe('Workbench Perspective', () => { expect(fixture.debugElement.query(By.directive(WorkbenchLayoutComponent))).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ - child1: new MPart({id: 'left', views: [{id: 'navigator'}], activeViewId: 'navigator'}), + child1: new MPart({id: 'left', views: [{id: 'view.101'}], activeViewId: 'view.101'}), child2: new MPart({id: MAIN_AREA}), direction: 'row', ratio: .25, @@ -113,20 +114,22 @@ describe('Workbench Perspective', () => { }); }); - it('should open an unnamed view in the active part of perspective without main area', async () => { + it('should open a empty-path view in the active part of perspective without main area', async () => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest({ + providers: [ + provideWorkbenchForTest({ layout: factory => factory .addPart('left') .addPart('right', {align: 'right'}) - .addView('list', {partId: 'left', activateView: true}) - .addView('overview', {partId: 'right', activateView: true}) + .addView('view.101', {partId: 'left', activateView: true}) + .addView('view.102', {partId: 'right', activateView: true}) + .navigateView('view.101', [], {hint: 'list'}) + .navigateView('view.102', [], {hint: 'overview'}) .activatePart('right'), }), - RouterTestingModule.withRoutes([ - {path: '', outlet: 'list', component: TestComponent}, - {path: '', outlet: 'overview', component: TestComponent}, + provideRouter([ + {path: '', canMatch: [canMatchWorkbenchView('list')], component: TestComponent}, + {path: '', canMatch: [canMatchWorkbenchView('overview')], component: TestComponent}, {path: 'details/:id', component: TestComponent}, ]), ], @@ -138,8 +141,8 @@ describe('Workbench Perspective', () => { expect(fixture.debugElement.query(By.directive(WorkbenchLayoutComponent))).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ - child1: new MPart({id: 'left', views: [{id: 'list'}], activeViewId: 'list'}), - child2: new MPart({id: 'right', views: [{id: 'overview'}], activeViewId: 'overview'}), + child1: new MPart({id: 'left', views: [{id: 'view.101'}], activeViewId: 'view.101'}), + child2: new MPart({id: 'right', views: [{id: 'view.102'}], activeViewId: 'view.102'}), direction: 'row', ratio: .5, }), @@ -150,12 +153,12 @@ describe('Workbench Perspective', () => { await TestBed.inject(WorkbenchRouter).navigate(['details/1']); await waitUntilStable(); - // unnamed view should be opened in the active part (right) of the workbench grid + // empty-path view should be opened in the active part (right) of the workbench grid expect(fixture.debugElement.query(By.directive(WorkbenchLayoutComponent))).toEqualWorkbenchLayout({ workbenchGrid: { root: new MTreeNode({ - child1: new MPart({id: 'left', views: [{id: 'list'}], activeViewId: 'list'}), - child2: new MPart({id: 'right', views: [{id: 'overview'}, {id: 'view.1'}], activeViewId: 'view.1'}), + child1: new MPart({id: 'left', views: [{id: 'view.101'}], activeViewId: 'view.101'}), + child2: new MPart({id: 'right', views: [{id: 'view.102'}, {id: 'view.1'}], activeViewId: 'view.1'}), direction: 'row', ratio: .5, }), diff --git a/projects/scion/workbench/src/lib/perspective/workench-perspective-serializer.service.ts b/projects/scion/workbench/src/lib/perspective/workench-perspective-serializer.service.ts new file mode 100644 index 000000000..1abdb9269 --- /dev/null +++ b/projects/scion/workbench/src/lib/perspective/workench-perspective-serializer.service.ts @@ -0,0 +1,62 @@ +/* + * 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 + */ + +import {inject, Injectable} from '@angular/core'; +import {MPerspectiveLayout} from '../perspective/workbench-perspective.model'; +import {WorkbenchMigrator} from '../migration/workbench-migrator'; +import {WorkbenchPerspectiveMigrationV2} from './migration/workbench-perspective-migration-v2.service'; + +/** + * Serializes and deserializes a base64-encoded JSON into a {@link MPerspectiveLayout}. + */ +@Injectable({providedIn: 'root'}) +export class WorkbenchPerspectiveSerializer { + + private _workbenchPerspectiveMigrator = new WorkbenchMigrator() + .registerMigration(1, inject(WorkbenchPerspectiveMigrationV2)); + + /** + * Serializes the given perspective layout into a URL-safe base64 string. + */ + public serialize(data: MPerspectiveLayout): string { + const json = JSON.stringify(data); + return window.btoa(`${json}${VERSION_SEPARATOR}${WORKBENCH_PERSPECTIVE_LAYOUT_VERSION}`); + } + + /** + * Deserializes the given base64-serialized perspective layout, applying necessary migrations if the serialized layout is outdated. + */ + public deserialize(serialized: string | null | undefined): MPerspectiveLayout | null { + if (!serialized?.length) { + return null; + } + + const [json, version] = window.atob(serialized).split(VERSION_SEPARATOR, 2); + const serializedVersion = Number.isNaN(Number(version)) ? 1 : Number(version); + const migrated = this._workbenchPerspectiveMigrator.migrate(json, {from: serializedVersion, to: WORKBENCH_PERSPECTIVE_LAYOUT_VERSION}); + return JSON.parse(migrated); + } +} + +/** + * Represents the current version of the workbench perspective layout. + * + * Increment this version and write a migrator when introducting a breaking change. + * + * @see WorkbenchMigrator + */ +export const WORKBENCH_PERSPECTIVE_LAYOUT_VERSION = 2; + +/** + * Separates the serialized JSON model and its version in the base64-encoded string. + * + * Format: // + */ +const VERSION_SEPARATOR = '//'; diff --git "a/projects/scion/workbench/src/lib/perspective/\311\265workbench-perspective.model.ts" "b/projects/scion/workbench/src/lib/perspective/\311\265workbench-perspective.model.ts" index 4a4d12c21..4107e7738 100644 --- "a/projects/scion/workbench/src/lib/perspective/\311\265workbench-perspective.model.ts" +++ "b/projects/scion/workbench/src/lib/perspective/\311\265workbench-perspective.model.ts" @@ -8,22 +8,21 @@ * SPDX-License-Identifier: EPL-2.0 */ import {ɵWorkbenchLayoutFactory} from '../layout/ɵworkbench-layout.factory'; -import {MPartGrid} from '../layout/workbench-layout.model'; import {EnvironmentInjector, inject, InjectionToken, runInInjectionContext} from '@angular/core'; import {WorkbenchLayoutFn, WorkbenchPerspective, WorkbenchPerspectiveDefinition} from './workbench-perspective.model'; -import {BehaviorSubject, Observable, Subject} from 'rxjs'; -import {WorkbenchNavigation, WorkbenchRouter} from '../routing/workbench-router.service'; +import {BehaviorSubject, Observable} from 'rxjs'; +import {ɵWorkbenchRouter} from '../routing/ɵworkbench-router.service'; import {ɵWorkbenchLayout} from '../layout/ɵworkbench-layout'; import {WorkbenchGridMerger} from './workbench-grid-merger.service'; import {WorkbenchPerspectiveStorageService} from './workbench-perspective-storage.service'; import {WorkbenchLayoutService} from '../layout/workbench-layout.service'; -import {filter, map, takeUntil} from 'rxjs/operators'; -import {WorkbenchLayoutSerializer} from '../layout/workench-layout-serializer.service'; -import {RouterUtils} from '../routing/router.util'; -import {Router} from '@angular/router'; +import {filter, map} from 'rxjs/operators'; import {WorkbenchPerspectiveViewConflictResolver} from './workbench-perspective-view-conflict-resolver.service'; import {serializeExecution} from '../common/operators'; -import {Commands} from '../routing/routing.model'; +import {UrlSegment} from '@angular/router'; +import {MAIN_AREA} from '../layout/workbench-layout'; +import {ɵDestroyRef} from '../common/ɵdestroy-ref'; +import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; /** * DI token that holds the identity of the active perspective. @@ -42,18 +41,15 @@ export class ɵWorkbenchPerspective implements WorkbenchPerspective { private _workbenchGridMerger = inject(WorkbenchGridMerger); private _workbenchPerspectiveStorageService = inject(WorkbenchPerspectiveStorageService); private _workbenchLayoutService = inject(WorkbenchLayoutService); - private _workbenchLayoutSerializer = inject(WorkbenchLayoutSerializer); - private _workbenchRouter = inject(WorkbenchRouter); - private _router = inject(Router); + private _workbenchRouter = inject(ɵWorkbenchRouter); private _environmentInjector = inject(EnvironmentInjector); private _initialLayoutFn: WorkbenchLayoutFn; private _activePerspectiveId$ = inject(ACTIVE_PERSPECTIVE_ID$); private _perspectiveViewConflictResolver = inject(WorkbenchPerspectiveViewConflictResolver); - private _destroy$ = new Subject(); + private _destroyRef = new ɵDestroyRef(); - private _initialWorkbenchGrid: MPartGrid | undefined; - private _workbenchGrid: MPartGrid | undefined; - private _viewOutlets: {[viewId: string]: Commands} = {}; + private _initialPerspectiveLayout: ɵWorkbenchLayout | undefined; + private _perspectiveLayout: ɵWorkbenchLayout | undefined; public id: string; public transient: boolean; @@ -66,7 +62,7 @@ export class ɵWorkbenchPerspective implements WorkbenchPerspective { this.data = definition.data ?? {}; this.active$ = this._activePerspectiveId$.pipe(map(activePerspectiveId => activePerspectiveId === this.id)); this._initialLayoutFn = definition.layout; - this.onLayoutChange(layout => this.storePerspectiveLayout(layout)); + this.onPerspectiveLayoutChange(layout => this.storePerspectiveLayout(layout)); } /** @@ -74,28 +70,16 @@ export class ɵWorkbenchPerspective implements WorkbenchPerspective { */ public async activate(): Promise { // Create the initial workbench grid when constructed for the first time. - this._initialWorkbenchGrid ??= await this.createInitialWorkbenchGrid(); - - // Load perspective data from storage. - const perspectiveData = !this.transient ? await this._workbenchPerspectiveStorageService.loadPerspectiveData(this.id) : null; - if (perspectiveData) { - this._workbenchGrid = this._workbenchGridMerger.merge({ - local: this._workbenchLayoutFactory.create({workbenchGrid: perspectiveData.workbenchGrid}).workbenchGrid, - base: this._workbenchLayoutFactory.create({workbenchGrid: perspectiveData.initialWorkbenchGrid}).workbenchGrid, - remote: this._initialWorkbenchGrid, - }); - this._viewOutlets = perspectiveData.viewOutlets; - } - else { - this._workbenchGrid ??= this._initialWorkbenchGrid; - this._viewOutlets ??= {}; - } + this._initialPerspectiveLayout ??= await this.createInitialPerspectiveLayout(); + + // Load the layout from the storage, if present, or use the initial layout otherwise. + this._perspectiveLayout = (await this.loadPerspectiveLayout()) ?? this._initialPerspectiveLayout; // Memoize currently active perspective for a potential rollback in case the activation fails. const currentActivePerspectiveId = this._activePerspectiveId$.value; // Perform navigation to activate the layout of this perspective. - const navigated = await this._workbenchRouter.ɵnavigate(currentLayout => { + const navigated = await this._workbenchRouter.navigate(currentLayout => { // Mark this perspective as active after the initial navigation (1) but before the actual Angular routing (2). // // (1) Otherwise, if the initial navigation is asynchronous, such as when lazy loading components or using asynchronous guards, @@ -103,8 +87,8 @@ export class ɵWorkbenchPerspective implements WorkbenchPerspective { // (2) Enables routes to evaluate the active perspective in a `canMatch` guard, e.g., to display a perspective-specific start page. this._activePerspectiveId$.next(this.id); - // Apply the layout of this perspective. - return this.createActivationNavigation(currentLayout); + // Create layout with the workbench grid of this perspective and the main area of the current layout. + return this.createLayoutForActivation(currentLayout); }); if (!navigated) { this._activePerspectiveId$.next(currentActivePerspectiveId); @@ -116,50 +100,53 @@ export class ɵWorkbenchPerspective implements WorkbenchPerspective { * Resets this perspective to its initial layout. */ public async reset(): Promise { - this._workbenchGrid = this._initialWorkbenchGrid; - this._viewOutlets = {}; + this._perspectiveLayout = this._initialPerspectiveLayout; - // Apply the initial perspective layout. - await this._workbenchRouter.ɵnavigate(currentLayout => this.createActivationNavigation(currentLayout)); + // Reset to the initial layout. + await this._workbenchRouter.navigate(currentLayout => this.createLayoutForActivation(currentLayout)); } /** - * Creates the {@link WorkbenchNavigation} object to activate this perspective. + * Creates layout with the workbench grid of this perspective and the main area of the current layout. * - * When switching perspective, name clashes between the views contained in the perspective - * and the views contained in the main area are possible. The navigation detects and resolves name conflicts, + * When switching perspective, id clashes between the views contained in the perspective and the + * views contained in the main area are possible. The activation detects and resolves conflicts, * changing the layout of this perspective if necessary. */ - private createActivationNavigation(currentLayout: ɵWorkbenchLayout): WorkbenchNavigation { - if (!this._workbenchGrid) { - throw Error('[WorkbenchPerspectiveError] Perspective not yet constructed.'); + private createLayoutForActivation(currentLayout: ɵWorkbenchLayout): ɵWorkbenchLayout { + if (!this._perspectiveLayout) { + throw Error(`[PerspectiveActivateError] Perspective '${this.id}' not constructed.`); } - // If the current layout has a main area, resolve name clashes between views of this perspective and views contained in the main area. - if (currentLayout.mainAreaGrid) { - const resolved = this._perspectiveViewConflictResolver.resolve(currentLayout.mainAreaGrid, {workbenchGrid: this._workbenchGrid, viewOutlets: this._viewOutlets}); - this._workbenchGrid = resolved.workbenchGrid; - this._viewOutlets = resolved.viewOutlets; + // View outlets of the new layout. + const viewOutlets = new Map(); + + // Detect and resolve id clashes between views defined by this perspective and views contained in the main area, + // assigning views of this perspective a new identity. + if (currentLayout.hasPart(MAIN_AREA, {grid: 'workbench'}) && this._perspectiveLayout.hasPart(MAIN_AREA, {grid: 'workbench'})) { + this._perspectiveLayout = this._perspectiveViewConflictResolver.resolve(currentLayout, this._perspectiveLayout); + } + + // Add view outlets of views contained in the main area. + if (currentLayout.hasPart(MAIN_AREA, {grid: 'workbench'}) && this._perspectiveLayout.hasPart(MAIN_AREA, {grid: 'workbench'})) { + Object.entries(currentLayout.viewOutlets({grid: 'mainArea'})).forEach(([viewId, segments]) => { + viewOutlets.set(viewId, segments); + }); } - const newLayout = this._workbenchLayoutFactory.create({ - workbenchGrid: this._workbenchGrid, + // Add view outlets of views contained in this perspective. + Object.entries(this._perspectiveLayout.viewOutlets()).forEach(([viewId, segments]) => { + viewOutlets.set(viewId, segments); + }); + + // Create the layout for this perspective. + return this._workbenchLayoutFactory.create({ + workbenchGrid: this._perspectiveLayout.workbenchGrid, mainAreaGrid: currentLayout.mainAreaGrid, + viewOutlets: Object.fromEntries(viewOutlets), + viewStates: currentLayout.viewStates({grid: 'mainArea'}), // preserve view state of views in main area; view state of perspective cannot be restored since not persisted // Do not preserve maximized state when switching between perspectives. }); - - // Preserve view outlets defined in current layout's main area only if new layout contains a main area. - const outletsToRemove = newLayout.mainAreaGrid ? currentLayout.views({grid: 'workbench'}) : currentLayout.views(); - - return { - layout: newLayout, - viewOutlets: { - // Remove outlets of current perspective from the URL. - ...RouterUtils.outletsFromCurrentUrl(this._router, outletsToRemove.map(view => view.id), () => null), - // Add outlets of the perspective to activate to the URL. - ...this._viewOutlets, - }, - }; } public get active(): boolean { @@ -167,47 +154,85 @@ export class ɵWorkbenchPerspective implements WorkbenchPerspective { } /** - * Creates the initial workbench grid of this perspective as defined in the perspective definition. + * Creates the initial layout of this perspective as defined in the perspective definition. */ - private async createInitialWorkbenchGrid(): Promise { - const layout = await runInInjectionContext(this._environmentInjector, () => this._initialLayoutFn(this._workbenchLayoutFactory)); - return (layout as ɵWorkbenchLayout).workbenchGrid; + private async createInitialPerspectiveLayout(): Promise<ɵWorkbenchLayout> { + return await runInInjectionContext(this._environmentInjector, () => this._initialLayoutFn(this._workbenchLayoutFactory)) as ɵWorkbenchLayout; } /** - * Invokes the callback when the layout of this perspective changes. + * Subscribes to workbench layout changes, invoking the given callback on layout change, but only if this perspective is active. */ - private onLayoutChange(callback: (layout: ɵWorkbenchLayout) => Promise): void { + private onPerspectiveLayoutChange(callback: (layout: ɵWorkbenchLayout) => Promise): void { this._workbenchLayoutService.layout$ .pipe( filter(() => this.active), serializeExecution(callback), - takeUntil(this._destroy$), + takeUntilDestroyed(this._destroyRef), ) .subscribe(); } + /** + * Loads the layout of this perspective from storage, applying necessary migrations if the layout is outdated. + * Returns `null` if not stored or could not be deserialized. + */ + private async loadPerspectiveLayout(): Promise<ɵWorkbenchLayout | null> { + if (this.transient) { + return this._perspectiveLayout ?? null; + } + + const perspectiveLayout = await this._workbenchPerspectiveStorageService.loadPerspectiveLayout(this.id); + if (!perspectiveLayout) { + return null; + } + + return this._workbenchGridMerger.merge({ + local: this._workbenchLayoutFactory.create({ + workbenchGrid: perspectiveLayout.userLayout.workbenchGrid, + viewOutlets: perspectiveLayout.userLayout.viewOutlets, + }), + base: this._workbenchLayoutFactory.create({ + workbenchGrid: perspectiveLayout.referenceLayout.workbenchGrid, + viewOutlets: perspectiveLayout.referenceLayout.viewOutlets, + }), + remote: this._initialPerspectiveLayout!, + }); + } + /** * Stores the layout of this perspective. * * If an anonymous perspective, only memoizes the layout, but does not write it to storage. */ - private async storePerspectiveLayout(layout: ɵWorkbenchLayout): Promise { - // Memoize layout and outlets. - this._workbenchGrid = layout.workbenchGrid; - this._viewOutlets = RouterUtils.outletsFromCurrentUrl(this._router, layout.views({grid: 'workbench'}).map(view => view.id)); - - // Store the layout if not a transient perspective. - if (!this.transient) { - await this._workbenchPerspectiveStorageService.storePerspectiveData(this.id, { - initialWorkbenchGrid: this._workbenchLayoutSerializer.serialize(this._initialWorkbenchGrid), - workbenchGrid: this._workbenchLayoutSerializer.serialize(this._workbenchGrid), - viewOutlets: this._viewOutlets, - }); + private async storePerspectiveLayout(currentLayout: ɵWorkbenchLayout): Promise { + // Memoize the layout of this perspective. + this._perspectiveLayout = this._workbenchLayoutFactory.create({ + workbenchGrid: currentLayout.workbenchGrid, + viewOutlets: currentLayout.viewOutlets({grid: 'workbench'}), + }); + + // Do not store the layout if a transient perspective. + if (this.transient) { + return; } + + const serializedReferenceLayout = this._initialPerspectiveLayout!.serialize(); + const serializedUserLayout = this._perspectiveLayout.serialize(); + + await this._workbenchPerspectiveStorageService.storePerspectiveLayout(this.id, { + referenceLayout: { + workbenchGrid: serializedReferenceLayout.workbenchGrid, + viewOutlets: serializedReferenceLayout.workbenchViewOutlets, + }, + userLayout: { + workbenchGrid: serializedUserLayout.workbenchGrid, + viewOutlets: serializedUserLayout.workbenchViewOutlets, + }, + }); } public destroy(): void { - this._destroy$.next(); + this._destroyRef.destroy(); } } diff --git a/projects/scion/workbench/src/lib/popup/popup.config.ts b/projects/scion/workbench/src/lib/popup/popup.config.ts index cb0804dec..fe2f6a1f2 100644 --- a/projects/scion/workbench/src/lib/popup/popup.config.ts +++ b/projects/scion/workbench/src/lib/popup/popup.config.ts @@ -18,6 +18,7 @@ import {map} from 'rxjs/operators'; import {ɵDestroyRef} from '../common/ɵdestroy-ref'; import {ɵWorkbenchDialog} from '../dialog/ɵworkbench-dialog'; import {Blockable} from '../glass-pane/blockable'; +import {ViewId} from '../view/workbench-view.model'; /** * Configures the content to be displayed in a popup. @@ -105,7 +106,7 @@ export abstract class PopupConfig { */ public readonly size?: PopupSize; /** - * Specifies CSS class(es) to be added to the popup, useful in end-to-end tests for locating the popup. + * Specifies CSS class(es) to add to the popup, e.g., to locate the popup in tests. */ public readonly cssClass?: string | string[]; /** @@ -120,7 +121,7 @@ export abstract class PopupConfig { * By default, if opening the popup in the context of a view, that view is used as the popup's contextual view. * If you set the view id to `null`, the popup will open without referring to the contextual view. */ - viewId?: string | null; + viewId?: ViewId | null; }; } @@ -296,5 +297,5 @@ export interface PopupReferrer { /** * Identity of the view if opened in the context of a view. */ - viewId?: string; + viewId?: ViewId; } diff --git a/projects/scion/workbench/src/lib/portal/workbench-portal-outlet.directive.ts b/projects/scion/workbench/src/lib/portal/workbench-portal-outlet.directive.ts index 563ae1600..d66a84867 100644 --- a/projects/scion/workbench/src/lib/portal/workbench-portal-outlet.directive.ts +++ b/projects/scion/workbench/src/lib/portal/workbench-portal-outlet.directive.ts @@ -20,9 +20,9 @@ import {WbComponentPortal} from './wb-component-portal'; * Usage: * * ```html - * + * * - * + * * ```` * * @see WbComponentPortal diff --git a/projects/scion/workbench/src/lib/public_api.ts b/projects/scion/workbench/src/lib/public_api.ts index e484575c1..db73a0989 100644 --- a/projects/scion/workbench/src/lib/public_api.ts +++ b/projects/scion/workbench/src/lib/public_api.ts @@ -8,11 +8,12 @@ * SPDX-License-Identifier: EPL-2.0 */ -export {WorkbenchModuleConfig, MenuItemConfig, ViewMenuItemsConfig} from './workbench-module-config'; -export {WorkbenchModule} from './workbench.module'; +export {WorkbenchConfig, MenuItemConfig, ViewMenuItemsConfig} from './workbench-config'; +export {WorkbenchModule, WorkbenchModuleConfig} from './workbench.module'; +export {provideWorkbench} from './workbench.provider'; export {WorkbenchService} from './workbench.service'; export {WORKBENCH_ID} from './workbench-id'; -export {WorkbenchViewPreDestroy, WorkbenchPartAction, WorkbenchTheme, CanMatchPartFn, WorkbenchMenuItem, WorkbenchMenuItemFactoryFn} from './workbench.model'; +export {WorkbenchPartAction, WorkbenchTheme, CanMatchPartFn, WorkbenchMenuItem, WorkbenchMenuItemFactoryFn, CanClose} from './workbench.model'; export {WorkbenchComponent} from './workbench.component'; export {VIEW_TAB_RENDERING_CONTEXT, ViewTabRenderingContext} from './workbench.constants'; diff --git a/projects/scion/workbench/src/lib/registry/workbench-object-registry.ts b/projects/scion/workbench/src/lib/registry/workbench-object-registry.ts index 9d085e804..3d5e70cb8 100644 --- a/projects/scion/workbench/src/lib/registry/workbench-object-registry.ts +++ b/projects/scion/workbench/src/lib/registry/workbench-object-registry.ts @@ -29,8 +29,8 @@ export class WorkbenchObjectRegistry { * Creates an instance of the registry. * * @param config - Controls the creation of the registry. - * @property keyFn - Function to extract the key of an object. - * @property nullObjectErrorFn - Function to provide an error when looking up an object not contained in the registry. + * @param config.keyFn - Function to extract the key of an object. + * @param config.nullObjectErrorFn - Function to provide an error when looking up an object not contained in the registry. */ constructor(config: {keyFn: (object: T) => KEY; nullObjectErrorFn: (key: KEY) => Error}) { this._keyFn = config.keyFn; diff --git a/projects/scion/workbench/src/lib/routing/empty-outlet/empty-outlet.component.html b/projects/scion/workbench/src/lib/routing/empty-outlet/empty-outlet.component.html new file mode 100644 index 000000000..7dd570e8c --- /dev/null +++ b/projects/scion/workbench/src/lib/routing/empty-outlet/empty-outlet.component.html @@ -0,0 +1 @@ + diff --git a/projects/scion/workbench/src/lib/routing/empty-outlet/empty-outlet.component.scss b/projects/scion/workbench/src/lib/routing/empty-outlet/empty-outlet.component.scss new file mode 100644 index 000000000..7a131cd50 --- /dev/null +++ b/projects/scion/workbench/src/lib/routing/empty-outlet/empty-outlet.component.scss @@ -0,0 +1,7 @@ +:host { + display: grid; + + > router-outlet { + position: absolute; // out of document flow + } +} diff --git a/projects/scion/workbench/src/lib/routing/empty-outlet/empty-outlet.component.ts b/projects/scion/workbench/src/lib/routing/empty-outlet/empty-outlet.component.ts new file mode 100644 index 000000000..4e0f62b23 --- /dev/null +++ b/projects/scion/workbench/src/lib/routing/empty-outlet/empty-outlet.component.ts @@ -0,0 +1,31 @@ +/* + * 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 + */ + +import {Component} from '@angular/core'; +import {RouterOutlet} from '@angular/router'; + +/** + * 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. + */ +@Component({ + templateUrl: './empty-outlet.component.html', + styleUrls: ['./empty-outlet.component.scss'], + standalone: true, + imports: [RouterOutlet], +}) +export class ɵEmptyOutletComponent { +} diff --git a/projects/scion/workbench/src/lib/routing/public_api.ts b/projects/scion/workbench/src/lib/routing/public_api.ts index 3e8192ebf..248505859 100644 --- a/projects/scion/workbench/src/lib/routing/public_api.ts +++ b/projects/scion/workbench/src/lib/routing/public_api.ts @@ -8,8 +8,8 @@ * SPDX-License-Identifier: EPL-2.0 */ -export {WorkbenchRouter, WorkbenchNavigationExtras} from './workbench-router.service'; +export {WorkbenchRouter} from './workbench-router.service'; export {WorkbenchRouterLinkDirective} from './workbench-router-link.directive'; export {WorkbenchAuxiliaryRoutesRegistrator} from './workbench-auxiliary-routes-registrator.service'; export {WorkbenchRouteData} from './workbench-route-data'; -export {Commands, ViewState} from './routing.model'; +export {Commands, ViewState, WorkbenchNavigationExtras, NavigateFn} from './routing.model'; diff --git a/projects/scion/workbench/src/lib/routing/route-resolve.spec.ts b/projects/scion/workbench/src/lib/routing/route-resolve.spec.ts deleted file mode 100644 index 3e5604ce5..000000000 --- a/projects/scion/workbench/src/lib/routing/route-resolve.spec.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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 - */ - -import {ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, waitForAsync} from '@angular/core/testing'; -import {Component} from '@angular/core'; -import {WorkbenchRouter} from './workbench-router.service'; -import {WorkbenchView} from '../view/workbench-view.model'; -import {advance, styleFixture} from '../testing/testing.util'; -import {WorkbenchComponent} from '../workbench.component'; -import {WorkbenchTestingModule} from '../testing/workbench-testing.module'; -import {RouterTestingModule} from '@angular/router/testing'; - -describe('WorkbenchRouter', () => { - - let fixture: ComponentFixture; - let workbenchRouter: WorkbenchRouter; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest(), - RouterTestingModule.withRoutes([ - {path: 'path/to/view', component: ViewComponent}, - {path: 'path/to/view-1', component: ViewComponent}, - {path: 'path/to/view-2', component: ViewComponent}, - {path: 'path/to/view-3', component: ViewComponent}, - {path: 'path', component: ViewComponent}, - {path: 'path/:segment1', component: ViewComponent}, - {path: 'path/:segment1/:segment2', component: ViewComponent}, - ]), - ], - }); - fixture = styleFixture(TestBed.createComponent(WorkbenchComponent)); - workbenchRouter = TestBed.inject(WorkbenchRouter); - })); - - it('resolves present views by path', fakeAsync(() => { - // Add View 1 - workbenchRouter.navigate(['path', 'to', 'view-1']).then(); - advance(fixture); - - // Add View 1 again - workbenchRouter.navigate(['path', 'to', 'view-1'], {target: 'blank'}).then(); - advance(fixture); - - // Add View 2 - workbenchRouter.navigate(['path', 'to', 'view-2']).then(); - advance(fixture); - - // Add View 2 again (activate) - workbenchRouter.navigate(['path', 'to', 'view-2']).then(); - advance(fixture); - - // Add View 3 - workbenchRouter.navigate(['path', 'to', 'view-3']).then(); - advance(fixture); - - expect(workbenchRouter.resolvePresentViewIds(['path', 'to', 'view-1'])).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2'])); - expect(workbenchRouter.resolvePresentViewIds(['path', 'to', 'view-2'])).toEqual(['view.3']); - expect(workbenchRouter.resolvePresentViewIds(['path', 'to', 'view-3'])).toEqual(['view.4']); - - discardPeriodicTasks(); - })); - - it('resolves present views by path and ignores matrix params', fakeAsync(() => { - // Add View 1 - workbenchRouter.navigate(['path', 'to', 'view', {'matrixParam': 'A'}], {target: 'blank'}).then(); - advance(fixture); - - // Add View 1 again (existing view is activated) - workbenchRouter.navigate(['path', 'to', 'view', {'matrixParam': 'A'}]).then(); - advance(fixture); - - // Add View 2 (new view is created, because target is 'blank') - workbenchRouter.navigate(['path', 'to', 'view', {'matrixParam': 'B'}], {target: 'blank'}).then(); - advance(fixture); - - // Update matrix param (both views are updated) - workbenchRouter.navigate(['path', 'to', 'view', {'matrixParam': 'B'}]).then(); - advance(fixture); - - // Update matrix param (both views are updated) - workbenchRouter.navigate(['path', 'to', 'view', {'matrixParam': 'C'}]).then(); - advance(fixture); - - expect(workbenchRouter.resolvePresentViewIds(['path', 'to', 'view'])).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2'])); - expect(workbenchRouter.resolvePresentViewIds(['path', 'to', 'view', {'matrixParam': 'A'}])).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2'])); - expect(workbenchRouter.resolvePresentViewIds(['path', 'to', 'view', {'matrixParam': 'B'}])).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2'])); - expect(workbenchRouter.resolvePresentViewIds(['path', 'to', 'view', {'matrixParam': 'C'}])).toEqual(jasmine.arrayWithExactContents(['view.1', 'view.2'])); - - discardPeriodicTasks(); - })); - - it('resolves present views by path containing wildcards', fakeAsync(() => { - // Add View 1 - workbenchRouter.navigate(['path'], {target: 'blank'}).then(); - advance(fixture); - - // Add View 2 - workbenchRouter.navigate(['path', 1], {target: 'blank'}).then(); - advance(fixture); - - // Add View 3 - workbenchRouter.navigate(['path', 2], {target: 'blank'}).then(); - advance(fixture); - - // Add View 4 - workbenchRouter.navigate(['path', 1, 1], {target: 'blank'}).then(); - advance(fixture); - - // Add View 5 - workbenchRouter.navigate(['path', 1, 2], {target: 'blank'}).then(); - advance(fixture); - - // Add View 6 - workbenchRouter.navigate(['path', 2, 1], {target: 'blank'}).then(); - advance(fixture); - - // Add View 7 - workbenchRouter.navigate(['path', 2, 2], {target: 'blank'}).then(); - advance(fixture); - - // Wildcard match is disabled by default - expect(workbenchRouter.resolvePresentViewIds(['path'])).toEqual(['view.1']); - expect(workbenchRouter.resolvePresentViewIds(['path', '*'])).toEqual([]); - expect(workbenchRouter.resolvePresentViewIds(['path', 1, '*'])).toEqual([]); - expect(workbenchRouter.resolvePresentViewIds(['path', '*', 1])).toEqual([]); - expect(workbenchRouter.resolvePresentViewIds(['path', '*', '*'])).toEqual([]); - - // Set `matchWildcardSegments` option to `true` - expect(workbenchRouter.resolvePresentViewIds(['path'], {matchWildcardSegments: true})).toEqual(['view.1']); - expect(workbenchRouter.resolvePresentViewIds(['path', '*'], {matchWildcardSegments: true})).toEqual(jasmine.arrayWithExactContents(['view.2', 'view.3'])); - expect(workbenchRouter.resolvePresentViewIds(['path', 1, '*'], {matchWildcardSegments: true})).toEqual(jasmine.arrayWithExactContents(['view.4', 'view.5'])); - expect(workbenchRouter.resolvePresentViewIds(['path', '*', 1], {matchWildcardSegments: true})).toEqual(jasmine.arrayWithExactContents(['view.4', 'view.6'])); - expect(workbenchRouter.resolvePresentViewIds(['path', '*', '*'], {matchWildcardSegments: true})).toEqual(jasmine.arrayWithExactContents(['view.4', 'view.5', 'view.6', 'view.7'])); - - discardPeriodicTasks(); - })); -}); - -/**************************************************************************************************** - * Definition of App Test Module * - ****************************************************************************************************/ -@Component({selector: 'spec-view', template: '{{view.id}}', standalone: true}) -class ViewComponent { - - constructor(public view: WorkbenchView) { - } -} diff --git a/projects/scion/workbench/src/lib/routing/router-navigate.spec.ts b/projects/scion/workbench/src/lib/routing/router-navigate.spec.ts index 8ddc6869c..f7ad3c839 100644 --- a/projects/scion/workbench/src/lib/routing/router-navigate.spec.ts +++ b/projects/scion/workbench/src/lib/routing/router-navigate.spec.ts @@ -9,31 +9,29 @@ */ import {discardPeriodicTasks, fakeAsync, TestBed} from '@angular/core/testing'; -import {Component, NgModule} from '@angular/core'; -import {RouterModule} from '@angular/router'; +import {Component} from '@angular/core'; +import {provideRouter, Routes} from '@angular/router'; import {WorkbenchRouter} from './workbench-router.service'; -import {CommonModule} from '@angular/common'; import {expect} from '../testing/jasmine/matcher/custom-matchers.definition'; import {toShowCustomMatcher} from '../testing/jasmine/matcher/to-show.matcher'; import {advance, clickElement, styleFixture} from '../testing/testing.util'; -import {WorkbenchTestingModule} from '../testing/workbench-testing.module'; import {WorkbenchComponent} from '../workbench.component'; -import {RouterTestingModule} from '@angular/router/testing'; import {WorkbenchRouterLinkDirective} from '../routing/workbench-router-link.directive'; +import {provideWorkbenchForTest} from '../testing/workbench.provider'; /** * Test setup: * * - * +--------------+ - * | Test Module | - * +--------------+ + * +-------------+ + * | Application | + * +-------------+ * | * feature-a (route) * | * v * +-------------------------------------+ - * | Feature Module A | + * | Feature A | * |-------------------------------------| * | routes: | * | | @@ -46,7 +44,7 @@ import {WorkbenchRouterLinkDirective} from '../routing/workbench-router-link.dir * | * v * +-------------------------------------+ - * | Feature Module B | + * | Feature B | * |-------------------------------------| * | routes: | * | | @@ -64,17 +62,17 @@ describe('Router', () => { it('allows for relative and absolute navigation', fakeAsync(() => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest(), - RouterTestingModule.withRoutes([ - {path: 'feature-a', loadChildren: () => FeatureAModule}, + providers: [ + provideWorkbenchForTest(), + provideRouter([ + {path: 'feature-a', loadChildren: () => routesFeatureA}, ]), ], }); const fixture = styleFixture(TestBed.createComponent(WorkbenchComponent)); const workbenchRouter = TestBed.inject(WorkbenchRouter); - // Navigate to entry component of feature module A + // Navigate to entry component of feature A workbenchRouter.navigate(['feature-a']).then(); advance(fixture); expect(fixture).toShow(FeatureA_EntryComponent, '1'); @@ -244,10 +242,10 @@ describe('Router', () => { it('allows to close views', fakeAsync(() => { TestBed.configureTestingModule({ - imports: [ - WorkbenchTestingModule.forTest(), - RouterTestingModule.withRoutes([ - {path: 'feature-a', loadChildren: () => FeatureAModule}, + providers: [ + provideWorkbenchForTest(), + provideRouter([ + {path: 'feature-a', loadChildren: () => routesFeatureA}, ]), ], }); @@ -292,25 +290,25 @@ describe('Router', () => { }); /**************************************************************************************************** - * Definition of Feature Module A * + * Definition of Feature A * ****************************************************************************************************/ @Component({ template: ` -

Feature Module A - Entry

+

Feature A - Entry

`, standalone: true, @@ -321,14 +319,14 @@ class FeatureA_EntryComponent { @Component({ template: ` -

Feature Module A - View 1

+

Feature A - View 1

@@ -341,10 +339,10 @@ class FeatureA_View1Component { @Component({ template: ` -

Feature Module A - View 2

+

Feature A - View 2

`, standalone: true, @@ -354,30 +352,22 @@ class FeatureA_View1Component { class FeatureA_View2Component { } -@NgModule({ - imports: [ - CommonModule, - WorkbenchTestingModule, - RouterModule.forChild([ - {path: '', component: FeatureA_EntryComponent}, - {path: 'view-1', component: FeatureA_View1Component}, - {path: 'view-2', component: FeatureA_View2Component}, - {path: 'feature-b', loadChildren: () => FeatureBModule}, - ]), - ], -}) -export class FeatureAModule { -} +const routesFeatureA: Routes = [ + {path: '', component: FeatureA_EntryComponent}, + {path: 'view-1', component: FeatureA_View1Component}, + {path: 'view-2', component: FeatureA_View2Component}, + {path: 'feature-b', loadChildren: () => routesFeatureB}, +]; /**************************************************************************************************** - * Definition of Feature Module B * + * Definition of Feature B * ****************************************************************************************************/ @Component({ template: ` -

Feature Module B - Entry

+

Feature B - Entry