@scion/workbench 20.0.0-beta.6 → 20.0.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,10 +3,10 @@ import { effect, untracked, Injectable, inject, Injector, signal, booleanAttribu
3
3
  import { Router, ChildrenOutletContexts, NavigationStart, ActivationStart, ActivationEnd, PRIMARY_OUTLET, UrlSegment, RouterOutlet, RouterEvent, NavigationEnd, NavigationCancel, NavigationError, ActivatedRoute } from '@angular/router';
4
4
  import { Arrays, Objects as Objects$1, Dictionaries, Defined, Observables, Maps } from '@scion/toolkit/util';
5
5
  import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
6
- import { skipUntil, mergeMap, filter, observeOn, catchError, startWith, take, map, switchMap as switchMap$1, skip, subscribeOn, finalize, distinctUntilChanged, tap, takeUntil, first, combineLatestWith } from 'rxjs/operators';
6
+ import { skipUntil, mergeMap, filter, observeOn, catchError, startWith, take, map, switchMap as switchMap$1, skip, subscribeOn, finalize, distinctUntilChanged, takeUntil, first, combineLatestWith } from 'rxjs/operators';
7
7
  import { coerceElement } from '@angular/cdk/coercion';
8
8
  import { UUID } from '@scion/toolkit/uuid';
9
- import { Observable, Subject, asapScheduler, AsyncSubject, lastValueFrom, iif, switchMap, race, pairwise, EMPTY, of, firstValueFrom, from, animationFrameScheduler, merge, fromEvent, BehaviorSubject, delay, identity, audit, share, noop, map as map$1, concatWith, withLatestFrom, mergeMap as mergeMap$1, mergeWith, combineLatest, asyncScheduler, timer } from 'rxjs';
9
+ import { Observable, Subject, asapScheduler, AsyncSubject, lastValueFrom, iif, switchMap, race, pairwise, EMPTY, of, firstValueFrom, from, animationFrameScheduler, merge, fromEvent, BehaviorSubject, delay, identity, noop, map as map$1, concatWith, withLatestFrom, mergeMap as mergeMap$1, mergeWith, NEVER, share, timer, ReplaySubject, combineLatest, asyncScheduler } from 'rxjs';
10
10
  import { fromMutation$, fromResize$ } from '@scion/toolkit/observable';
11
11
  import { subscribeIn, observeIn, filterArray, tapFirst } from '@scion/toolkit/operators';
12
12
  import { SciViewportComponent } from '@scion/components/viewport';
@@ -19,7 +19,7 @@ import * as i1 from '@angular/forms';
19
19
  import { NonNullableFormBuilder, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
20
20
  import { FocusMonitor, CdkTrapFocus } from '@angular/cdk/a11y';
21
21
  import { trigger, transition, style, animate } from '@angular/animations';
22
- import { ManifestService, MicrofrontendPlatform, PlatformState, MessageHeaders, MessageClient, ResponseStatusCodes, OutletRouter, MicrofrontendPlatformConfig, APP_IDENTITY, mapToBody, HostManifestInterceptor, ObservableDecorator, IntentInterceptor, CapabilityInterceptor, MicrofrontendPlatformHost, IntentClient, PlatformPropertyService } from '@scion/microfrontend-platform';
22
+ import { ManifestService, MicrofrontendPlatform, PlatformState, MessageHeaders, MessageClient, ResponseStatusCodes, mapToBody, OutletRouter, MicrofrontendPlatformConfig, APP_IDENTITY, HostManifestInterceptor, ObservableDecorator, IntentInterceptor, CapabilityInterceptor, MicrofrontendPlatformHost, IntentClient, PlatformPropertyService } from '@scion/microfrontend-platform';
23
23
  import { Beans } from '@scion/toolkit/bean-manager';
24
24
  import { ɵMicrofrontendRouteParams as _MicrofrontendRouteParams, WorkbenchCapabilities, WorkbenchTextService, WorkbenchMessageBox, eMESSAGE_BOX_MESSAGE_PARAM, ɵTHEME_CONTEXT_KEY as _THEME_CONTEXT_KEY, ɵWorkbenchCommands as _WorkbenchCommands, ɵPOPUP_CONTEXT as _POPUP_CONTEXT, ɵWorkbenchPopupMessageHeaders as _WorkbenchPopupMessageHeaders, WorkbenchPopup, ɵMESSAGE_BOX_CONTEXT as _MESSAGE_BOX_CONTEXT, ɵDIALOG_CONTEXT as _DIALOG_CONTEXT, ɵWorkbenchDialogMessageHeaders as _WorkbenchDialogMessageHeaders, WorkbenchDialog as WorkbenchDialog$1, WorkbenchRouter as WorkbenchRouter$1, WorkbenchPopupService, WorkbenchMessageBoxService as WorkbenchMessageBoxService$1, ɵWorkbenchMessageBoxService as _WorkbenchMessageBoxService, WorkbenchDialogService as WorkbenchDialogService$1, ɵWorkbenchDialogService as _WorkbenchDialogService, WorkbenchNotificationService, ɵWorkbenchTextService as _WorkbenchTextService, ɵVIEW_ID_CONTEXT_KEY as _VIEW_ID_CONTEXT_KEY, WorkbenchClient } from '@scion/workbench-client';
25
25
  import { SciThrobberComponent } from '@scion/components/throbber';
@@ -5570,22 +5570,22 @@ function parseMatrixParams(matrixParams) {
5570
5570
  return {};
5571
5571
  }
5572
5572
  const params = {};
5573
- for (const match of escapeSemicolon(matrixParams).matchAll(/(?<paramName>[^=;]+)=(?<paramValue>[^;]*)/g)) {
5573
+ for (const match of encodeEscapedSemicolons(matrixParams).matchAll(/(?<paramName>[^=;]+)=(?<paramValue>[^;]*)/g)) {
5574
5574
  const { paramName, paramValue } = match.groups;
5575
- params[unescapeSemicolon(paramName)] = unescapeSemicolon(paramValue);
5575
+ params[paramName] = decodeSemicolons(paramValue);
5576
5576
  }
5577
5577
  return params;
5578
5578
  /**
5579
- * Replaces escaped semicolons (`\;`) with the placeholder (`ɵ`) to prevent interpretation as key-value separators.
5579
+ * Encodes escaped semicolons (`\\;`) as `&#x3b` (Unicode) to prevent interpretation as interpolation parameter separators.
5580
5580
  */
5581
- function escapeSemicolon(value) {
5582
- return value.replaceAll('\\;', 'ɵ');
5581
+ function encodeEscapedSemicolons(value) {
5582
+ return value.replaceAll('\\;', '&#x3b');
5583
5583
  }
5584
5584
  /**
5585
- * Restores escaped semicolons by replacing the placeholder (`ɵ`) back to semicolons (`;`).
5585
+ * Decodes encoded semicolons (`&#x3b`) back to semicolons (`;`).
5586
5586
  */
5587
- function unescapeSemicolon(value) {
5588
- return value.replaceAll('ɵ', ';');
5587
+ function decodeSemicolons(value) {
5588
+ return value.replaceAll('&#x3b', ';');
5589
5589
  }
5590
5590
  }
5591
5591
 
@@ -6399,7 +6399,7 @@ class ViewDropZoneDirective {
6399
6399
  _zone = inject(NgZone);
6400
6400
  _id = UUID.randomUUID();
6401
6401
  _boundingClientRect = boundingClientRect(inject(ElementRef));
6402
- _dropRegion$ = createDropRegionObservable();
6402
+ _dropRegion$ = new BehaviorSubject(null);
6403
6403
  _dropRegionSize = computed(() => ({
6404
6404
  maxHeight: coercePixelValue(this.dropRegionSize(), { containerSize: this._boundingClientRect().height }),
6405
6405
  maxWidth: coercePixelValue(this.dropRegionSize(), { containerSize: this._boundingClientRect().width }),
@@ -6484,7 +6484,9 @@ class ViewDropZoneDirective {
6484
6484
  });
6485
6485
  // Enable drop zone based on dragging over an allowed drop region.
6486
6486
  // If enabled, drag events are not received by nested drop zones.
6487
- const dropZoneActivator = this._dropRegion$.subscribe(dropRegion => {
6487
+ const dropZoneActivator = this._dropRegion$
6488
+ .pipe(distinctUntilChanged())
6489
+ .subscribe(dropRegion => {
6488
6490
  setStyle(dropZoneElement, { 'pointer-events': dropRegion ? null : 'none' });
6489
6491
  setAttribute(dropZoneElement, { 'data-region': dropRegion });
6490
6492
  });
@@ -6509,7 +6511,7 @@ class ViewDropZoneDirective {
6509
6511
  // order rather than in the order of tracked signal changes.
6510
6512
  const dragging = this._viewDragService.dragging;
6511
6513
  this._dropRegion$
6512
- .pipe(
6514
+ .pipe(distinctUntilChanged(),
6513
6515
  // When leaving the drop zone (no drop region) but continue dragging, remove the drop zone asynchronously
6514
6516
  // to animate transition of the placeholder to another drop zone. If ended dragging, remove the placeholder
6515
6517
  // immediately.
@@ -6613,28 +6615,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImpor
6613
6615
  function coercePixelValue(pixelOrRatio, options) {
6614
6616
  return (pixelOrRatio <= 1) ? options.containerSize * pixelOrRatio : pixelOrRatio;
6615
6617
  }
6616
- /**
6617
- * Creates a subject-like observable to observe and set the drop region,
6618
- * multicasting the drop zone to many observers while throttling emission
6619
- * to the latest drop region per animation frame.
6620
- */
6621
- function createDropRegionObservable() {
6622
- const zone = inject(NgZone);
6623
- const region$ = new BehaviorSubject(null);
6624
- const observe$ = region$
6625
- .pipe(subscribeIn(fn => zone.runOutsideAngular(fn)),
6626
- // Ensure not to be in Angular.
6627
- tap(() => NgZone.assertNotInAngularZone()),
6628
- // Throttle emission to a single event per animation frame.
6629
- audit(() => nextAnimationFrame$()), share());
6630
- // Add notifier function.
6631
- observe$.next = (region) => {
6632
- if (region$.value !== region) {
6633
- zone.runOutsideAngular(() => region$.next(region));
6634
- }
6635
- };
6636
- return observe$;
6637
- }
6638
6618
 
6639
6619
  /*
6640
6620
  * Copyright (c) 2018-2024 Swiss Federal Railways
@@ -14359,86 +14339,185 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImpor
14359
14339
  /**
14360
14340
  * Registers a text provider for the SCION Workbench to get texts from micro apps.
14361
14341
  *
14362
- * If the key matches the format of a remote key, text is requested via intent from the respective 'text-provider' capability.
14363
- * If not, the key is ignored and `undefined` is returned.
14364
- *
14365
- * Remote key format: "workbench.external.~<APP_SYMBOLIC_NAME>~.<TEXT_KEY>".
14342
+ * This text provider provides texts for keys matching the format: "workbench.external.scion-workbench-client.<APP_SYMBOLIC_NAME>.<TRANSLATABLE>".
14366
14343
  *
14367
14344
  * @see createRemoteTranslatable
14368
14345
  */
14369
14346
  function provideRemoteTextProvider() {
14370
- const REMOTE_KEY = /^workbench\.external\.~(?<provider>[^\\~]+)~\.(?<key>.+)$/;
14347
+ const REMOTE_TRANSLATION_KEY = /^workbench\.external\.scion-workbench-client\.(?<provider>[^\\.]+)\.%(?<key>.+)$/;
14348
+ const REMOTE_TEXT = /^workbench\.external\.scion-workbench-client\.(?<provider>[^\\.]+)\.(?<text>[^%].+)$/;
14349
+ const cache = new Map();
14371
14350
  return makeEnvironmentProviders([
14372
14351
  {
14373
14352
  provide: WORKBENCH_TEXT_PROVIDER,
14374
- useValue: remoteTextProvider,
14353
+ useValue: provideRemoteText,
14354
+ multi: true,
14355
+ },
14356
+ {
14357
+ provide: WORKBENCH_TEXT_PROVIDER,
14358
+ useValue: interpolateRemoteText,
14375
14359
  multi: true,
14376
14360
  },
14377
14361
  ]);
14378
- function remoteTextProvider(remoteKey, params) {
14379
- // Test if the key matches a remote key.
14380
- const match = REMOTE_KEY.exec(remoteKey);
14362
+ /**
14363
+ * Provides text from a remote app.
14364
+ */
14365
+ function provideRemoteText(translationKey, params) {
14366
+ // Test if the key matches a remote translation key.
14367
+ const match = REMOTE_TRANSLATION_KEY.exec(translationKey);
14381
14368
  if (!match) {
14382
- return undefined; // ignore key
14369
+ return undefined;
14383
14370
  }
14384
- // Parse key and provider from the remote key.
14371
+ // Parse key and provider from the remote translation key.
14385
14372
  const { key, provider } = match.groups;
14373
+ // Return cached text if cached.
14374
+ const cacheKey = createCacheKey(translationKey, params, provider);
14375
+ if (cache.has(cacheKey)) {
14376
+ return toSignal(cache.get(cacheKey), { initialValue: '' });
14377
+ }
14386
14378
  // Request the text by intent. Parameters starting with the topic protocol 'topic://' are resolved via messaging.
14387
- const text$ = observeParams$(params)
14388
- .pipe(map$1(params => params.reduce((translatable, [name, value]) => `${translatable};${name}=${value}`, `%${key}`)), switchMap(translatable => Beans.get(WorkbenchTextService).text$(translatable, { provider: provider })), map$1(text => text ?? key));
14379
+ const text$ = observeParams$(params, { provider })
14380
+ .pipe(
14381
+ // Ensure the observable to never complete independent of text provider request completion, simplifying cache cleanup as
14382
+ // finalize is only called when the last subscriber unsubscribes, after the specified TTL.
14383
+ concatWith(NEVER), map$1(params => params.reduce((translatable, [param, value]) => `${translatable};${param}=${encodeSemicolons$1(value)}`, `%${key}`)), switchMap(translatable => Beans.get(WorkbenchTextService).text$(translatable, { provider })), map$1(text => text ?? `%${key}`),
14384
+ // Remove cached text when an error occurs, or when the subscriber count drops to zero, after the specified TTL.
14385
+ finalize(() => cache.delete(cacheKey)), share({
14386
+ connector: () => new ReplaySubject(1),
14387
+ resetOnRefCountZero: () => timer(0, animationFrameScheduler), // reset asynchronously to prevent flickering of translated texts on re-layout
14388
+ }));
14389
+ cache.set(cacheKey, text$);
14390
+ return toSignal(text$, { initialValue: '' });
14391
+ }
14392
+ /**
14393
+ * Substitutes named parameters in a remote text.
14394
+ */
14395
+ function interpolateRemoteText(translationKey, params) {
14396
+ // Test if the key matches a remote text.
14397
+ const match = REMOTE_TEXT.exec(translationKey);
14398
+ if (!match) {
14399
+ return undefined;
14400
+ }
14401
+ // Parse text and provider from the remote text.
14402
+ const { text, provider } = match.groups;
14403
+ // Return cached text if cached.
14404
+ const cacheKey = createCacheKey(translationKey, params, provider);
14405
+ if (cache.has(cacheKey)) {
14406
+ return toSignal(cache.get(cacheKey), { initialValue: '' });
14407
+ }
14408
+ // Substitute params. Parameters starting with the topic protocol 'topic://' are resolved via messaging.
14409
+ const text$ = observeParams$(params, { provider })
14410
+ .pipe(
14411
+ // Never complete the observable, simplifying cache cleanup as `finalize` is only called when the last subscriber unsubscribes, after the specified TTL.
14412
+ concatWith(NEVER), map$1(params => params.reduce((text, [param, value]) => text.replaceAll(`:${param}`, value), decodeSemicolons(text))),
14413
+ // Remove cached text when an error occurs, or when the subscriber count drops to zero, after the specified TTL.
14414
+ finalize(() => cache.delete(cacheKey)), share({
14415
+ connector: () => new ReplaySubject(1),
14416
+ resetOnRefCountZero: () => timer(0, animationFrameScheduler), // reset asynchronously to prevent flickering of translated texts on re-layout
14417
+ }));
14418
+ cache.set(cacheKey, text$);
14389
14419
  return toSignal(text$, { initialValue: '' });
14390
14420
  }
14391
14421
  /**
14392
- * Creates an Observable that emits tuples of name/value pairs from the passed parameters.
14422
+ * Creates an Observable that emits tuples of name-value pairs from the passed parameters.
14393
14423
  *
14394
14424
  * Parameters starting with the topic protocol 'topic://' are resolved via topic-based messaging.
14395
14425
  */
14396
- function observeParams$(params) {
14397
- const observableParams = Object.entries(params).map(([name, value]) => {
14426
+ function observeParams$(params, options) {
14427
+ const observableParams = Object.entries(params).map(([param, value]) => {
14398
14428
  if (!value.startsWith(TOPIC_PROTOCOL)) {
14399
- return of([name, value]);
14429
+ return of([param, value]);
14400
14430
  }
14401
14431
  const topic = value.substring(TOPIC_PROTOCOL.length);
14402
- return Beans.get(MessageClient).request$(topic, undefined, { retain: true }).pipe(map$1(({ body: resolved }) => ([name, resolved ?? ''])));
14432
+ return Beans.get(MessageClient).request$(topic, undefined, { retain: true })
14433
+ .pipe(mapToBody(),
14434
+ // Resolve text if the resolver returns a translatable.
14435
+ switchMap(resolved => resolved?.startsWith('%') ? Beans.get(WorkbenchTextService).text$(resolved, options) : of(resolved)), map$1(resolved => [param, resolved ?? '']));
14403
14436
  });
14404
14437
  return observableParams.length ? combineLatest(observableParams) : of([]);
14405
14438
  }
14439
+ /**
14440
+ * Creates the cache key for specified translatable.
14441
+ */
14442
+ function createCacheKey(key, params, provider) {
14443
+ const translatable = Object.entries(params).reduce((translatable, [param, value]) => `${translatable};${param}=${value}`, `%${key}`);
14444
+ return `${translatable}@${provider}`;
14445
+ }
14406
14446
  }
14407
14447
  /**
14408
14448
  * Creates a translatable for the SCION Workbench to request the text from a micro app.
14409
14449
  *
14410
- * Passed parameters are used to substitute named interpolation parameters. A named interpolation parameter starts with a colon (`:`) followed by a name.
14450
+ * Passed parameters are used to substitute named parameters in the text or interpolation parameters of the translation key.
14411
14451
  *
14412
- * Example: %translationKey;param1=value1;param2=:value2 // :value2 is a named interpolation parameter
14452
+ * A named parameter starts with a colon (`:`) followed by the parameter name and can be substituted by an explicit value (passed as value param) or topic (passed as topic param).
14453
+ * Topic params define a topic with the actual value requested when resolving the translatable. A topic can also reference value params as named params in topic segments.
14413
14454
  *
14414
- * Named interpolation parameters can be substituted by explicit values (passed as value params) or topics (passed as topic params).
14415
- * Unlike value params, topic params define a topic and the actual value will be requested when resolving the translatable.
14416
- * Like the translatable's interpolation params, a topic can reference value params as named parameters in topic segments.
14455
+ * @example - Translation Key
14456
+ * `%translationKey;param1=:namedParam1;param2=:namedParam2`
14457
+ *
14458
+ * @example - Text
14459
+ * `Text with :namedParam1 and :namedParam2`
14417
14460
  *
14418
14461
  * @param translatable - Specifies the translatable.
14419
- * @param config - Specifies the text provider and values for substituting named interpolation parameters.
14462
+ * @param config - Specifies the text provider and values for substituting named parameters.
14420
14463
  * @return Translatable that can be passed to the workbench's {@link text()} function for translation.
14421
14464
  *
14422
14465
  * @see provideRemoteTextProvider
14423
14466
  */
14424
14467
  function createRemoteTranslatable(translatable, config) {
14425
- if (!translatable?.startsWith('%')) {
14468
+ if (!translatable) {
14469
+ return translatable;
14470
+ }
14471
+ // Create Map of value params.
14472
+ const valueParams = new Map(Object.entries(Dictionaries.coerce(config.valueParams)).map(([param, value]) => [param, encodeSemicolons$1(value)]));
14473
+ // Create Map of topic params, substituting referenced value params.
14474
+ const topicParams = new Map(Object.entries(Dictionaries.coerce(config.topicParams)).map(([param, topic]) => [param, toTopicParam(topic)]));
14475
+ // Return text as-is if not a translation key nor referencing parameters.
14476
+ if (!translatable.startsWith('%') && !containsParam(translatable, valueParams) && !containsParam(translatable, topicParams)) {
14426
14477
  return translatable;
14427
14478
  }
14428
- const remoteTranslatable = `%workbench.external.~${config.appSymbolicName}~.${translatable.substring(1)}`;
14429
- const valueParams = Dictionaries.coerce(config.valueParams);
14430
- const topicParams = Object.fromEntries(Object.entries(Dictionaries.coerce(config.topicParams))
14431
- // Replace named params in topic segments.
14432
- .map(([name, topic]) => [name, topic.replace(/(?<=\/|^):(?<namedParam>[^/]+)/g, (match, namedParam) => `${valueParams[namedParam] ?? match}`)])
14433
- // Add topic protocol to indicate resolution via topic-based messaging.
14434
- .map(([paramName, topic]) => [paramName, `${TOPIC_PROTOCOL}${topic}`]));
14435
- // Replace named params in matrix param values.
14436
- return remoteTranslatable.replace(/(?<==):(?<namedParam>[^;]+)/g, (match, namedParam) => `${valueParams[namedParam] ?? topicParams[namedParam] ?? match}`);
14479
+ const remoteTranslatablePrefix = `%workbench.external.scion-workbench-client.${config.appSymbolicName}`;
14480
+ if (translatable.startsWith('%')) {
14481
+ // Substitute named parameters in interpolation params.
14482
+ return `${remoteTranslatablePrefix}.${translatable}`.replace(/(?<==):(?<namedParam>[^;]+)/g, (match, param) => valueParams.get(param) ?? topicParams.get(param) ?? match);
14483
+ }
14484
+ else {
14485
+ // Append referenced parameters in matrix notation.
14486
+ return [...valueParams, ...topicParams]
14487
+ .filter(([param]) => translatable.includes(`:${param}`))
14488
+ .reduce((translatable, [param, value]) => `${translatable};${param}=${value}`, `${remoteTranslatablePrefix}.${encodeSemicolons$1(translatable)}`);
14489
+ }
14490
+ /**
14491
+ * Adds the topic protocol to indicate resolution via topic-based messaging and substitutes named topic segments.
14492
+ */
14493
+ function toTopicParam(topic) {
14494
+ return `${TOPIC_PROTOCOL}${topic.replace(/(?<=\/|^):(?<namedParam>[^/]+)/g, (match, namedParam) => {
14495
+ return valueParams.get(namedParam) ?? match;
14496
+ })}`;
14497
+ }
14498
+ /**
14499
+ * Tests whether the passed text references any of the passed params.
14500
+ */
14501
+ function containsParam(text, params) {
14502
+ return Array.from(params.keys()).some(param => text.includes(`:${param}`));
14503
+ }
14437
14504
  }
14438
14505
  /**
14439
14506
  * Prefix of topic params.
14440
14507
  */
14441
14508
  const TOPIC_PROTOCOL = 'topic://';
14509
+ /**
14510
+ * Encodes semicolons (`;`) as `&#x3b` (Unicode) to prevent interpretation as interpolation parameter separators.
14511
+ */
14512
+ function encodeSemicolons$1(value) {
14513
+ return `${value}`.replaceAll(';', '&#x3b');
14514
+ }
14515
+ /**
14516
+ * Decodes encoded semicolons (`&#x3b`) back to semicolons (`;`).
14517
+ */
14518
+ function decodeSemicolons(value) {
14519
+ return value.replaceAll('&#x3b', ';');
14520
+ }
14442
14521
 
14443
14522
  /*
14444
14523
  * Copyright (c) 2018-2024 Swiss Federal Railways
@@ -16625,7 +16704,10 @@ class MicrofrontendViewComponent {
16625
16704
  }
16626
16705
  installNavigator() {
16627
16706
  this._route.params
16628
- .pipe(switchMap(params => this.fetchCapability$(params[_MicrofrontendRouteParams.ɵVIEW_CAPABILITY_ID]).pipe(map(capability => ({ capability, params })))), executeOnCapabilityChange(context => this.onCapabilityChange(context)), filterNullCapability(), delayIfLazy(), serializeExecution(context => this.onNavigate(context)), subscribeOn(asyncScheduler), // subscribe asynchronously to prevent manual change detection in `onCapabilityChange` during component construction.
16707
+ .pipe(switchMap(params => this.fetchCapability$(params[_MicrofrontendRouteParams.ɵVIEW_CAPABILITY_ID]).pipe(map(capability => ({ capability, params })))), executeOn({
16708
+ onEach: context => this.setViewTitleAndHeading(context),
16709
+ onCapabilityChange: context => this.onCapabilityChange(context),
16710
+ }), filterNullCapability(), delayIfLazy(), serializeExecution(context => this.onNavigate(context)), subscribeOn(asyncScheduler), // subscribe asynchronously to prevent manual change detection in `onCapabilityChange` during component construction.
16629
16711
  takeUntilDestroyed())
16630
16712
  .subscribe();
16631
16713
  }
@@ -16736,13 +16818,23 @@ class MicrofrontendViewComponent {
16736
16818
  .catch((error) => this._messageClient.publish(replyTo, stringifyError(error), { headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR) }));
16737
16819
  });
16738
16820
  }
16821
+ /**
16822
+ * Sets view title and heading as defined on the capability.
16823
+ */
16824
+ setViewTitleAndHeading(context) {
16825
+ const { capability, params } = context;
16826
+ if (capability?.properties.title) {
16827
+ this.view.title = createRemoteTranslatable(capability.properties.title, { appSymbolicName: capability.metadata.appSymbolicName, valueParams: params, topicParams: capability.properties.resolve });
16828
+ }
16829
+ if (capability?.properties.heading) {
16830
+ this.view.heading = createRemoteTranslatable(capability.properties.heading, { appSymbolicName: capability.metadata.appSymbolicName, valueParams: params, topicParams: capability.properties.resolve });
16831
+ }
16832
+ }
16739
16833
  /**
16740
16834
  * Updates the properties of this view, such as the view title, as defined by the capability.
16741
16835
  */
16742
16836
  setViewProperties(context) {
16743
- const { capability, params } = context;
16744
- this.view.title = (capability && createRemoteTranslatable(capability.properties.title, { appSymbolicName: capability.metadata.appSymbolicName, valueParams: params, topicParams: capability.properties.resolve })) ?? null;
16745
- this.view.heading = (capability && createRemoteTranslatable(capability.properties.heading, { appSymbolicName: capability.metadata.appSymbolicName, valueParams: params, topicParams: capability.properties.resolve })) ?? null;
16837
+ const { capability } = context;
16746
16838
  this.view.classList.application = capability?.properties.cssClass;
16747
16839
  this.view.closable = capability?.properties.closable ?? true;
16748
16840
  this.view.dirty = false;
@@ -16876,14 +16968,17 @@ function configureMicrofrontendGlassPane() {
16876
16968
  ];
16877
16969
  }
16878
16970
  /**
16879
- * Executes passed function each time when the source emits a different capability.
16971
+ * Executes passed functions based on the current state.
16880
16972
  */
16881
- function executeOnCapabilityChange(onCapabilityChange) {
16973
+ function executeOn(executeOn) {
16974
+ const { onEach, onCapabilityChange } = executeOn;
16882
16975
  let prevCapability = null;
16883
16976
  return map(({ capability, params }) => {
16884
16977
  const context = { capability, prevCapability, params };
16978
+ onEach?.(context);
16979
+ // Test if the capability has changed.
16885
16980
  if (prevCapability?.metadata.id !== capability?.metadata.id) {
16886
- onCapabilityChange(context);
16981
+ onCapabilityChange?.(context);
16887
16982
  prevCapability = capability;
16888
16983
  }
16889
16984
  return context;
@@ -17055,7 +17150,7 @@ function provideHostTextProvider() {
17055
17150
  provideMicrofrontendPlatformInitializer(() => {
17056
17151
  const injector = inject(Injector);
17057
17152
  WorkbenchClient.registerTextProvider((key, params) => {
17058
- const translatable = Object.entries(params).reduce((translatable, [name, value]) => `${translatable};${name}=${value}`, `%${key}`);
17153
+ const translatable = Object.entries(params).reduce((translatable, [name, value]) => `${translatable};${name}=${encodeSemicolons(value)}`, `%${key}`);
17059
17154
  const environmentInjector = createEnvironmentInjector([], injector.get(EnvironmentInjector));
17060
17155
  return toObservable(text(translatable, { injector: environmentInjector }), { injector: environmentInjector })
17061
17156
  .pipe(map$1(text => text !== '' && text !== key ? text : undefined), // emit `undefined` if not found the text
@@ -17064,6 +17159,14 @@ function provideHostTextProvider() {
17064
17159
  }),
17065
17160
  ]);
17066
17161
  }
17162
+ /**
17163
+ * Encodes semicolons (`;`) as `\\;` to prevent interpretation as interpolation parameter separators.
17164
+ *
17165
+ * @see Translatable
17166
+ */
17167
+ function encodeSemicolons(value) {
17168
+ return value.replaceAll(';', '\\;');
17169
+ }
17067
17170
 
17068
17171
  /*
17069
17172
  * Copyright (c) 2018-2024 Swiss Federal Railways