@yuuvis/client-framework 2.10.3 → 2.11.0

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.
Files changed (60) hide show
  1. package/autocomplete/lib/autocomplete.component.d.ts +4 -4
  2. package/autocomplete/lib/autocomplete.interface.d.ts +2 -2
  3. package/common/lib/services/index.d.ts +1 -0
  4. package/common/lib/services/layout-settings/layout-settings.service.d.ts +15 -0
  5. package/common/lib/services/theme/index.d.ts +3 -0
  6. package/common/lib/services/theme/theme.models.d.ts +16 -0
  7. package/common/lib/services/theme/theme.provider.d.ts +4 -0
  8. package/common/lib/services/theme/theme.service.d.ts +16 -0
  9. package/fesm2022/yuuvis-client-framework-common.mjs +213 -4
  10. package/fesm2022/yuuvis-client-framework-common.mjs.map +1 -1
  11. package/fesm2022/yuuvis-client-framework-datepicker.mjs +1 -1
  12. package/fesm2022/yuuvis-client-framework-datepicker.mjs.map +1 -1
  13. package/fesm2022/yuuvis-client-framework-forms.mjs +46 -21
  14. package/fesm2022/yuuvis-client-framework-forms.mjs.map +1 -1
  15. package/fesm2022/yuuvis-client-framework-object-flavor.mjs +2 -2
  16. package/fesm2022/yuuvis-client-framework-object-flavor.mjs.map +1 -1
  17. package/fesm2022/yuuvis-client-framework-object-relationship.mjs +135 -52
  18. package/fesm2022/yuuvis-client-framework-object-relationship.mjs.map +1 -1
  19. package/fesm2022/yuuvis-client-framework-object-versions.mjs +4 -3
  20. package/fesm2022/yuuvis-client-framework-object-versions.mjs.map +1 -1
  21. package/fesm2022/yuuvis-client-framework-query-list.mjs +5 -4
  22. package/fesm2022/yuuvis-client-framework-query-list.mjs.map +1 -1
  23. package/fesm2022/yuuvis-client-framework-tile-list.mjs +20 -4
  24. package/fesm2022/yuuvis-client-framework-tile-list.mjs.map +1 -1
  25. package/fesm2022/yuuvis-client-framework.mjs +616 -98
  26. package/fesm2022/yuuvis-client-framework.mjs.map +1 -1
  27. package/forms/lib/elements/datetime/datetime.component.d.ts +0 -1
  28. package/forms/lib/elements/organization/organization.component.d.ts +1 -1
  29. package/forms/lib/elements/organization-set/organization-set.component.d.ts +1 -1
  30. package/index.d.ts +5 -2
  31. package/lib/config/index.d.ts +1 -0
  32. package/lib/config/session/index.d.ts +3 -0
  33. package/lib/config/session/session-activity-window-before-end.const.d.ts +43 -0
  34. package/lib/config/session/session-default-duration.const.d.ts +47 -0
  35. package/lib/config/session/session-popup-before-end.const.d.ts +47 -0
  36. package/lib/enums/channel-message.enum.d.ts +4 -0
  37. package/lib/enums/index.d.ts +1 -0
  38. package/lib/models/index.d.ts +2 -0
  39. package/lib/models/session/channel-payload.model.d.ts +5 -0
  40. package/lib/models/session/index.d.ts +1 -0
  41. package/lib/models/snack-bar/index.d.ts +3 -0
  42. package/lib/models/snack-bar/snack-bar-data.model.d.ts +6 -0
  43. package/lib/models/snack-bar/snack-bar-level.model.d.ts +1 -0
  44. package/lib/{services/snack-bar/snack-bar.interface.d.ts → models/snack-bar/snack-bar-options.model.d.ts} +1 -6
  45. package/lib/providers/index.d.ts +1 -0
  46. package/lib/providers/session/index.d.ts +1 -0
  47. package/lib/providers/session/provide-session.provider.d.ts +43 -0
  48. package/lib/services/index.d.ts +2 -2
  49. package/lib/services/session/session.service.d.ts +113 -0
  50. package/lib/services/snack-bar/snack-bar.service.d.ts +5 -5
  51. package/object-relationship/index.d.ts +1 -0
  52. package/object-relationship/lib/actions/add-relationship/add-relationship.component.d.ts +10 -0
  53. package/object-relationship/lib/actions/relationship-target-search/relationship-target-search.component.d.ts +17 -4
  54. package/object-relationship/lib/object-relationship.const.d.ts +0 -1
  55. package/object-versions/lib/object-versions.component.d.ts +1 -0
  56. package/package.json +8 -8
  57. package/query-list/lib/query-list.component.d.ts +8 -7
  58. package/tile-list/lib/tile-list/tile-list.component.d.ts +4 -2
  59. package/lib/assets/i18n/de.json +0 -202
  60. package/lib/assets/i18n/en.json +0 -202
@@ -1,21 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { NgModule, inject, Injectable, signal, Component, makeEnvironmentProviders, provideAppInitializer, NgZone } from '@angular/core';
3
- import { CommonModule } from '@angular/common';
2
+ import { inject, Injectable, signal, Component, makeEnvironmentProviders, provideAppInitializer, NgZone, NgModule } from '@angular/core';
3
+ import { finalize, timer, switchMap, map, debounceTime } from 'rxjs';
4
+ import { TranslateService } from '@ngx-translate/core';
4
5
  import { MatSnackBar, MatSnackBarRef, MAT_SNACK_BAR_DATA, MatSnackBarLabel, MatSnackBarActions, MatSnackBarAction } from '@angular/material/snack-bar';
5
6
  import * as i1 from '@angular/material/button';
6
7
  import { MatButtonModule } from '@angular/material/button';
7
-
8
- class YuuvisClientFrameworkModule {
9
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: YuuvisClientFrameworkModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
10
- static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.15", ngImport: i0, type: YuuvisClientFrameworkModule, imports: [CommonModule] }); }
11
- static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: YuuvisClientFrameworkModule, imports: [CommonModule] }); }
12
- }
13
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: YuuvisClientFrameworkModule, decorators: [{
14
- type: NgModule,
15
- args: [{
16
- imports: [CommonModule],
17
- }]
18
- }] });
8
+ import { AppCacheService, BackendService, UserService } from '@yuuvis/client-core';
9
+ import { CommonModule } from '@angular/common';
19
10
 
20
11
  /**
21
12
  * List of element selectors that should be excluded from halo focus
@@ -132,6 +123,152 @@ const haloFocusStyles = {
132
123
  transform: 'translateZ(0)'
133
124
  };
134
125
 
126
+ /**
127
+ * Default session duration (in milliseconds) when no explicit duration is provided.
128
+ *
129
+ * This value is used as the fallback session lifetime when:
130
+ * - `provideSession()` is called without a duration parameter
131
+ * - Before `startSession(duration)` is called with a backend-provided value
132
+ *
133
+ * **Timeline:**
134
+ * ```
135
+ * [Session Start] ──────────────────────────────► [Session Expires]
136
+ * ← sessionDefaultDuration →
137
+ * ```
138
+ *
139
+ * **Override Methods:**
140
+ * - **At startup (known duration)**: `provideSession(customDuration)`
141
+ * - **After login (dynamic duration)**: `sessionService.startSession(backendDuration)`
142
+ *
143
+ * **Usage Scenarios:**
144
+ *
145
+ * Scenario 1 - Fixed duration:
146
+ * ```ts
147
+ * // app.config.ts
148
+ * providers: [
149
+ * provideSession(45 * 60 * 1000) // Override to 45 minutes
150
+ * ]
151
+ * ```
152
+ *
153
+ * Scenario 2 - Backend-provided duration:
154
+ * ```ts
155
+ * // app.config.ts
156
+ * providers: [
157
+ * provideSession() // Uses default 30 minutes initially
158
+ * ]
159
+ *
160
+ * // auth.service.ts (after login)
161
+ * login().subscribe(response => {
162
+ * sessionService.startSession(response.sessionExpiresIn); // Update to backend value
163
+ * });
164
+ * ```
165
+ *
166
+ * **Related Constants:**
167
+ * - {@link sessionActivityWindowBeforeEnd} - When to start tracking user activity
168
+ * - {@link sessionPopupBeforeEnd} - When to show the expiry warning popup
169
+ *
170
+ * @default 30 minutes (1800000 milliseconds)
171
+ */
172
+ const sessionDefaultDuration = 30 * 60 * 1000; // 30 minutes
173
+
174
+ /**
175
+ * Time window (in milliseconds) before session expiry when user activity triggers auto-extension.
176
+ *
177
+ * During this window, the service tracks user interactions (mouse, keyboard, scroll) and HTTP activity.
178
+ * If any activity is detected, the session is automatically extended, preventing the expiry warning popup.
179
+ *
180
+ * **Session Timeline:**
181
+ * ```
182
+ * [Session Start] ──────────────────────────────► [Session Expires]
183
+ * ▲ ▲
184
+ * │ └─ sessionPopupBeforeEnd (1 min)
185
+ * └─ Activity window starts (2 min)
186
+ *
187
+ * ┌─────────────────────────────────────────────┐
188
+ * │ Activity Window (2 minutes) │
189
+ * │ • Tracks: mousemove, keydown, click, scroll │
190
+ * │ • Tracks: HTTP requests (debounced) │
191
+ * │ • If activity detected → auto-extend │
192
+ * │ • If no activity → show warning popup │
193
+ * └─────────────────────────────────────────────┘
194
+ * ```
195
+ *
196
+ * **Why This Matters:**
197
+ * - Prevents unnecessary interruptions for active users
198
+ * - Reduces server-side session renewal traffic by batching extensions
199
+ * - Provides a grace period before showing the expiry warning
200
+ *
201
+ * **Tracked Activities:**
202
+ * - **User interactions**: `mousemove`, `keydown`, `click`, `scroll` on `window`
203
+ * - **HTTP traffic**: Any request via `BackendService.httpCommunicationOccurred$` (debounced 500ms)
204
+ *
205
+ * **Interaction with Other Constants:**
206
+ * - Must be **greater than** {@link sessionPopupBeforeEnd} to allow the activity window before the popup
207
+ * - Typical relationship: `activityWindow > popupWarning` (e.g., 2 min > 1 min)
208
+ *
209
+ * **Performance Note:**
210
+ * Activity tracking is only active during this window, not throughout the entire session,
211
+ * minimizing performance impact of event listeners.
212
+ *
213
+ * @default 2 minutes (120000 milliseconds)
214
+ * @see {@link SessionService.setupUserActivityTracking} for implementation details
215
+ */
216
+ const sessionActivityWindowBeforeEnd = 2 * 60 * 1000; // 2 minutes
217
+
218
+ /**
219
+ * Time (in milliseconds) before session expiry when the warning popup is displayed.
220
+ *
221
+ * When this threshold is reached without detected user activity during the activity window,
222
+ * a snackbar notification appears, warning the user that their session will expire soon
223
+ * and providing an "Extend session" action button.
224
+ *
225
+ * **Session Timeline:**
226
+ * ```
227
+ * [Session Start] ──────────────────────────────► [Session Expires]
228
+ * ▲ ▲
229
+ * │ └─ Popup appears (1 min before expiry)
230
+ * └─ Activity window starts (2 min before expiry)
231
+ *
232
+ * User has no activity during the 2-minute window:
233
+ * ├─ 2 min before: Activity tracking starts
234
+ * ├─ 1 min before: ⚠️ Popup shows "Session expires in one minute"
235
+ * └─ 0 min: Auto-logout if no action taken
236
+ * ```
237
+ *
238
+ * **Popup Behavior:**
239
+ * - Displays a warning message: "Session expires in one minute"
240
+ * - Provides an action button: "Extend session"
241
+ * - Clicking "Extend session" → extends session by the full session duration
242
+ * - Ignoring the popup → automatic logout when timer reaches zero
243
+ * - Only one popup is shown at a time (subsequent extensions dismiss the existing popup)
244
+ *
245
+ * **Cross-Tab Synchronization:**
246
+ * If the user extends the session in one browser tab, all other tabs:
247
+ * - Automatically dismiss their popups (if shown)
248
+ * - Reset their timers to the new expiry time
249
+ * - Avoid showing redundant warnings
250
+ *
251
+ * **Interaction with Other Constants:**
252
+ * - Must be **less than** {@link sessionActivityWindowBeforeEnd}
253
+ * - Typical relationship: `activityWindow > popupWarning` (e.g., 2 min > 1 min)
254
+ * - The gap between them determines how long the activity window runs before the popup
255
+ *
256
+ * **UX Considerations:**
257
+ * - **1 minute** gives users enough time to react without being too intrusive
258
+ * - Too short (e.g., 10 seconds): Users may not have time to respond
259
+ * - Too long (e.g., 5 minutes): Users may be unnecessarily interrupted if they return
260
+ *
261
+ * @default 1 minute (60000 milliseconds)
262
+ * @see {@link SessionService.showPopup} for popup implementation
263
+ */
264
+ const sessionPopupBeforeEnd = 60 * 1000; // 1 minute
265
+
266
+ var ChannelMessage;
267
+ (function (ChannelMessage) {
268
+ ChannelMessage["SessionExtended"] = "SessionExtended";
269
+ ChannelMessage["SessionLogout"] = "SessionLogout";
270
+ })(ChannelMessage || (ChannelMessage = {}));
271
+
135
272
  /**
136
273
  * Service that creates and manages a visual "halo" border around keyboard-focused elements
137
274
  * to enhance navigation visibility and improve accessibility.
@@ -374,89 +511,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImpo
374
511
  }]
375
512
  }] });
376
513
 
377
- class SnackBarService {
378
- #snackBar = inject(MatSnackBar);
379
- info(message, action) {
380
- return this.snack(message, { action, level: 'info' });
381
- }
382
- success(message, action) {
383
- return this.snack(message, { action, level: 'success' });
384
- }
385
- warning(message, action) {
386
- return this.snack(message, { action, level: 'warning' });
387
- }
388
- danger(message, action) {
389
- return this.snack(message, { action, level: 'danger' });
390
- }
391
- snack(message, options) {
392
- return this.#snackBar.openFromComponent(SnackBarComponent, {
393
- data: {
394
- level: options.level,
395
- message,
396
- action: options.action
397
- },
398
- duration: options.duration || 3000,
399
- horizontalPosition: options.horizontalPosition,
400
- verticalPosition: options.verticalPosition,
401
- panelClass: ['yuv-snack-bar', 'level-' + options.level]
402
- });
403
- }
404
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SnackBarService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
405
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SnackBarService, providedIn: 'root' }); }
406
- }
407
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SnackBarService, decorators: [{
408
- type: Injectable,
409
- args: [{
410
- providedIn: 'root'
411
- }]
412
- }] });
413
- class SnackBarComponent {
414
- constructor() {
415
- this.#snackBarRef = inject(MatSnackBarRef);
416
- this.#snackData = inject(MAT_SNACK_BAR_DATA);
417
- this.level = signal(this.#snackData.level);
418
- this.message = signal(this.#snackData.message);
419
- this.action = signal(this.#snackData.action);
420
- }
421
- #snackBarRef;
422
- #snackData;
423
- dismiss(withAction = false) {
424
- if (withAction) {
425
- this.#snackBarRef.dismissWithAction();
426
- }
427
- else if (!this.action()) {
428
- this.#snackBarRef.dismiss();
429
- }
430
- }
431
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SnackBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
432
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.15", type: SnackBarComponent, isStandalone: true, selector: "yuv-snack-bar-component", host: { properties: { "class.info": "level() === 'info'", "class.success": "level() === 'success'", "class.warning": "level() === 'warning'", "class.danger": "level() === 'danger'" } }, ngImport: i0, template: `
433
- <span matSnackBarLabel (click)="dismiss()"> {{ message() }} </span>
434
- @let a = action();
435
- @if (a) {
436
- <span matSnackBarActions>
437
- <button mat-button matSnackBarAction (click)="dismiss(!!a)">{{ action() }}</button>
438
- </span>
439
- }
440
- `, isInline: true, styles: [":host{display:flex}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatSnackBarLabel, selector: "[matSnackBarLabel]" }, { kind: "directive", type: MatSnackBarActions, selector: "[matSnackBarActions]" }, { kind: "directive", type: MatSnackBarAction, selector: "[matSnackBarAction]" }] }); }
441
- }
442
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SnackBarComponent, decorators: [{
443
- type: Component,
444
- args: [{ selector: 'yuv-snack-bar-component', template: `
445
- <span matSnackBarLabel (click)="dismiss()"> {{ message() }} </span>
446
- @let a = action();
447
- @if (a) {
448
- <span matSnackBarActions>
449
- <button mat-button matSnackBarAction (click)="dismiss(!!a)">{{ action() }}</button>
450
- </span>
451
- }
452
- `, imports: [MatButtonModule, MatSnackBarLabel, MatSnackBarActions, MatSnackBarAction], host: {
453
- '[class.info]': "level() === 'info'",
454
- '[class.success]': "level() === 'success'",
455
- '[class.warning]': "level() === 'warning'",
456
- '[class.danger]': "level() === 'danger'"
457
- }, styles: [":host{display:flex}\n"] }]
458
- }] });
459
-
460
514
  /**
461
515
  * Utility service providing helper methods for the Halo Focus feature.
462
516
  *
@@ -854,6 +908,408 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImpo
854
908
  type: Injectable
855
909
  }] });
856
910
 
911
+ class SnackBarService {
912
+ #snackBar = inject(MatSnackBar);
913
+ info(message, action, duration) {
914
+ return this.snack(message, { action, level: 'info', ...(duration ? { duration } : {}) });
915
+ }
916
+ success(message, action, duration) {
917
+ return this.snack(message, { action, level: 'success', ...(duration ? { duration } : {}) });
918
+ }
919
+ warning(message, action, duration) {
920
+ return this.snack(message, { action, level: 'warning', ...(duration ? { duration } : {}) });
921
+ }
922
+ danger(message, action, duration) {
923
+ return this.snack(message, { action, level: 'danger', ...(duration ? { duration } : {}) });
924
+ }
925
+ snack(message, options) {
926
+ return this.#snackBar.openFromComponent(SnackBarComponent, {
927
+ data: {
928
+ level: options.level,
929
+ message,
930
+ action: options.action
931
+ },
932
+ ...(options.duration ? { duration: options.duration } : {}),
933
+ horizontalPosition: options.horizontalPosition,
934
+ verticalPosition: options.verticalPosition,
935
+ panelClass: ['yuv-snack-bar', 'level-' + options.level]
936
+ });
937
+ }
938
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SnackBarService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
939
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SnackBarService, providedIn: 'root' }); }
940
+ }
941
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SnackBarService, decorators: [{
942
+ type: Injectable,
943
+ args: [{
944
+ providedIn: 'root'
945
+ }]
946
+ }] });
947
+ class SnackBarComponent {
948
+ constructor() {
949
+ this.#snackBarRef = inject(MatSnackBarRef);
950
+ this.#snackData = inject(MAT_SNACK_BAR_DATA);
951
+ this.level = signal(this.#snackData.level);
952
+ this.message = signal(this.#snackData.message);
953
+ this.action = signal(this.#snackData.action);
954
+ }
955
+ #snackBarRef;
956
+ #snackData;
957
+ dismiss(withAction = false) {
958
+ if (withAction) {
959
+ this.#snackBarRef.dismissWithAction();
960
+ }
961
+ else if (!this.action()) {
962
+ this.#snackBarRef.dismiss();
963
+ }
964
+ }
965
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SnackBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
966
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.15", type: SnackBarComponent, isStandalone: true, selector: "yuv-snack-bar-component", host: { properties: { "class.info": "level() === 'info'", "class.success": "level() === 'success'", "class.warning": "level() === 'warning'", "class.danger": "level() === 'danger'" } }, ngImport: i0, template: `
967
+ <span matSnackBarLabel (click)="dismiss()"> {{ message() }} </span>
968
+ @let a = action();
969
+ @if (a) {
970
+ <span matSnackBarActions>
971
+ <button mat-button matSnackBarAction (click)="dismiss(!!a)">{{ action() }}</button>
972
+ </span>
973
+ }
974
+ `, isInline: true, styles: [":host{display:flex}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: MatSnackBarLabel, selector: "[matSnackBarLabel]" }, { kind: "directive", type: MatSnackBarActions, selector: "[matSnackBarActions]" }, { kind: "directive", type: MatSnackBarAction, selector: "[matSnackBarAction]" }] }); }
975
+ }
976
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SnackBarComponent, decorators: [{
977
+ type: Component,
978
+ args: [{ selector: 'yuv-snack-bar-component', template: `
979
+ <span matSnackBarLabel (click)="dismiss()"> {{ message() }} </span>
980
+ @let a = action();
981
+ @if (a) {
982
+ <span matSnackBarActions>
983
+ <button mat-button matSnackBarAction (click)="dismiss(!!a)">{{ action() }}</button>
984
+ </span>
985
+ }
986
+ `, imports: [MatButtonModule, MatSnackBarLabel, MatSnackBarActions, MatSnackBarAction], host: {
987
+ '[class.info]': "level() === 'info'",
988
+ '[class.success]': "level() === 'success'",
989
+ '[class.warning]': "level() === 'warning'",
990
+ '[class.danger]': "level() === 'danger'"
991
+ }, styles: [":host{display:flex}\n"] }]
992
+ }] });
993
+
994
+ /**
995
+ * Manages client-side session expiry: persists expiration, tracks user and HTTP activity,
996
+ * shows a pre-expiry popup with an extend CTA, and syncs state across tabs via BroadcastChannel.
997
+ *
998
+ * Key behaviors
999
+ * - Persists `expiresAt` in AppCacheService so multiple tabs share the same deadline
1000
+ * - Automatically listens to BackendService HTTP activity (debounced) to extend sessions
1001
+ * - Extends backend session by calling `/api-web/api/idm/whoami` endpoint (skipped when triggered by HTTP activity)
1002
+ * - User activity (mouse, keyboard, scroll) inside a defined window will auto-extend the session
1003
+ * - Displays a snack popup shortly before expiry; user can extend from the popup
1004
+ * - Broadcasts `SessionExtended` / `SessionLogout` to keep tabs in sync
1005
+ *
1006
+ * Setup options
1007
+ *
1008
+ * Option 1: Known session duration at startup
1009
+ * Use when the session duration is known in advance and remains constant.
1010
+ * ```ts
1011
+ * // app.config.ts
1012
+ * export const appConfig: ApplicationConfig = {
1013
+ * providers: [
1014
+ * provideSession(30 * 60 * 1000), // 30 minutes - set at app startup
1015
+ * ]
1016
+ * };
1017
+ * ```
1018
+ *
1019
+ * Option 2: Dynamic session duration from backend
1020
+ * Use when the session duration is determined after login (e.g., from backend response).
1021
+ * ```ts
1022
+ * // app.config.ts
1023
+ * export const appConfig: ApplicationConfig = {
1024
+ * providers: [
1025
+ * provideSession(), // Initialize without duration (defaults to 30 minutes)
1026
+ * ]
1027
+ * };
1028
+ *
1029
+ * // After login in AuthService:
1030
+ * login().subscribe(response => {
1031
+ * const sessionDuration = response.sessionExpiresIn; // from backend
1032
+ * sessionService.startSession(sessionDuration);
1033
+ * });
1034
+ * ```
1035
+ * Note: If no duration is provided to provideSession(), a default of 30 minutes is used until startSession() is called.
1036
+ *
1037
+ * Lifecycle notes
1038
+ * - HTTP activity is automatically tracked via BackendService.httpCommunicationOccurred$
1039
+ * - Backend session is extended via whoami endpoint, except when triggered by HTTP activity or cross-tab sync
1040
+ * - Internal flag prevents recursive whoami calls when the endpoint itself triggers httpCommunicationOccurred$
1041
+ * - Warning and logout timers are reset on every extend
1042
+ * - All timers and the popup are cleared on logout
1043
+ * - Broadcast messages are listened for and mirrored across tabs
1044
+ * - For UI activity, this service listens to DOM events (mousemove, keydown, click, scroll)
1045
+ */
1046
+ class SessionService {
1047
+ constructor() {
1048
+ //#region Dependencies
1049
+ /**
1050
+ * AppCacheService is used to persist the session expiry timestamp in localStorage.
1051
+ *
1052
+ * Why persist to storage instead of runtime memory:
1053
+ * - Single source of truth: All browser tabs can read the same expiry value directly
1054
+ * from storage without inter-tab communication
1055
+ * - Avoids master/slave tab pattern: No need to elect one "master" tab to hold the
1056
+ * authoritative expiry time and sync it to others
1057
+ * - Survives page refresh: Users don't lose their session when refreshing the page
1058
+ * - Simplifies synchronization: BroadcastChannel is only used for notifications about
1059
+ * changes (extend/logout), not for maintaining shared state
1060
+ * - Consistent behavior: Every tab independently calculates timers based on the same
1061
+ * persisted expiry value, ensuring uniform warning/logout timing
1062
+ */
1063
+ this.#appCacheService = inject(AppCacheService);
1064
+ this.#snackBarService = inject(SnackBarService);
1065
+ this.#backendService = inject(BackendService);
1066
+ this.#userService = inject(UserService);
1067
+ this.translate = inject(TranslateService);
1068
+ //#endregion
1069
+ //#region Properties
1070
+ this.#sessionDuration = sessionDefaultDuration;
1071
+ this.#sessionStorageKey = 'session-expires-at';
1072
+ this.#activityWindowBeforeEnd = sessionActivityWindowBeforeEnd;
1073
+ this.#popupBeforeEnd = sessionPopupBeforeEnd;
1074
+ this.#activityDetectedInWindow = false;
1075
+ this.#trackingWindowActivity = false;
1076
+ this.#userActivityTrackingInitialized = false;
1077
+ this.#extendingSessionViaBackend = false;
1078
+ this.#sessionChannel = new BroadcastChannel('session_channel');
1079
+ }
1080
+ //#region Dependencies
1081
+ /**
1082
+ * AppCacheService is used to persist the session expiry timestamp in localStorage.
1083
+ *
1084
+ * Why persist to storage instead of runtime memory:
1085
+ * - Single source of truth: All browser tabs can read the same expiry value directly
1086
+ * from storage without inter-tab communication
1087
+ * - Avoids master/slave tab pattern: No need to elect one "master" tab to hold the
1088
+ * authoritative expiry time and sync it to others
1089
+ * - Survives page refresh: Users don't lose their session when refreshing the page
1090
+ * - Simplifies synchronization: BroadcastChannel is only used for notifications about
1091
+ * changes (extend/logout), not for maintaining shared state
1092
+ * - Consistent behavior: Every tab independently calculates timers based on the same
1093
+ * persisted expiry value, ensuring uniform warning/logout timing
1094
+ */
1095
+ #appCacheService;
1096
+ #snackBarService;
1097
+ #backendService;
1098
+ #userService;
1099
+ //#endregion
1100
+ //#region Properties
1101
+ #sessionDuration;
1102
+ #sessionStorageKey;
1103
+ #activityWindowBeforeEnd;
1104
+ #popupBeforeEnd;
1105
+ #activityWindowStart$;
1106
+ #activityWindowEnd$;
1107
+ #logoutTimer$;
1108
+ #httpActivitySubscription$;
1109
+ #activityDetectedInWindow;
1110
+ #trackingWindowActivity;
1111
+ #userActivityTrackingInitialized;
1112
+ #extendingSessionViaBackend;
1113
+ #sessionChannel;
1114
+ #snackReference;
1115
+ //#endregion
1116
+ //#region Public methods
1117
+ /**
1118
+ * Initializes cross-tab listeners and activity hooks.
1119
+ *
1120
+ * IMPORTANT: This is automatically called by `provideSession()` via APP_INITIALIZER.
1121
+ * You should NOT call this manually - just add `provideSession()` to your app providers.
1122
+ *
1123
+ * What it does:
1124
+ * - Initializes session with provided or default duration value
1125
+ * - Wires BroadcastChannel subscriptions for cross-tab sync
1126
+ * - Subscribes to BackendService.httpCommunicationOccurred$ for automatic HTTP activity tracking
1127
+ * - Attaches DOM event listeners (mousemove, keydown, click, scroll) for the activity window
1128
+ */
1129
+ init(sessionDuration) {
1130
+ this.startSession(sessionDuration ?? sessionDefaultDuration);
1131
+ this.listenToChannel();
1132
+ this.setupHttpDebounce();
1133
+ this.setupUserActivityTracking();
1134
+ }
1135
+ /**
1136
+ * Sets the session duration and starts the session lifecycle.
1137
+ *
1138
+ * Use this method when you need to set or update the session duration dynamically,
1139
+ * typically after receiving authentication/session information from the backend.
1140
+ *
1141
+ * This is the correct approach when:
1142
+ * - Session duration is not known at application startup
1143
+ * - Duration comes from a backend API response after login
1144
+ * - You need to manually override the duration set via provideSession()
1145
+ *
1146
+ * Example:
1147
+ * ```ts
1148
+ * // After login in AuthService
1149
+ * login().subscribe(response => {
1150
+ * this.sessionService.startSession(response.sessionExpiresIn);
1151
+ * });
1152
+ * ```
1153
+ *
1154
+ * @param duration Total session length in milliseconds
1155
+ */
1156
+ startSession(duration) {
1157
+ this.#sessionDuration = duration;
1158
+ this.extendSession(false, undefined, true);
1159
+ }
1160
+ /**
1161
+ * Extends the session expiry, resets all timers, and optionally broadcasts to other tabs.
1162
+ *
1163
+ * @param broadcast When true (default), posts a BroadcastChannel message so other tabs sync
1164
+ * @param expiresAt Optional custom expiry timestamp; if not provided, calculates based on session duration
1165
+ * @param skipBackendCall When true, skips the whoami backend call (used when already triggered by HTTP activity)
1166
+ */
1167
+ extendSession(broadcast = true, expiresAt, skipBackendCall = false) {
1168
+ const newExpiresAt = expiresAt ?? Date.now() + this.#sessionDuration;
1169
+ this.setExpiresAt(newExpiresAt);
1170
+ this.resetAllTimers();
1171
+ if (!skipBackendCall && !this.#extendingSessionViaBackend) {
1172
+ this.#extendingSessionViaBackend = true;
1173
+ this.#backendService
1174
+ .get('/idm/whoami')
1175
+ .pipe(finalize(() => {
1176
+ // Keep flag true longer than debounce time to prevent duplicate broadcasts
1177
+ // from httpCommunicationOccurred$ triggered by this whoami call
1178
+ setTimeout(() => {
1179
+ this.#extendingSessionViaBackend = false;
1180
+ }, 600);
1181
+ if (broadcast) {
1182
+ this.#sessionChannel.postMessage({
1183
+ type: ChannelMessage.SessionExtended,
1184
+ expiresAt: newExpiresAt
1185
+ });
1186
+ }
1187
+ }))
1188
+ .subscribe();
1189
+ }
1190
+ else if (broadcast) {
1191
+ this.#sessionChannel.postMessage({
1192
+ type: ChannelMessage.SessionExtended,
1193
+ expiresAt: newExpiresAt
1194
+ });
1195
+ }
1196
+ }
1197
+ //#endregion
1198
+ //#region Core Logic
1199
+ listenToChannel() {
1200
+ this.#sessionChannel.onmessage = (event) => {
1201
+ const message = event.data;
1202
+ switch (message.type) {
1203
+ case ChannelMessage.SessionExtended:
1204
+ this.extendSession(false, message.expiresAt, true);
1205
+ break;
1206
+ case ChannelMessage.SessionLogout:
1207
+ this.performLogout(false);
1208
+ break;
1209
+ }
1210
+ };
1211
+ }
1212
+ scheduleActivityWindow() {
1213
+ this.getExpiresAt().subscribe((expiresAt) => {
1214
+ const activityWindowStartIn = expiresAt - Date.now() - this.#activityWindowBeforeEnd;
1215
+ const activityWindowEndIn = expiresAt - Date.now() - this.#popupBeforeEnd;
1216
+ this.#activityWindowStart$ = timer(Math.max(activityWindowStartIn, 0)).subscribe(() => {
1217
+ this.#trackingWindowActivity = true;
1218
+ this.#activityDetectedInWindow = false;
1219
+ });
1220
+ this.#activityWindowEnd$ = timer(Math.max(activityWindowEndIn, 0)).subscribe(() => {
1221
+ this.#trackingWindowActivity = false;
1222
+ if (this.#activityDetectedInWindow) {
1223
+ this.extendSession();
1224
+ }
1225
+ else {
1226
+ this.showPopup();
1227
+ }
1228
+ });
1229
+ });
1230
+ }
1231
+ showPopup() {
1232
+ if (this.#snackReference)
1233
+ return;
1234
+ const message = this.translate.instant('yuv.session.expires.message');
1235
+ const action = this.translate.instant('yuv.session.extend.action');
1236
+ this.#snackReference = this.#snackBarService.warning(message, action);
1237
+ this.scheduleLogout();
1238
+ this.#snackReference.onAction().subscribe(() => {
1239
+ this.#snackReference?.dismiss();
1240
+ this.extendSession();
1241
+ });
1242
+ this.#snackReference.afterDismissed().subscribe(() => {
1243
+ this.#snackReference = undefined;
1244
+ });
1245
+ }
1246
+ scheduleLogout() {
1247
+ this.#logoutTimer$?.unsubscribe();
1248
+ this.#logoutTimer$ = this.getExpiresAt()
1249
+ .pipe(switchMap((expiresAt) => timer(Math.max(expiresAt - Date.now(), 0)).pipe(map(() => expiresAt))))
1250
+ .subscribe((expiresAt) => {
1251
+ if (Date.now() >= expiresAt) {
1252
+ this.performLogout(true);
1253
+ }
1254
+ });
1255
+ }
1256
+ performLogout(broadcast = true) {
1257
+ this.clearTimers();
1258
+ this.#snackReference?.dismiss();
1259
+ if (broadcast) {
1260
+ this.#sessionChannel.postMessage({
1261
+ type: ChannelMessage.SessionLogout
1262
+ });
1263
+ }
1264
+ this.#userService.logout();
1265
+ }
1266
+ //#endregion
1267
+ //#region Activity + HTTP
1268
+ setupHttpDebounce() {
1269
+ this.#httpActivitySubscription$?.unsubscribe();
1270
+ this.#httpActivitySubscription$ = this.#backendService.httpCommunicationOccurred$.pipe(debounceTime(500)).subscribe(() => {
1271
+ if (!this.#extendingSessionViaBackend) {
1272
+ this.extendSession(true, undefined, true);
1273
+ }
1274
+ });
1275
+ }
1276
+ setupUserActivityTracking() {
1277
+ if (this.#userActivityTrackingInitialized)
1278
+ return;
1279
+ ['mousemove', 'keydown', 'click', 'scroll'].forEach((event) => window.addEventListener(event, () => {
1280
+ if (this.#trackingWindowActivity) {
1281
+ this.#activityDetectedInWindow = true;
1282
+ }
1283
+ }));
1284
+ this.#userActivityTrackingInitialized = true;
1285
+ }
1286
+ //#endregion
1287
+ //#region Utilities
1288
+ resetAllTimers() {
1289
+ this.clearTimers();
1290
+ this.scheduleActivityWindow();
1291
+ this.#snackReference?.dismiss();
1292
+ }
1293
+ getExpiresAt() {
1294
+ return this.#appCacheService.getItem(this.#sessionStorageKey);
1295
+ }
1296
+ setExpiresAt(expiresAt) {
1297
+ this.#appCacheService.setItem(this.#sessionStorageKey, expiresAt).subscribe();
1298
+ }
1299
+ clearTimers() {
1300
+ this.#activityWindowStart$?.unsubscribe();
1301
+ this.#activityWindowEnd$?.unsubscribe();
1302
+ this.#logoutTimer$?.unsubscribe();
1303
+ this.#httpActivitySubscription$?.unsubscribe();
1304
+ }
1305
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SessionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1306
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SessionService, providedIn: 'root' }); }
1307
+ }
1308
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: SessionService, decorators: [{
1309
+ type: Injectable,
1310
+ args: [{ providedIn: 'root' }]
1311
+ }] });
1312
+
857
1313
  /**
858
1314
  * Provides and initializes the Halo Focus feature for the Angular application.
859
1315
  *
@@ -924,9 +1380,71 @@ function provideHaloFocus(config) {
924
1380
  ]);
925
1381
  }
926
1382
 
1383
+ /**
1384
+ * Provides and initializes the SessionService at application startup.
1385
+ *
1386
+ * What it does
1387
+ * - Registers SessionService as a singleton to manage session expiry across the app
1388
+ * - Runs `session.init()` via APP_INITIALIZER when the app boots
1389
+ * - Initializes cross-tab BroadcastChannel sync, HTTP debounce hooks, and user-activity listeners
1390
+ * - Automatically tracks HTTP activity and user interactions to extend sessions
1391
+ *
1392
+ * Usage scenarios
1393
+ *
1394
+ * Scenario 1: Known session duration at startup
1395
+ * When the session duration is predetermined and constant, provide it here.
1396
+ * ```ts
1397
+ * // app.config.ts
1398
+ * export const appConfig: ApplicationConfig = {
1399
+ * providers: [
1400
+ * provideSession(30 * 60 * 1000), // 30 minutes
1401
+ * ]
1402
+ * };
1403
+ * ```
1404
+ *
1405
+ * Scenario 2: Dynamic session duration from backend
1406
+ * When the session duration is determined after login (e.g., from backend response),
1407
+ * omit the parameter here and call `startSession()` after receiving the duration.
1408
+ * ```ts
1409
+ * // app.config.ts
1410
+ * export const appConfig: ApplicationConfig = {
1411
+ * providers: [
1412
+ * provideSession(), // Defaults to 30 minutes until startSession() is called
1413
+ * ]
1414
+ * };
1415
+ *
1416
+ * // After login in AuthService:
1417
+ * login().subscribe(response => {
1418
+ * sessionService.startSession(response.sessionExpiresIn); // Set actual duration
1419
+ * });
1420
+ * ```
1421
+ *
1422
+ * @param sessionDuration Optional session duration in milliseconds. If omitted, defaults to 30 minutes (1800000ms).
1423
+ */
1424
+ function provideSession(sessionDuration) {
1425
+ return makeEnvironmentProviders([
1426
+ provideAppInitializer(() => {
1427
+ const session = inject(SessionService);
1428
+ session.init(sessionDuration);
1429
+ })
1430
+ ]);
1431
+ }
1432
+
1433
+ class YuuvisClientFrameworkModule {
1434
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: YuuvisClientFrameworkModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
1435
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.15", ngImport: i0, type: YuuvisClientFrameworkModule, imports: [CommonModule] }); }
1436
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: YuuvisClientFrameworkModule, imports: [CommonModule] }); }
1437
+ }
1438
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: YuuvisClientFrameworkModule, decorators: [{
1439
+ type: NgModule,
1440
+ args: [{
1441
+ imports: [CommonModule],
1442
+ }]
1443
+ }] });
1444
+
927
1445
  /**
928
1446
  * Generated bundle index. Do not edit.
929
1447
  */
930
1448
 
931
- export { HaloFocusService, HaloUtilityService, SnackBarComponent, SnackBarService, YuuvisClientFrameworkModule, provideHaloFocus };
1449
+ export { ChannelMessage, HaloFocusService, HaloUtilityService, SessionService, SnackBarComponent, SnackBarService, YuuvisClientFrameworkModule, defaultHaloFocusOffset, haloExcludedElementsInMatFormField, haloFocusNavigationKeys, haloFocusStyles, provideHaloFocus, provideSession, sessionActivityWindowBeforeEnd, sessionDefaultDuration, sessionPopupBeforeEnd };
932
1450
  //# sourceMappingURL=yuuvis-client-framework.mjs.map