@scion/workbench 20.0.0-beta.7 → 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';
@@ -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
@@ -14366,6 +14346,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.3", ngImpor
14366
14346
  function provideRemoteTextProvider() {
14367
14347
  const REMOTE_TRANSLATION_KEY = /^workbench\.external\.scion-workbench-client\.(?<provider>[^\\.]+)\.%(?<key>.+)$/;
14368
14348
  const REMOTE_TEXT = /^workbench\.external\.scion-workbench-client\.(?<provider>[^\\.]+)\.(?<text>[^%].+)$/;
14349
+ const cache = new Map();
14369
14350
  return makeEnvironmentProviders([
14370
14351
  {
14371
14352
  provide: WORKBENCH_TEXT_PROVIDER,
@@ -14389,9 +14370,23 @@ function provideRemoteTextProvider() {
14389
14370
  }
14390
14371
  // Parse key and provider from the remote translation key.
14391
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
+ }
14392
14378
  // Request the text by intent. Parameters starting with the topic protocol 'topic://' are resolved via messaging.
14393
14379
  const text$ = observeParams$(params, { provider })
14394
- .pipe(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));
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$);
14395
14390
  return toSignal(text$, { initialValue: '' });
14396
14391
  }
14397
14392
  /**
@@ -14405,9 +14400,22 @@ function provideRemoteTextProvider() {
14405
14400
  }
14406
14401
  // Parse text and provider from the remote text.
14407
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
14408
  // Substitute params. Parameters starting with the topic protocol 'topic://' are resolved via messaging.
14409
14409
  const text$ = observeParams$(params, { provider })
14410
- .pipe(map$1(params => params.reduce((text, [param, value]) => text.replaceAll(`:${param}`, value), decodeSemicolons(text))));
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$);
14411
14419
  return toSignal(text$, { initialValue: '' });
14412
14420
  }
14413
14421
  /**
@@ -14428,6 +14436,13 @@ function provideRemoteTextProvider() {
14428
14436
  });
14429
14437
  return observableParams.length ? combineLatest(observableParams) : of([]);
14430
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
+ }
14431
14446
  }
14432
14447
  /**
14433
14448
  * Creates a translatable for the SCION Workbench to request the text from a micro app.
@@ -16689,7 +16704,10 @@ class MicrofrontendViewComponent {
16689
16704
  }
16690
16705
  installNavigator() {
16691
16706
  this._route.params
16692
- .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.
16693
16711
  takeUntilDestroyed())
16694
16712
  .subscribe();
16695
16713
  }
@@ -16800,13 +16818,23 @@ class MicrofrontendViewComponent {
16800
16818
  .catch((error) => this._messageClient.publish(replyTo, stringifyError(error), { headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR) }));
16801
16819
  });
16802
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
+ }
16803
16833
  /**
16804
16834
  * Updates the properties of this view, such as the view title, as defined by the capability.
16805
16835
  */
16806
16836
  setViewProperties(context) {
16807
- const { capability, params } = context;
16808
- this.view.title = (capability && createRemoteTranslatable(capability.properties.title, { appSymbolicName: capability.metadata.appSymbolicName, valueParams: params, topicParams: capability.properties.resolve })) ?? null;
16809
- this.view.heading = (capability && createRemoteTranslatable(capability.properties.heading, { appSymbolicName: capability.metadata.appSymbolicName, valueParams: params, topicParams: capability.properties.resolve })) ?? null;
16837
+ const { capability } = context;
16810
16838
  this.view.classList.application = capability?.properties.cssClass;
16811
16839
  this.view.closable = capability?.properties.closable ?? true;
16812
16840
  this.view.dirty = false;
@@ -16940,14 +16968,17 @@ function configureMicrofrontendGlassPane() {
16940
16968
  ];
16941
16969
  }
16942
16970
  /**
16943
- * Executes passed function each time when the source emits a different capability.
16971
+ * Executes passed functions based on the current state.
16944
16972
  */
16945
- function executeOnCapabilityChange(onCapabilityChange) {
16973
+ function executeOn(executeOn) {
16974
+ const { onEach, onCapabilityChange } = executeOn;
16946
16975
  let prevCapability = null;
16947
16976
  return map(({ capability, params }) => {
16948
16977
  const context = { capability, prevCapability, params };
16978
+ onEach?.(context);
16979
+ // Test if the capability has changed.
16949
16980
  if (prevCapability?.metadata.id !== capability?.metadata.id) {
16950
- onCapabilityChange(context);
16981
+ onCapabilityChange?.(context);
16951
16982
  prevCapability = capability;
16952
16983
  }
16953
16984
  return context;