@yuuvis/client-framework 3.8.1 → 3.8.2
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.
- package/fesm2022/yuuvis-client-framework-metadata-form-defaults.mjs +2 -2
- package/fesm2022/yuuvis-client-framework-metadata-form-defaults.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-object-details.mjs +47 -1
- package/fesm2022/yuuvis-client-framework-object-details.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-smart-search.mjs +11 -1
- package/fesm2022/yuuvis-client-framework-smart-search.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework-tile-list.mjs +4 -0
- package/fesm2022/yuuvis-client-framework-tile-list.mjs.map +1 -1
- package/fesm2022/yuuvis-client-framework.mjs +245 -227
- package/fesm2022/yuuvis-client-framework.mjs.map +1 -1
- package/lib/assets/i18n/de.json +24 -24
- package/lib/assets/i18n/en.json +24 -24
- package/package.json +5 -5
- package/types/yuuvis-client-framework.d.ts +1 -3
|
@@ -2,10 +2,12 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { inject, Injectable, ApplicationRef, DestroyRef, signal, ChangeDetectionStrategy, Component, makeEnvironmentProviders, provideAppInitializer, NgZone, NgModule } from '@angular/core';
|
|
3
3
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
4
4
|
import { SwUpdate } from '@angular/service-worker';
|
|
5
|
-
import { TranslateService } from '@ngx-translate/core';
|
|
6
|
-
import { ConfirmService } from '@yuuvis/client-framework/common';
|
|
7
|
-
import { filter, first, concat, interval, finalize, timer,
|
|
8
|
-
import {
|
|
5
|
+
import { _, TranslateService } from '@ngx-translate/core';
|
|
6
|
+
import { ConfirmService, DialogComponent } from '@yuuvis/client-framework/common';
|
|
7
|
+
import { filter, first, concat, interval, finalize, timer, debounceTime } from 'rxjs';
|
|
8
|
+
import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
|
|
9
|
+
import { TranslatePipe, AppCacheService, BackendService, UserService, EventService, YuvEventType } from '@yuuvis/client-core';
|
|
10
|
+
import { YmtButtonDirective } from '@yuuvis/material';
|
|
9
11
|
import * as i1 from '@angular/material/button';
|
|
10
12
|
import { MatButtonModule } from '@angular/material/button';
|
|
11
13
|
import { MatSnackBar, MatSnackBarRef, MAT_SNACK_BAR_DATA, MatSnackBarLabel, MatSnackBarActions, MatSnackBarAction } from '@angular/material/snack-bar';
|
|
@@ -966,6 +968,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
966
968
|
type: Injectable
|
|
967
969
|
}] });
|
|
968
970
|
|
|
971
|
+
_('yuv.pwa.update.unrecoverable.message');
|
|
972
|
+
_('yuv.pwa.update.message');
|
|
973
|
+
_('yuv.pwa.update.title');
|
|
974
|
+
_('yuv.pwa.update.confirm');
|
|
975
|
+
_('yuv.pwa.update.cancel');
|
|
969
976
|
/**
|
|
970
977
|
* Detects newly deployed versions of the application (PWA) and lets the user
|
|
971
978
|
* decide when to apply them.
|
|
@@ -1075,140 +1082,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
1075
1082
|
args: [{ providedIn: 'root' }]
|
|
1076
1083
|
}] });
|
|
1077
1084
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
success(message, action, duration) {
|
|
1094
|
-
return this.snack(message, {
|
|
1095
|
-
action,
|
|
1096
|
-
level: 'success',
|
|
1097
|
-
...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
warning(message, action, duration) {
|
|
1101
|
-
return this.snack(message, {
|
|
1102
|
-
action,
|
|
1103
|
-
level: 'warning',
|
|
1104
|
-
...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
danger(message, action, duration) {
|
|
1108
|
-
return this.snack(message, {
|
|
1109
|
-
action,
|
|
1110
|
-
level: 'danger',
|
|
1111
|
-
...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
snack(message, options) {
|
|
1115
|
-
return this.#snackBar.openFromComponent(SnackBarComponent, {
|
|
1116
|
-
data: {
|
|
1117
|
-
level: options.level,
|
|
1118
|
-
message,
|
|
1119
|
-
action: options.action
|
|
1120
|
-
},
|
|
1121
|
-
...(options.duration ? { duration: options.duration } : {}),
|
|
1122
|
-
horizontalPosition: options.horizontalPosition,
|
|
1123
|
-
verticalPosition: options.verticalPosition,
|
|
1124
|
-
panelClass: ['yuv-snack-bar', 'level-' + options.level]
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1127
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1128
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarService, providedIn: 'root' }); }
|
|
1129
|
-
}
|
|
1130
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarService, decorators: [{
|
|
1131
|
-
type: Injectable,
|
|
1132
|
-
args: [{
|
|
1133
|
-
providedIn: 'root'
|
|
1134
|
-
}]
|
|
1135
|
-
}], ctorParameters: () => [] });
|
|
1136
|
-
class SnackBarComponent {
|
|
1085
|
+
/**
|
|
1086
|
+
* Blocking modal shown shortly before session expiry.
|
|
1087
|
+
*
|
|
1088
|
+
* Replaces the previous non-blocking snackbar: opened with `disableClose: true`, it blocks
|
|
1089
|
+
* background interaction so an active user cannot miss it. Displays a live, second-by-second
|
|
1090
|
+
* countdown until expiry and a single "Extend session" action that closes the dialog with `true`.
|
|
1091
|
+
* If the countdown reaches zero, {@link SessionService} closes the dialog and logs the user out.
|
|
1092
|
+
*/
|
|
1093
|
+
class SessionExpiryDialogComponent {
|
|
1094
|
+
//#region Dependencies
|
|
1095
|
+
#dialogRef;
|
|
1096
|
+
#data;
|
|
1097
|
+
//#endregion
|
|
1137
1098
|
constructor() {
|
|
1138
|
-
|
|
1139
|
-
this.#
|
|
1140
|
-
this.#
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1099
|
+
//#region Dependencies
|
|
1100
|
+
this.#dialogRef = inject(MatDialogRef);
|
|
1101
|
+
this.#data = inject(MAT_DIALOG_DATA);
|
|
1102
|
+
//#endregion
|
|
1103
|
+
//#region Properties
|
|
1104
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
1105
|
+
this.remainingSeconds = signal(this.#computeRemaining(), ...(ngDevMode ? [{ debugName: "remainingSeconds" }] : /* istanbul ignore next */ []));
|
|
1106
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
1107
|
+
interval(1000)
|
|
1108
|
+
.pipe(takeUntilDestroyed())
|
|
1109
|
+
.subscribe(() => this.remainingSeconds.set(this.#computeRemaining()));
|
|
1144
1110
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
if (withAction) {
|
|
1150
|
-
this.#snackBarRef.dismissWithAction();
|
|
1151
|
-
}
|
|
1152
|
-
else if (!this.action()) {
|
|
1153
|
-
this.#snackBarRef.dismiss();
|
|
1154
|
-
}
|
|
1111
|
+
//#region Public methods
|
|
1112
|
+
/** Requests session extension by closing the dialog with a positive result. */
|
|
1113
|
+
extend() {
|
|
1114
|
+
this.#dialogRef.close(true);
|
|
1155
1115
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
[attr.role]="a ? null : 'button'"
|
|
1162
|
-
[attr.tabindex]="a ? null : 0"
|
|
1163
|
-
(click)="dismiss()"
|
|
1164
|
-
(keydown.enter)="dismiss()"
|
|
1165
|
-
(keydown.space)="dismiss(); $event.preventDefault()"
|
|
1166
|
-
>
|
|
1167
|
-
{{ message() }}
|
|
1168
|
-
</span>
|
|
1169
|
-
@if (a) {
|
|
1170
|
-
<span matSnackBarActions>
|
|
1171
|
-
<button mat-button matSnackBarAction (click)="dismiss(true)">{{ a }}</button>
|
|
1172
|
-
</span>
|
|
1116
|
+
//#endregion
|
|
1117
|
+
//#region Private methods
|
|
1118
|
+
#computeRemaining() {
|
|
1119
|
+
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
|
1120
|
+
return Math.max(0, Math.ceil((this.#data.expiresAt - Date.now()) / 1000));
|
|
1173
1121
|
}
|
|
1174
|
-
|
|
1122
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SessionExpiryDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1123
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.12", type: SessionExpiryDialogComponent, isStandalone: true, selector: "yuv-session-expiry-dialog", ngImport: i0, template: "<yuv-dialog [headertitle]=\"'yuv.session.expires.title' | translate\">\n <main>{{ 'yuv.session.expires.message' | translate: { seconds: remainingSeconds() } }}</main>\n <footer>\n <button ymtButton=\"primary\" type=\"button\" (click)=\"extend()\">\n {{ 'yuv.session.extend.action' | translate }}\n </button>\n </footer>\n</yuv-dialog>\n", styles: ["main{padding:var(--ymt-spacing-m)}\n"], dependencies: [{ kind: "component", type: DialogComponent, selector: "yuv-dialog", inputs: ["headertitle", "headertitel"] }, { kind: "directive", type: YmtButtonDirective, selector: "button[ymtButton], a[ymtButton]", inputs: ["ymtButton", "disabled", "aria-disabled", "disableRipple", "disabledInteractive", "button-size"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1175
1124
|
}
|
|
1176
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type:
|
|
1125
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SessionExpiryDialogComponent, decorators: [{
|
|
1177
1126
|
type: Component,
|
|
1178
|
-
args: [{ selector: 'yuv-
|
|
1179
|
-
|
|
1180
|
-
<span
|
|
1181
|
-
matSnackBarLabel
|
|
1182
|
-
[attr.role]="a ? null : 'button'"
|
|
1183
|
-
[attr.tabindex]="a ? null : 0"
|
|
1184
|
-
(click)="dismiss()"
|
|
1185
|
-
(keydown.enter)="dismiss()"
|
|
1186
|
-
(keydown.space)="dismiss(); $event.preventDefault()"
|
|
1187
|
-
>
|
|
1188
|
-
{{ message() }}
|
|
1189
|
-
</span>
|
|
1190
|
-
@if (a) {
|
|
1191
|
-
<span matSnackBarActions>
|
|
1192
|
-
<button mat-button matSnackBarAction (click)="dismiss(true)">{{ a }}</button>
|
|
1193
|
-
</span>
|
|
1194
|
-
}
|
|
1195
|
-
`, imports: [MatButtonModule, MatSnackBarLabel, MatSnackBarActions, MatSnackBarAction], host: {
|
|
1196
|
-
'[class.info]': "level() === 'info'",
|
|
1197
|
-
'[class.success]': "level() === 'success'",
|
|
1198
|
-
'[class.warning]': "level() === 'warning'",
|
|
1199
|
-
'[class.danger]': "level() === 'danger'"
|
|
1200
|
-
}, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:flex}\n"] }]
|
|
1201
|
-
}] });
|
|
1202
|
-
function isMessageDescriptor(value) {
|
|
1203
|
-
return typeof value === 'object';
|
|
1204
|
-
}
|
|
1205
|
-
function bindMessage(value, translate, fallback) {
|
|
1206
|
-
if (isMessageDescriptor(value)) {
|
|
1207
|
-
return toSignal(translate.stream(value.key, value.params), { initialValue: fallback ?? '' });
|
|
1208
|
-
}
|
|
1209
|
-
const literal = value;
|
|
1210
|
-
return signal(literal ?? fallback);
|
|
1211
|
-
}
|
|
1127
|
+
args: [{ selector: 'yuv-session-expiry-dialog', imports: [DialogComponent, YmtButtonDirective, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<yuv-dialog [headertitle]=\"'yuv.session.expires.title' | translate\">\n <main>{{ 'yuv.session.expires.message' | translate: { seconds: remainingSeconds() } }}</main>\n <footer>\n <button ymtButton=\"primary\" type=\"button\" (click)=\"extend()\">\n {{ 'yuv.session.extend.action' | translate }}\n </button>\n </footer>\n</yuv-dialog>\n", styles: ["main{padding:var(--ymt-spacing-m)}\n"] }]
|
|
1128
|
+
}], ctorParameters: () => [] });
|
|
1212
1129
|
|
|
1213
1130
|
/**
|
|
1214
1131
|
* Manages client-side session expiry: persists expiration, tracks user and HTTP activity,
|
|
@@ -1219,7 +1136,7 @@ function bindMessage(value, translate, fallback) {
|
|
|
1219
1136
|
* - Automatically listens to BackendService HTTP activity (debounced) to extend sessions
|
|
1220
1137
|
* - Extends backend session by calling `/api-web/api/idm/whoami` endpoint` (skipped when triggered by HTTP activity)
|
|
1221
1138
|
* - User activity (mouse, keyboard, scroll) inside a defined window will auto-extend the session
|
|
1222
|
-
* - Displays a
|
|
1139
|
+
* - Displays a blocking countdown dialog shortly before expiry; user can extend from the dialog
|
|
1223
1140
|
* - Broadcasts `SessionExtended` / `SessionLogout` to keep tabs in sync
|
|
1224
1141
|
*
|
|
1225
1142
|
* Setup options
|
|
@@ -1263,42 +1180,6 @@ function bindMessage(value, translate, fallback) {
|
|
|
1263
1180
|
* - For UI activity, this service listens to DOM events (mousemove, keydown, click, scroll)
|
|
1264
1181
|
*/
|
|
1265
1182
|
class SessionService {
|
|
1266
|
-
constructor() {
|
|
1267
|
-
//#region Dependencies
|
|
1268
|
-
/**
|
|
1269
|
-
* AppCacheService is used to persist the session expiry timestamp in localStorage.
|
|
1270
|
-
*
|
|
1271
|
-
* Why persist to storage instead of runtime memory:
|
|
1272
|
-
* - Single source of truth: All browser tabs can read the same expiry value directly
|
|
1273
|
-
* from storage without inter-tab communication
|
|
1274
|
-
* - Avoids master/slave tab pattern: No need to elect one "master" tab to hold the
|
|
1275
|
-
* authoritative expiry time and sync it to others
|
|
1276
|
-
* - Survives page refresh: Users don't lose their session when refreshing the page
|
|
1277
|
-
* - Simplifies synchronization: BroadcastChannel is only used for notifications about
|
|
1278
|
-
* changes (extend/logout), not for maintaining shared state
|
|
1279
|
-
* - Consistent behavior: Every tab independently calculates timers based on the same
|
|
1280
|
-
* persisted expiry value, ensuring uniform warning/logout timing
|
|
1281
|
-
*/
|
|
1282
|
-
this.#appCacheService = inject(AppCacheService);
|
|
1283
|
-
this.#snackBarService = inject(SnackBarService);
|
|
1284
|
-
this.#backendService = inject(BackendService);
|
|
1285
|
-
this.#userService = inject(UserService);
|
|
1286
|
-
this.#eventService = inject(EventService);
|
|
1287
|
-
this.#destroyRef = inject(DestroyRef);
|
|
1288
|
-
this.translate = inject(TranslateService);
|
|
1289
|
-
//#endregion
|
|
1290
|
-
//#region Properties
|
|
1291
|
-
this.#sessionDuration = sessionDefaultDuration;
|
|
1292
|
-
this.#sessionStorageKey = 'session-expires-at';
|
|
1293
|
-
this.#activityWindowBeforeEnd = sessionActivityWindowBeforeEnd;
|
|
1294
|
-
this.#popupBeforeEnd = sessionPopupBeforeEnd;
|
|
1295
|
-
this.#activityDetectedInWindow = false;
|
|
1296
|
-
this.#trackingWindowActivity = false;
|
|
1297
|
-
this.#userActivityTrackingInitialized = false;
|
|
1298
|
-
this.#extendingSessionViaBackend = false;
|
|
1299
|
-
this.#isLoggingOut = false;
|
|
1300
|
-
this.#sessionChannel = new BroadcastChannel('session_channel');
|
|
1301
|
-
}
|
|
1302
1183
|
//#region Dependencies
|
|
1303
1184
|
/**
|
|
1304
1185
|
* AppCacheService is used to persist the session expiry timestamp in localStorage.
|
|
@@ -1314,30 +1195,39 @@ class SessionService {
|
|
|
1314
1195
|
* - Consistent behavior: Every tab independently calculates timers based on the same
|
|
1315
1196
|
* persisted expiry value, ensuring uniform warning/logout timing
|
|
1316
1197
|
*/
|
|
1317
|
-
#appCacheService;
|
|
1318
|
-
#
|
|
1319
|
-
#backendService;
|
|
1320
|
-
#userService;
|
|
1321
|
-
#eventService;
|
|
1322
|
-
#destroyRef;
|
|
1198
|
+
#appCacheService = inject(AppCacheService);
|
|
1199
|
+
#dialog = inject(MatDialog);
|
|
1200
|
+
#backendService = inject(BackendService);
|
|
1201
|
+
#userService = inject(UserService);
|
|
1202
|
+
#eventService = inject(EventService);
|
|
1203
|
+
#destroyRef = inject(DestroyRef);
|
|
1323
1204
|
//#endregion
|
|
1324
1205
|
//#region Properties
|
|
1325
|
-
#sessionDuration;
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1206
|
+
#sessionDuration = sessionDefaultDuration;
|
|
1207
|
+
/**
|
|
1208
|
+
* In-memory source of truth for the current expiry timestamp (ms).
|
|
1209
|
+
*
|
|
1210
|
+
* All timer scheduling reads this synchronously. The value is also persisted to
|
|
1211
|
+
* AppCacheService for cross-tab/refresh recovery, but the persisted copy is intentionally
|
|
1212
|
+
* never read back for scheduling: the previous async read-after-write against IndexedDB
|
|
1213
|
+
* could return a stale (about-to-expire) value right after an extend, firing the logout
|
|
1214
|
+
* timer immediately and logging the user out despite extending.
|
|
1215
|
+
*/
|
|
1216
|
+
#expiresAt = 0;
|
|
1217
|
+
#sessionStorageKey = 'session-expires-at';
|
|
1218
|
+
#activityWindowBeforeEnd = sessionActivityWindowBeforeEnd;
|
|
1219
|
+
#popupBeforeEnd = sessionPopupBeforeEnd;
|
|
1330
1220
|
#activityWindowStart$;
|
|
1331
1221
|
#activityWindowEnd$;
|
|
1332
1222
|
#logoutTimer$;
|
|
1333
1223
|
#httpActivitySubscription$;
|
|
1334
|
-
#activityDetectedInWindow;
|
|
1335
|
-
#trackingWindowActivity;
|
|
1336
|
-
#userActivityTrackingInitialized;
|
|
1337
|
-
#extendingSessionViaBackend;
|
|
1338
|
-
#isLoggingOut;
|
|
1339
|
-
#sessionChannel;
|
|
1340
|
-
#
|
|
1224
|
+
#activityDetectedInWindow = false;
|
|
1225
|
+
#trackingWindowActivity = false;
|
|
1226
|
+
#userActivityTrackingInitialized = false;
|
|
1227
|
+
#extendingSessionViaBackend = false;
|
|
1228
|
+
#isLoggingOut = false;
|
|
1229
|
+
#sessionChannel = new BroadcastChannel('session_channel');
|
|
1230
|
+
#dialogRef;
|
|
1341
1231
|
//#endregion
|
|
1342
1232
|
//#region Public methods
|
|
1343
1233
|
/**
|
|
@@ -1395,6 +1285,7 @@ class SessionService {
|
|
|
1395
1285
|
if (this.#isLoggingOut)
|
|
1396
1286
|
return;
|
|
1397
1287
|
const newExpiresAt = expiresAt ?? Date.now() + this.#sessionDuration;
|
|
1288
|
+
this.#expiresAt = newExpiresAt;
|
|
1398
1289
|
this.setExpiresAt(newExpiresAt);
|
|
1399
1290
|
this.resetAllTimers();
|
|
1400
1291
|
if (skipBackendCall) {
|
|
@@ -1464,49 +1355,45 @@ class SessionService {
|
|
|
1464
1355
|
.subscribe(() => this.performLogout(true));
|
|
1465
1356
|
}
|
|
1466
1357
|
scheduleActivityWindow() {
|
|
1467
|
-
//
|
|
1468
|
-
//
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
this.#
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
this.#
|
|
1479
|
-
this
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
this.showPopup();
|
|
1485
|
-
}
|
|
1486
|
-
});
|
|
1358
|
+
// Scheduling reads #expiresAt synchronously (single source of truth) so the timers
|
|
1359
|
+
// always reflect the value just written by extendSession — no async read-after-write race.
|
|
1360
|
+
const now = Date.now();
|
|
1361
|
+
const activityWindowStartIn = this.#expiresAt - now - this.#activityWindowBeforeEnd;
|
|
1362
|
+
const activityWindowEndIn = this.#expiresAt - now - this.#popupBeforeEnd;
|
|
1363
|
+
this.#activityWindowStart$ = timer(Math.max(activityWindowStartIn, 0)).subscribe(() => {
|
|
1364
|
+
this.#trackingWindowActivity = true;
|
|
1365
|
+
this.#activityDetectedInWindow = false;
|
|
1366
|
+
});
|
|
1367
|
+
this.#activityWindowEnd$ = timer(Math.max(activityWindowEndIn, 0)).subscribe(() => {
|
|
1368
|
+
this.#trackingWindowActivity = false;
|
|
1369
|
+
if (this.#activityDetectedInWindow) {
|
|
1370
|
+
this.extendSession();
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
this.showPopup();
|
|
1374
|
+
}
|
|
1487
1375
|
});
|
|
1488
1376
|
}
|
|
1489
1377
|
showPopup() {
|
|
1490
|
-
if (this.#
|
|
1378
|
+
if (this.#dialogRef)
|
|
1491
1379
|
return;
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
this.#snackReference.onAction().subscribe(() => {
|
|
1497
|
-
this.#snackReference?.dismiss();
|
|
1498
|
-
this.extendSession();
|
|
1380
|
+
this.#dialogRef = this.#dialog.open(SessionExpiryDialogComponent, {
|
|
1381
|
+
data: { expiresAt: this.#expiresAt },
|
|
1382
|
+
disableClose: true,
|
|
1383
|
+
panelClass: 'yuv-session-expiry-dialog'
|
|
1499
1384
|
});
|
|
1500
|
-
this
|
|
1501
|
-
|
|
1385
|
+
this.scheduleLogout();
|
|
1386
|
+
this.#dialogRef.afterClosed().subscribe((extend) => {
|
|
1387
|
+
this.#dialogRef = undefined;
|
|
1388
|
+
if (extend) {
|
|
1389
|
+
this.extendSession();
|
|
1390
|
+
}
|
|
1502
1391
|
});
|
|
1503
1392
|
}
|
|
1504
1393
|
scheduleLogout() {
|
|
1505
1394
|
this.#logoutTimer$?.unsubscribe();
|
|
1506
|
-
this.#logoutTimer$ = this.
|
|
1507
|
-
|
|
1508
|
-
.subscribe((expiresAt) => {
|
|
1509
|
-
if (Date.now() >= expiresAt) {
|
|
1395
|
+
this.#logoutTimer$ = timer(Math.max(this.#expiresAt - Date.now(), 0)).subscribe(() => {
|
|
1396
|
+
if (Date.now() >= this.#expiresAt) {
|
|
1510
1397
|
this.performLogout(true);
|
|
1511
1398
|
}
|
|
1512
1399
|
});
|
|
@@ -1517,7 +1404,7 @@ class SessionService {
|
|
|
1517
1404
|
this.#isLoggingOut = true;
|
|
1518
1405
|
this.clearTimers();
|
|
1519
1406
|
this.#httpActivitySubscription$?.unsubscribe();
|
|
1520
|
-
this.#
|
|
1407
|
+
this.#dialogRef?.close();
|
|
1521
1408
|
if (broadcast) {
|
|
1522
1409
|
this.#sessionChannel.postMessage({
|
|
1523
1410
|
type: ChannelMessage.SessionLogout
|
|
@@ -1555,16 +1442,12 @@ class SessionService {
|
|
|
1555
1442
|
resetAllTimers() {
|
|
1556
1443
|
this.clearTimers();
|
|
1557
1444
|
this.scheduleActivityWindow();
|
|
1558
|
-
this.#
|
|
1559
|
-
}
|
|
1560
|
-
getExpiresAt() {
|
|
1561
|
-
return this.#appCacheService.getItem(this.#sessionStorageKey);
|
|
1445
|
+
this.#dialogRef?.close();
|
|
1562
1446
|
}
|
|
1563
1447
|
setExpiresAt(expiresAt) {
|
|
1564
1448
|
this.#appCacheService.setItem(this.#sessionStorageKey, expiresAt).subscribe();
|
|
1565
1449
|
}
|
|
1566
1450
|
clearTimers() {
|
|
1567
|
-
this.#activityWindowSchedule$?.unsubscribe();
|
|
1568
1451
|
this.#activityWindowStart$?.unsubscribe();
|
|
1569
1452
|
this.#activityWindowEnd$?.unsubscribe();
|
|
1570
1453
|
this.#logoutTimer$?.unsubscribe();
|
|
@@ -1577,6 +1460,141 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
1577
1460
|
args: [{ providedIn: 'root' }]
|
|
1578
1461
|
}] });
|
|
1579
1462
|
|
|
1463
|
+
const SNACK_BAR_DEFAULT_DURATION = 3000;
|
|
1464
|
+
class SnackBarService {
|
|
1465
|
+
#snackBar;
|
|
1466
|
+
constructor() {
|
|
1467
|
+
this.#snackBar = inject(MatSnackBar);
|
|
1468
|
+
this.translate = inject(TranslateService);
|
|
1469
|
+
this.translate.onLangChange.pipe(takeUntilDestroyed()).subscribe(() => this.#snackBar.dismiss());
|
|
1470
|
+
}
|
|
1471
|
+
info(message, action, duration) {
|
|
1472
|
+
return this.snack(message, {
|
|
1473
|
+
action,
|
|
1474
|
+
level: 'info',
|
|
1475
|
+
...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
success(message, action, duration) {
|
|
1479
|
+
return this.snack(message, {
|
|
1480
|
+
action,
|
|
1481
|
+
level: 'success',
|
|
1482
|
+
...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
warning(message, action, duration) {
|
|
1486
|
+
return this.snack(message, {
|
|
1487
|
+
action,
|
|
1488
|
+
level: 'warning',
|
|
1489
|
+
...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
danger(message, action, duration) {
|
|
1493
|
+
return this.snack(message, {
|
|
1494
|
+
action,
|
|
1495
|
+
level: 'danger',
|
|
1496
|
+
...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
snack(message, options) {
|
|
1500
|
+
return this.#snackBar.openFromComponent(SnackBarComponent, {
|
|
1501
|
+
data: {
|
|
1502
|
+
level: options.level,
|
|
1503
|
+
message,
|
|
1504
|
+
action: options.action
|
|
1505
|
+
},
|
|
1506
|
+
...(options.duration ? { duration: options.duration } : {}),
|
|
1507
|
+
horizontalPosition: options.horizontalPosition,
|
|
1508
|
+
verticalPosition: options.verticalPosition,
|
|
1509
|
+
panelClass: ['yuv-snack-bar', 'level-' + options.level]
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1513
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarService, providedIn: 'root' }); }
|
|
1514
|
+
}
|
|
1515
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarService, decorators: [{
|
|
1516
|
+
type: Injectable,
|
|
1517
|
+
args: [{
|
|
1518
|
+
providedIn: 'root'
|
|
1519
|
+
}]
|
|
1520
|
+
}], ctorParameters: () => [] });
|
|
1521
|
+
class SnackBarComponent {
|
|
1522
|
+
constructor() {
|
|
1523
|
+
this.#snackBarRef = inject(MatSnackBarRef);
|
|
1524
|
+
this.#snackData = inject(MAT_SNACK_BAR_DATA);
|
|
1525
|
+
this.#translate = inject(TranslateService);
|
|
1526
|
+
this.level = signal(this.#snackData.level, ...(ngDevMode ? [{ debugName: "level" }] : /* istanbul ignore next */ []));
|
|
1527
|
+
this.message = bindMessage(this.#snackData.message, this.#translate, '');
|
|
1528
|
+
this.action = bindMessage(this.#snackData.action, this.#translate, undefined);
|
|
1529
|
+
}
|
|
1530
|
+
#snackBarRef;
|
|
1531
|
+
#snackData;
|
|
1532
|
+
#translate;
|
|
1533
|
+
dismiss(withAction = false) {
|
|
1534
|
+
if (withAction) {
|
|
1535
|
+
this.#snackBarRef.dismissWithAction();
|
|
1536
|
+
}
|
|
1537
|
+
else if (!this.action()) {
|
|
1538
|
+
this.#snackBarRef.dismiss();
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1542
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", 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: `
|
|
1543
|
+
@let a = action();
|
|
1544
|
+
<span
|
|
1545
|
+
matSnackBarLabel
|
|
1546
|
+
[attr.role]="a ? null : 'button'"
|
|
1547
|
+
[attr.tabindex]="a ? null : 0"
|
|
1548
|
+
(click)="dismiss()"
|
|
1549
|
+
(keydown.enter)="dismiss()"
|
|
1550
|
+
(keydown.space)="dismiss(); $event.preventDefault()"
|
|
1551
|
+
>
|
|
1552
|
+
{{ message() }}
|
|
1553
|
+
</span>
|
|
1554
|
+
@if (a) {
|
|
1555
|
+
<span matSnackBarActions>
|
|
1556
|
+
<button mat-button matSnackBarAction (click)="dismiss(true)">{{ a }}</button>
|
|
1557
|
+
</span>
|
|
1558
|
+
}
|
|
1559
|
+
`, isInline: true, styles: [":host{display:flex}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatSnackBarLabel, selector: "[matSnackBarLabel]" }, { kind: "directive", type: MatSnackBarActions, selector: "[matSnackBarActions]" }, { kind: "directive", type: MatSnackBarAction, selector: "[matSnackBarAction]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1560
|
+
}
|
|
1561
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarComponent, decorators: [{
|
|
1562
|
+
type: Component,
|
|
1563
|
+
args: [{ selector: 'yuv-snack-bar-component', template: `
|
|
1564
|
+
@let a = action();
|
|
1565
|
+
<span
|
|
1566
|
+
matSnackBarLabel
|
|
1567
|
+
[attr.role]="a ? null : 'button'"
|
|
1568
|
+
[attr.tabindex]="a ? null : 0"
|
|
1569
|
+
(click)="dismiss()"
|
|
1570
|
+
(keydown.enter)="dismiss()"
|
|
1571
|
+
(keydown.space)="dismiss(); $event.preventDefault()"
|
|
1572
|
+
>
|
|
1573
|
+
{{ message() }}
|
|
1574
|
+
</span>
|
|
1575
|
+
@if (a) {
|
|
1576
|
+
<span matSnackBarActions>
|
|
1577
|
+
<button mat-button matSnackBarAction (click)="dismiss(true)">{{ a }}</button>
|
|
1578
|
+
</span>
|
|
1579
|
+
}
|
|
1580
|
+
`, imports: [MatButtonModule, MatSnackBarLabel, MatSnackBarActions, MatSnackBarAction], host: {
|
|
1581
|
+
'[class.info]': "level() === 'info'",
|
|
1582
|
+
'[class.success]': "level() === 'success'",
|
|
1583
|
+
'[class.warning]': "level() === 'warning'",
|
|
1584
|
+
'[class.danger]': "level() === 'danger'"
|
|
1585
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:flex}\n"] }]
|
|
1586
|
+
}] });
|
|
1587
|
+
function isMessageDescriptor(value) {
|
|
1588
|
+
return typeof value === 'object';
|
|
1589
|
+
}
|
|
1590
|
+
function bindMessage(value, translate, fallback) {
|
|
1591
|
+
if (isMessageDescriptor(value)) {
|
|
1592
|
+
return toSignal(translate.stream(value.key, value.params), { initialValue: fallback ?? '' });
|
|
1593
|
+
}
|
|
1594
|
+
const literal = value;
|
|
1595
|
+
return signal(literal ?? fallback);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1580
1598
|
/**
|
|
1581
1599
|
* Provides and initializes the Halo Focus feature for the Angular application.
|
|
1582
1600
|
*
|