oip-common 0.6.6 → 0.7.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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, inject, signal, computed, effect, isDevMode, DestroyRef, ChangeDetectorRef, Component, Input, InjectionToken, PLATFORM_ID, ViewChild, HostBinding, EventEmitter, Output, Renderer2, SecurityContext, ChangeDetectionStrategy, Injector, EnvironmentInjector, ViewContainerRef, makeEnvironmentProviders, importProvidersFrom } from '@angular/core';
2
+ import { Injectable, inject, signal, computed, effect, isDevMode, DestroyRef, ChangeDetectorRef, Component, Input, InjectionToken, PLATFORM_ID, ViewChild, HostBinding, EventEmitter, Output, untracked, Renderer2, SecurityContext, ChangeDetectionStrategy, Injector, EnvironmentInjector, ViewContainerRef, makeEnvironmentProviders, importProvidersFrom } from '@angular/core';
3
3
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
4
  import { Subject, BehaviorSubject, of, tap, shareReplay, ReplaySubject, firstValueFrom, from, filter as filter$1 } from 'rxjs';
5
5
  import * as i2$1 from 'primeng/api';
@@ -8,7 +8,7 @@ import { HttpErrorResponse, HttpClient as HttpClient$1, provideHttpClient, withI
8
8
  import * as i1$7 from '@ngx-translate/core';
9
9
  import { TranslateService, TranslatePipe, TranslateModule, TranslateLoader } from '@ngx-translate/core';
10
10
  import * as i1$5 from '@angular/router';
11
- import { Router, ActivatedRoute, RouterModule, NavigationEnd, RouterLinkActive, RouterLink } from '@angular/router';
11
+ import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError, NavigationSkipped, ActivatedRoute, RouterModule, RouterLinkActive, RouterLink } from '@angular/router';
12
12
  import { tap as tap$1, catchError, filter, distinctUntilChanged, switchMap, map, take } from 'rxjs/operators';
13
13
  import { Title, DomSanitizer } from '@angular/platform-browser';
14
14
  import { PrimeNG } from 'primeng/config';
@@ -26,8 +26,6 @@ import * as i3$3 from 'primeng/styleclass';
26
26
  import { StyleClassModule } from 'primeng/styleclass';
27
27
  import { updatePreset, updateSurfacePalette, $t, definePreset } from '@primeng/themes';
28
28
  import Aura from '@primeng/themes/aura';
29
- import Lara from '@primeng/themes/lara';
30
- import Nora from '@primeng/themes/nora';
31
29
  import * as i3 from 'primeng/selectbutton';
32
30
  import { SelectButtonModule } from 'primeng/selectbutton';
33
31
  import { Tabs, TabList, Tab } from 'primeng/tabs';
@@ -41,7 +39,7 @@ import { PaginatorModule } from 'primeng/paginator';
41
39
  import * as i7 from 'primeng/popover';
42
40
  import { Popover, PopoverModule } from 'primeng/popover';
43
41
  import * as i4 from 'primeng/progressspinner';
44
- import { ProgressSpinnerModule } from 'primeng/progressspinner';
42
+ import { ProgressSpinnerModule, ProgressSpinner } from 'primeng/progressspinner';
45
43
  import * as i9 from 'primeng/tag';
46
44
  import { Tag, TagModule } from 'primeng/tag';
47
45
  import * as signalR from '@microsoft/signalr';
@@ -546,7 +544,7 @@ var msgService$1 = {
546
544
  secondary: "Secondary"
547
545
  };
548
546
  var primeng$1 = "";
549
- var en$h = {
547
+ var en$j = {
550
548
  msgService: msgService$1,
551
549
  primeng: primeng$1
552
550
  };
@@ -750,7 +748,7 @@ var primeng = {
750
748
  zoomOut: "Уменьшить"
751
749
  }
752
750
  };
753
- var ru$h = {
751
+ var ru$j = {
754
752
  msgService: msgService,
755
753
  primeng: primeng
756
754
  };
@@ -759,7 +757,7 @@ var ru$h = {
759
757
  * Global dictionaries shared by all components. They are bundled with the library,
760
758
  * so the application never requests `assets/i18n/{lang}.json`.
761
759
  */
762
- const globalTranslations = { en: en$h, ru: ru$h };
760
+ const globalTranslations = { en: en$j, ru: ru$j };
763
761
  /**
764
762
  * Service for managing translation loading in the application
765
763
  */
@@ -1378,13 +1376,94 @@ function collectKeyPaths(value, prefix = '', paths = new Set()) {
1378
1376
  return paths;
1379
1377
  }
1380
1378
 
1379
+ /**
1380
+ * Tracks whether the application is switching between modules.
1381
+ *
1382
+ * Two sources feed the state:
1383
+ * - router navigation, from `NavigationStart` until the navigation ends, is cancelled or fails;
1384
+ * - explicit work registered with {@link begin}/{@link end} or {@link track}, which covers everything
1385
+ * running after `NavigationEnd`: module instance rights, settings and extension loading.
1386
+ *
1387
+ * The UI blocker rendered by the layout observes {@link loading}.
1388
+ */
1389
+ class ModuleLoadingService {
1390
+ /** Releases a stuck blocker when a caller never balances its `begin()`. */
1391
+ static { this.watchdogTimeoutMs = 30_000; }
1392
+ constructor() {
1393
+ this.router = inject(Router);
1394
+ this.navigating = signal(false, ...(ngDevMode ? [{ debugName: "navigating" }] : []));
1395
+ this.pending = signal(0, ...(ngDevMode ? [{ debugName: "pending" }] : []));
1396
+ /** True while a module transition is in progress. */
1397
+ this.loading = computed(() => this.navigating() || this.pending() > 0, ...(ngDevMode ? [{ debugName: "loading" }] : []));
1398
+ this.router.events.pipe(takeUntilDestroyed()).subscribe((event) => {
1399
+ if (event instanceof NavigationStart) {
1400
+ this.navigating.set(true);
1401
+ }
1402
+ else if (event instanceof NavigationEnd ||
1403
+ event instanceof NavigationCancel ||
1404
+ event instanceof NavigationError ||
1405
+ event instanceof NavigationSkipped) {
1406
+ this.navigating.set(false);
1407
+ }
1408
+ });
1409
+ }
1410
+ /**
1411
+ * Registers one unit of loading work. Every call must be balanced with {@link end},
1412
+ * preferably from a `finally` block. Prefer {@link track} when the work is a promise.
1413
+ */
1414
+ begin() {
1415
+ this.pending.update((count) => count + 1);
1416
+ this.armWatchdog();
1417
+ }
1418
+ /** Releases one unit of loading work registered with {@link begin}. */
1419
+ end() {
1420
+ this.pending.update((count) => Math.max(0, count - 1));
1421
+ this.armWatchdog();
1422
+ }
1423
+ /**
1424
+ * Blocks the UI until `work` settles, and keeps the blocker released when it rejects.
1425
+ *
1426
+ * @example
1427
+ * await this.moduleLoading.track(this.reloadModuleInstance());
1428
+ */
1429
+ async track(work) {
1430
+ this.begin();
1431
+ try {
1432
+ return await work;
1433
+ }
1434
+ finally {
1435
+ this.end();
1436
+ }
1437
+ }
1438
+ armWatchdog() {
1439
+ if (this.watchdogHandle != null) {
1440
+ clearTimeout(this.watchdogHandle);
1441
+ this.watchdogHandle = undefined;
1442
+ }
1443
+ if (this.pending() === 0) {
1444
+ return;
1445
+ }
1446
+ this.watchdogHandle = setTimeout(() => {
1447
+ this.watchdogHandle = undefined;
1448
+ console.warn(`[ModuleLoadingService] Loading work did not finish within ${ModuleLoadingService.watchdogTimeoutMs}ms, releasing the UI blocker.`);
1449
+ this.pending.set(0);
1450
+ }, ModuleLoadingService.watchdogTimeoutMs);
1451
+ }
1452
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ModuleLoadingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1453
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ModuleLoadingService, providedIn: 'root' }); }
1454
+ }
1455
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ModuleLoadingService, decorators: [{
1456
+ type: Injectable,
1457
+ args: [{ providedIn: 'root' }]
1458
+ }], ctorParameters: () => [] });
1459
+
1381
1460
  var baseComponent$1 = {
1382
1461
  content: "Content",
1383
1462
  settings: "Settings",
1384
1463
  security: "Security",
1385
1464
  success: "Saved successfully"
1386
1465
  };
1387
- var en$g = {
1466
+ var en$i = {
1388
1467
  baseComponent: baseComponent$1
1389
1468
  };
1390
1469
 
@@ -1394,7 +1473,7 @@ var baseComponent = {
1394
1473
  security: "Безопасность",
1395
1474
  success: "Сохранено"
1396
1475
  };
1397
- var ru$g = {
1476
+ var ru$i = {
1398
1477
  baseComponent: baseComponent
1399
1478
  };
1400
1479
 
@@ -1471,6 +1550,7 @@ class BaseModuleComponent {
1471
1550
  this.securitySettings = [];
1472
1551
  this.destroyRef = inject(DestroyRef);
1473
1552
  this.securityService = inject(SecurityService);
1553
+ this.moduleLoadingService = inject(ModuleLoadingService);
1474
1554
  this.httpClient = inject(HttpClient);
1475
1555
  /**
1476
1556
  * Provide access to app settings
@@ -1529,7 +1609,7 @@ class BaseModuleComponent {
1529
1609
  * @type {Subject<TLocalStoreSettings>}
1530
1610
  */
1531
1611
  this.localSettingsUpdate = new Subject();
1532
- this.baseTranslations = provideTranslations({ en: en$g, ru: ru$g });
1612
+ this.baseTranslations = provideTranslations({ en: en$i, ru: ru$i });
1533
1613
  this.l10nService = inject(L10nService);
1534
1614
  this.canRead = false;
1535
1615
  this.canEdit = false;
@@ -1757,7 +1837,8 @@ class BaseModuleComponent {
1757
1837
  });
1758
1838
  }
1759
1839
  async reloadModuleInstance() {
1760
- this.moduleInstanceReloadPromise = this.moduleInstanceReloadPromise.then(async () => {
1840
+ // Blocks the UI for the whole bootstrap: router navigation has already ended by the time it runs.
1841
+ this.moduleInstanceReloadPromise = this.moduleInstanceReloadPromise.then(() => this.moduleLoadingService.track((async () => {
1761
1842
  // Rights first: the backend rejects settings and data requests without the read right.
1762
1843
  await this.watchSecurityRights();
1763
1844
  if (!this.canRead) {
@@ -1765,7 +1846,7 @@ class BaseModuleComponent {
1765
1846
  }
1766
1847
  await this.getSettings();
1767
1848
  await this.onModuleInstanceChange();
1768
- });
1849
+ })()));
1769
1850
  await this.moduleInstanceReloadPromise;
1770
1851
  }
1771
1852
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: BaseModuleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
@@ -1829,7 +1910,7 @@ var securityComponent$1 = {
1829
1910
  savedSecurity: "Security settings saved",
1830
1911
  selectRoles: "Select roles"
1831
1912
  };
1832
- var en$f = {
1913
+ var en$h = {
1833
1914
  securityComponent: securityComponent$1
1834
1915
  };
1835
1916
 
@@ -1839,13 +1920,13 @@ var securityComponent = {
1839
1920
  savedSecurity: "Настройки сохранены",
1840
1921
  selectRoles: "Выберите роли"
1841
1922
  };
1842
- var ru$f = {
1923
+ var ru$h = {
1843
1924
  securityComponent: securityComponent
1844
1925
  };
1845
1926
 
1846
1927
  class SecurityComponent {
1847
1928
  constructor() {
1848
- this.translations = provideTranslations({ en: en$f, ru: ru$f });
1929
+ this.translations = provideTranslations({ en: en$h, ru: ru$h });
1849
1930
  this.msgService = inject(MsgService);
1850
1931
  this.translateService = inject(TranslateService);
1851
1932
  this.httpClient = inject(HttpClient);
@@ -2014,7 +2095,7 @@ const APP_THEME_PRESETS_MERGE_MODE = new InjectionToken('APP_THEME_PRESETS_MERGE
2014
2095
  factory: () => 'mergeWithDefaults'
2015
2096
  });
2016
2097
 
2017
- var en$e = {
2098
+ var en$g = {
2018
2099
  "app-configurator": {
2019
2100
  primary: "Primary",
2020
2101
  surface: "Surface",
@@ -2025,7 +2106,7 @@ var en$e = {
2025
2106
  }
2026
2107
  };
2027
2108
 
2028
- var ru$e = {
2109
+ var ru$g = {
2029
2110
  "app-configurator": {
2030
2111
  primary: "Основной",
2031
2112
  surface: "Фон",
@@ -2036,11 +2117,19 @@ var ru$e = {
2036
2117
  }
2037
2118
  };
2038
2119
 
2120
+ /**
2121
+ * Aura stays eager: it is the fallback preset and its primitive palette backs the color swatches
2122
+ * shown before any other preset is resolved. Lara and Nora are loaded on demand, which keeps roughly
2123
+ * 300 KB out of the initial bundle for applications that never switch away from the default theme.
2124
+ */
2039
2125
  const DEFAULT_THEME_PRESETS = [
2040
2126
  { id: 'Aura', label: 'Aura', preset: Aura },
2041
- { id: 'Lara', label: 'Lara', preset: Lara },
2042
- { id: 'Nora', label: 'Nora', preset: Nora }
2127
+ { id: 'Lara', label: 'Lara', preset: () => import('@primeng/themes/lara').then((m) => m.default) },
2128
+ { id: 'Nora', label: 'Nora', preset: () => import('@primeng/themes/nora').then((m) => m.default) }
2043
2129
  ];
2130
+ function isPresetLoader(preset) {
2131
+ return typeof preset === 'function';
2132
+ }
2044
2133
  const PRIMARY_COLORS = [
2045
2134
  'emerald',
2046
2135
  'green',
@@ -2061,7 +2150,7 @@ const PRIMARY_COLORS = [
2061
2150
  ];
2062
2151
  class AppConfiguratorComponent {
2063
2152
  constructor() {
2064
- this.translations = provideTranslations({ en: en$e, ru: ru$e });
2153
+ this.translations = provideTranslations({ en: en$g, ru: ru$g });
2065
2154
  this.router = inject(Router);
2066
2155
  this.config = inject(PrimeNG);
2067
2156
  this.layoutService = inject(LayoutService);
@@ -2072,6 +2161,14 @@ class AppConfiguratorComponent {
2072
2161
  this.themePresets = this.getThemePresets();
2073
2162
  this.themePresetsMap = new Map(this.themePresets.map((theme) => [theme.id, theme]));
2074
2163
  this.defaultThemePreset = this.themePresets[0] ?? DEFAULT_THEME_PRESETS[0];
2164
+ /**
2165
+ * Presets resolved so far, keyed by theme id. Seeded with the eagerly provided ones and filled in
2166
+ * as loaders resolve; it is a signal so the color swatches recompute once a preset arrives.
2167
+ */
2168
+ this.resolvedPresets = signal(new Map(this.themePresets
2169
+ .filter((theme) => !isPresetLoader(theme.preset))
2170
+ .map((theme) => [theme.id, theme.preset])), ...(ngDevMode ? [{ debugName: "resolvedPresets" }] : []));
2171
+ this.pendingPresets = new Map();
2075
2172
  this.fallbackPrimaryColors = (DEFAULT_THEME_PRESETS[0].preset.primitive ?? {});
2076
2173
  this.presets = this.themePresets.map((theme) => ({ label: theme.label ?? theme.id, value: theme.id }));
2077
2174
  this.showMenuModeButton = signal(!this.router.url.includes('auth'), ...(ngDevMode ? [{ debugName: "showMenuModeButton" }] : []));
@@ -2235,7 +2332,7 @@ class AppConfiguratorComponent {
2235
2332
  ngOnInit() {
2236
2333
  if (isPlatformBrowser(this.platformId)) {
2237
2334
  const presetId = this.ensureValidThemeId(this.layoutService.layoutConfig().preset);
2238
- this.onPresetChange(presetId);
2335
+ void this.onPresetChange(presetId);
2239
2336
  }
2240
2337
  }
2241
2338
  getPresetExt() {
@@ -2385,13 +2482,46 @@ class AppConfiguratorComponent {
2385
2482
  getThemeById(themeId) {
2386
2483
  return this.themePresetsMap.get(themeId ?? '') ?? this.defaultThemePreset;
2387
2484
  }
2485
+ /**
2486
+ * The preset of a theme, or `undefined` while its loader is still in flight.
2487
+ */
2488
+ getResolvedPreset(theme) {
2489
+ return isPresetLoader(theme.preset) ? this.resolvedPresets().get(theme.id) : theme.preset;
2490
+ }
2491
+ /**
2492
+ * Resolves a theme preset, running its loader at most once and caching the result.
2493
+ * A failed load falls back to the default preset so the configurator stays usable.
2494
+ */
2495
+ async loadPreset(theme) {
2496
+ const resolved = this.getResolvedPreset(theme);
2497
+ if (resolved) {
2498
+ return resolved;
2499
+ }
2500
+ const pending = this.pendingPresets.get(theme.id);
2501
+ if (pending) {
2502
+ return pending;
2503
+ }
2504
+ const load = theme.preset()
2505
+ .then((preset) => {
2506
+ this.resolvedPresets.update((presets) => new Map(presets).set(theme.id, preset));
2507
+ return preset;
2508
+ })
2509
+ .catch((error) => {
2510
+ console.error(`[AppConfigurator] Failed to load theme preset "${theme.id}".`, error);
2511
+ return this.getResolvedPreset(this.defaultThemePreset) ?? Aura;
2512
+ })
2513
+ .finally(() => this.pendingPresets.delete(theme.id));
2514
+ this.pendingPresets.set(theme.id, load);
2515
+ return load;
2516
+ }
2388
2517
  getPrimaryColorOptions(activeThemePreset) {
2389
2518
  if (activeThemePreset.primaryColors) {
2390
2519
  return Object.entries(activeThemePreset.primaryColors)
2391
2520
  .filter((entry) => Boolean(entry[1]))
2392
2521
  .map(([name, palette]) => ({ name, palette }));
2393
2522
  }
2394
- const presetPalette = activeThemePreset.preset.primitive ?? {};
2523
+ const presetPalette = this.getResolvedPreset(activeThemePreset)
2524
+ ?.primitive ?? {};
2395
2525
  const palettes = [{ name: 'noir', palette: {} }];
2396
2526
  PRIMARY_COLORS.forEach((color) => {
2397
2527
  palettes.push({
@@ -2442,9 +2572,10 @@ class AppConfiguratorComponent {
2442
2572
  updateSurfacePalette(color.palette);
2443
2573
  }
2444
2574
  }
2445
- onPresetChange(event) {
2575
+ async onPresetChange(event) {
2446
2576
  const nextThemeId = this.ensureValidThemeId(event);
2447
2577
  const nextTheme = this.getThemeById(nextThemeId);
2578
+ const preset = await this.loadPreset(nextTheme);
2448
2579
  const primaryColors = this.getPrimaryColorOptions(nextTheme);
2449
2580
  const surfaceColors = this.getSurfaceColorOptions(nextTheme);
2450
2581
  this.layoutService.layoutConfig.update((state) => ({
@@ -2457,7 +2588,6 @@ class AppConfiguratorComponent {
2457
2588
  ? (surfaceColors[0]?.name ?? state.surface)
2458
2589
  : state.surface
2459
2590
  }));
2460
- const preset = nextTheme.preset;
2461
2591
  const surfacePalette = this.surfaceColors().find((s) => s.name === this.selectedSurfaceColor())?.palette;
2462
2592
  $t().preset(preset).preset(this.getPresetExt()).surfacePalette(surfacePalette).use({ useDefaultOptions: true });
2463
2593
  }
@@ -3028,7 +3158,7 @@ var userNotifications$1 = {
3028
3158
  markAsRead: "Read",
3029
3159
  markedAsRead: "Notification marked as read"
3030
3160
  };
3031
- var en$d = {
3161
+ var en$f = {
3032
3162
  userNotifications: userNotifications$1
3033
3163
  };
3034
3164
 
@@ -3039,13 +3169,13 @@ var userNotifications = {
3039
3169
  markAsRead: "Прочитано",
3040
3170
  markedAsRead: "Оповещение отмечено прочитанным"
3041
3171
  };
3042
- var ru$d = {
3172
+ var ru$f = {
3043
3173
  userNotifications: userNotifications
3044
3174
  };
3045
3175
 
3046
3176
  class UserNotificationsComponent {
3047
3177
  constructor() {
3048
- this.translations = provideTranslations({ en: en$d, ru: ru$d });
3178
+ this.translations = provideTranslations({ en: en$f, ru: ru$f });
3049
3179
  this.notifications = [];
3050
3180
  this.totalCount = 0;
3051
3181
  this.skip = 0;
@@ -3474,7 +3604,7 @@ var topbar$1 = {
3474
3604
  profile: "Profile",
3475
3605
  applications: "Applications"
3476
3606
  };
3477
- var en$c = {
3607
+ var en$e = {
3478
3608
  topbar: topbar$1
3479
3609
  };
3480
3610
 
@@ -3487,13 +3617,13 @@ var topbar = {
3487
3617
  profile: "Профиль",
3488
3618
  applications: "Приложения"
3489
3619
  };
3490
- var ru$c = {
3620
+ var ru$e = {
3491
3621
  topbar: topbar
3492
3622
  };
3493
3623
 
3494
3624
  class AppTopbar {
3495
3625
  constructor() {
3496
- this.translations = provideTranslations({ en: en$c, ru: ru$c });
3626
+ this.translations = provideTranslations({ en: en$e, ru: ru$e });
3497
3627
  this.securityService = inject(SecurityService);
3498
3628
  this.topBarService = inject(TopBarService);
3499
3629
  this.userService = inject(UserService);
@@ -3822,6 +3952,18 @@ class MenuApi extends HttpClient {
3822
3952
  format: "json",
3823
3953
  ...params,
3824
3954
  });
3955
+ this.setStartModule = ({ id, ...query }, params = {}) => this.request({
3956
+ path: `/api/menu/set-start-module/${id}`,
3957
+ method: "POST",
3958
+ secure: true,
3959
+ ...params,
3960
+ });
3961
+ this.deleteStartModule = (params = {}) => this.request({
3962
+ path: `/api/menu/delete-start-module`,
3963
+ method: "DELETE",
3964
+ secure: true,
3965
+ ...params,
3966
+ });
3825
3967
  this.getModuleInstanceRights = (query, params = {}) => this.request({
3826
3968
  path: `/api/menu/get-module-instance-rights`,
3827
3969
  method: "GET",
@@ -3941,6 +4083,106 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
3941
4083
  type: Injectable
3942
4084
  }] });
3943
4085
 
4086
+ /**
4087
+ * Resolves the module instance the current user lands on when no explicit route is requested.
4088
+ *
4089
+ * The menu returned by the backend is already filtered by the rights of the current user, so the
4090
+ * first navigable leaf of that menu is always a module the user may open. When the user picked a
4091
+ * start module explicitly it comes back marked with `isStart`, and a module that was deleted or
4092
+ * whose rights were revoked simply disappears from the menu, which falls back to the first leaf.
4093
+ *
4094
+ * The resolved url is cached per user and dropped whenever the authenticated user changes or the
4095
+ * start module is reassigned.
4096
+ */
4097
+ class StartPageService {
4098
+ constructor() {
4099
+ this.menuApi = inject(MenuApi);
4100
+ this.router = inject(Router);
4101
+ this.securityService = inject(SecurityService);
4102
+ this.request = undefined;
4103
+ this.cachedIdentity = undefined;
4104
+ this.securityService.payload.subscribe((payload) => {
4105
+ // The menu depends on both the user and the roles of the current session.
4106
+ const roles = payload?.realm_access?.roles ?? [];
4107
+ const identity = `${payload?.preferred_username ?? ''}|${[...roles].sort().join(',')}`;
4108
+ if (identity !== this.cachedIdentity) {
4109
+ this.cachedIdentity = identity;
4110
+ this.clearCache();
4111
+ }
4112
+ });
4113
+ }
4114
+ /**
4115
+ * Gets the url of the start module instance.
4116
+ *
4117
+ * @returns The url tree to navigate to, or `null` when the user has no module available.
4118
+ */
4119
+ resolveStartUrl() {
4120
+ if (this.request) {
4121
+ return this.request;
4122
+ }
4123
+ this.request = this.loadStartUrl().catch((error) => {
4124
+ this.request = undefined;
4125
+ throw error;
4126
+ });
4127
+ return this.request;
4128
+ }
4129
+ /**
4130
+ * Makes the module instance the start page of the current user.
4131
+ *
4132
+ * @param moduleInstanceId Module instance to open by default.
4133
+ */
4134
+ async setStartModule(moduleInstanceId) {
4135
+ await this.menuApi.setStartModule({ id: moduleInstanceId });
4136
+ this.clearCache();
4137
+ }
4138
+ /**
4139
+ * Clears the start page of the current user, falling back to the first available module.
4140
+ */
4141
+ async clearStartModule() {
4142
+ await this.menuApi.deleteStartModule();
4143
+ this.clearCache();
4144
+ }
4145
+ /**
4146
+ * Drops the cached url, for example after the menu was changed.
4147
+ */
4148
+ clearCache() {
4149
+ this.request = undefined;
4150
+ }
4151
+ async loadStartUrl() {
4152
+ const menu = (await this.menuApi.get()) ?? [];
4153
+ const item = this.findStartItem(menu);
4154
+ return item ? this.router.createUrlTree(item.routerLink) : null;
4155
+ }
4156
+ findStartItem(menu) {
4157
+ const leaves = this.collectNavigableLeaves(menu);
4158
+ return leaves.find((item) => item.isStart) ?? leaves[0] ?? null;
4159
+ }
4160
+ collectNavigableLeaves(items) {
4161
+ const result = [];
4162
+ for (const item of [...items].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))) {
4163
+ if (item.separator) {
4164
+ continue;
4165
+ }
4166
+ if (item.items?.length) {
4167
+ // A folder is never a landing page itself, its children are.
4168
+ result.push(...this.collectNavigableLeaves(item.items));
4169
+ continue;
4170
+ }
4171
+ // A module without a real route would resolve back to the empty path and loop.
4172
+ if (item.routerLink?.some((segment) => !!segment && segment !== '/')) {
4173
+ result.push(item);
4174
+ }
4175
+ }
4176
+ return result;
4177
+ }
4178
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: StartPageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4179
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: StartPageService, providedIn: 'root' }); }
4180
+ }
4181
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: StartPageService, decorators: [{
4182
+ type: Injectable,
4183
+ args: [{ providedIn: 'root' }]
4184
+ }], ctorParameters: () => [] });
4185
+
3944
4186
  class MenuItemComponent {
3945
4187
  constructor(cd, router, menuService) {
3946
4188
  this.cd = cd;
@@ -3952,6 +4194,7 @@ class MenuItemComponent {
3952
4194
  this.msgService = inject(MsgService);
3953
4195
  this.menuDataService = inject(MenuApi);
3954
4196
  this.securityService = inject(SecurityService);
4197
+ this.startPageService = inject(StartPageService);
3955
4198
  this.active = false;
3956
4199
  this.subscriptions = [];
3957
4200
  this.localization = {};
@@ -4025,13 +4268,25 @@ class MenuItemComponent {
4025
4268
  this.menuItemCreateDialogComponent.showDialog();
4026
4269
  }
4027
4270
  onContextMenu($event, item) {
4271
+ const startModuleItems = this.getStartModuleItems(item);
4272
+ // Choosing a start page is available to every user, the rest of the menu is administrative.
4028
4273
  if (!this.securityService.isAdmin()) {
4274
+ if (startModuleItems.length === 0) {
4275
+ return;
4276
+ }
4277
+ $event.stopPropagation();
4278
+ $event.preventDefault();
4279
+ this.menuService.contextMenuItem = item;
4280
+ this.contextMenu.model = startModuleItems;
4281
+ this.contextMenu.show($event);
4029
4282
  return;
4030
4283
  }
4031
4284
  $event.stopPropagation();
4032
4285
  $event.preventDefault();
4033
4286
  this.menuService.contextMenuItem = item;
4034
4287
  this.contextMenu.model = [
4288
+ ...startModuleItems,
4289
+ { separator: true, visible: startModuleItems.length > 0 },
4035
4290
  {
4036
4291
  label: this.localization.new,
4037
4292
  icon: PrimeIcons.PLUS,
@@ -4072,6 +4327,39 @@ class MenuItemComponent {
4072
4327
  ];
4073
4328
  this.contextMenu.show($event);
4074
4329
  }
4330
+ /**
4331
+ * Builds the start page entries of the context menu. Only a navigable leaf can be a start page.
4332
+ */
4333
+ getStartModuleItems(item) {
4334
+ if (!item?.routerLink || item.items?.length) {
4335
+ return [];
4336
+ }
4337
+ return item.isStart
4338
+ ? [
4339
+ {
4340
+ label: this.localization.unsetStartModule,
4341
+ icon: PrimeIcons.STAR,
4342
+ command: () => this.clearStartModule()
4343
+ }
4344
+ ]
4345
+ : [
4346
+ {
4347
+ label: this.localization.setStartModule,
4348
+ icon: PrimeIcons.STAR_FILL,
4349
+ command: () => this.setStartModule(item)
4350
+ }
4351
+ ];
4352
+ }
4353
+ async setStartModule(item) {
4354
+ await this.startPageService.setStartModule(item.moduleInstanceId);
4355
+ this.msgService.success(this.localization.setStartModuleSuccessMessage);
4356
+ await this.menuService.loadMenu();
4357
+ }
4358
+ async clearStartModule() {
4359
+ await this.startPageService.clearStartModule();
4360
+ this.msgService.success(this.localization.unsetStartModuleSuccessMessage);
4361
+ await this.menuService.loadMenu();
4362
+ }
4075
4363
  deleteItem(event) {
4076
4364
  this.confirmationService.confirm({
4077
4365
  header: this.localization.deleteItemConfirmHeader,
@@ -4431,9 +4719,9 @@ class MenuItemCreateDialogComponent {
4431
4719
  <input
4432
4720
  autocomplete="off"
4433
4721
  class="flex-auto"
4722
+ disabled
4434
4723
  id="oip-menu-item-create-dialog-parent-input"
4435
4724
  pInputText
4436
- readonly
4437
4725
  [ngModel]="menuService.contextMenuItem?.label" />
4438
4726
  </div>
4439
4727
  }
@@ -4528,9 +4816,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
4528
4816
  <input
4529
4817
  autocomplete="off"
4530
4818
  class="flex-auto"
4819
+ disabled
4531
4820
  id="oip-menu-item-create-dialog-parent-input"
4532
4821
  pInputText
4533
- readonly
4534
4822
  [ngModel]="menuService.contextMenuItem?.label" />
4535
4823
  </div>
4536
4824
  }
@@ -4617,6 +4905,7 @@ class MenuItemEditDialogComponent {
4617
4905
  this.msgService = inject(MsgService);
4618
4906
  this.visibleChange = new EventEmitter();
4619
4907
  this.roles = [];
4908
+ this.moduleName = '';
4620
4909
  this.iconOptions = Object.values(PrimeIcons)
4621
4910
  .filter((icon) => typeof icon === 'string')
4622
4911
  .map((icon) => ({
@@ -4668,7 +4957,9 @@ class MenuItemEditDialogComponent {
4668
4957
  icon: this.menuService.contextMenuItem?.icon,
4669
4958
  viewRoles: this.menuService.contextMenuItem?.securities
4670
4959
  };
4671
- this.roles = await this.securityApi.getRealmRoles();
4960
+ const [roles, modules] = await Promise.all([this.securityApi.getRealmRoles(), this.menuService.getModules()]);
4961
+ this.roles = roles;
4962
+ this.moduleName = modules.find((module) => module.key === this.item.moduleId)?.value ?? '';
4672
4963
  this.visible = true;
4673
4964
  this.visibleChange.emit(this.visible);
4674
4965
  }
@@ -4692,6 +4983,19 @@ class MenuItemEditDialogComponent {
4692
4983
  [(ngModel)]="item.label" />
4693
4984
  </div>
4694
4985
 
4986
+ <div class="flex items-center gap-4 mb-4">
4987
+ <label class="font-semibold w-1/3" for="oip-menu-item-edit-dialog-module">
4988
+ {{ 'menuItemEditDialogComponent.module' | translate }}
4989
+ </label>
4990
+ <input
4991
+ autocomplete="off"
4992
+ class="flex-auto"
4993
+ disabled
4994
+ id="oip-menu-item-edit-dialog-module"
4995
+ pInputText
4996
+ [ngModel]="moduleName" />
4997
+ </div>
4998
+
4695
4999
  <div class="flex items-center gap-4 mb-4">
4696
5000
  <label class="font-semibold w-1/3" for="oip-menu-item-edit-dialog-icon">
4697
5001
  {{ 'menuItemEditDialogComponent.icon' | translate }}
@@ -4778,6 +5082,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
4778
5082
  [(ngModel)]="item.label" />
4779
5083
  </div>
4780
5084
 
5085
+ <div class="flex items-center gap-4 mb-4">
5086
+ <label class="font-semibold w-1/3" for="oip-menu-item-edit-dialog-module">
5087
+ {{ 'menuItemEditDialogComponent.module' | translate }}
5088
+ </label>
5089
+ <input
5090
+ autocomplete="off"
5091
+ class="flex-auto"
5092
+ disabled
5093
+ id="oip-menu-item-edit-dialog-module"
5094
+ pInputText
5095
+ [ngModel]="moduleName" />
5096
+ </div>
5097
+
4781
5098
  <div class="flex items-center gap-4 mb-4">
4782
5099
  <label class="font-semibold w-1/3" for="oip-menu-item-edit-dialog-icon">
4783
5100
  {{ 'menuItemEditDialogComponent.icon' | translate }}
@@ -4859,7 +5176,11 @@ var menuItemComponent$1 = {
4859
5176
  deleteItemConfirmRejectButtonPropsLabel: "Cancel",
4860
5177
  deleteItemConfirmAcceptButtonPropsLabel: "Delete",
4861
5178
  moveUp: "Move up",
4862
- moveDown: "Move down"
5179
+ moveDown: "Move down",
5180
+ setStartModule: "Set as start page",
5181
+ unsetStartModule: "Clear start page",
5182
+ setStartModuleSuccessMessage: "Start page updated",
5183
+ unsetStartModuleSuccessMessage: "Start page cleared"
4863
5184
  };
4864
5185
  var menuItemEditDialogComponent$1 = {
4865
5186
  header: "Edit menu item",
@@ -4883,7 +5204,7 @@ var menuItemCreateDialogComponent$1 = {
4883
5204
  cancel: "Cancel",
4884
5205
  save: "Save"
4885
5206
  };
4886
- var en$b = {
5207
+ var en$d = {
4887
5208
  menuComponent: menuComponent$1,
4888
5209
  menuItemComponent: menuItemComponent$1,
4889
5210
  menuItemEditDialogComponent: menuItemEditDialogComponent$1,
@@ -4904,7 +5225,11 @@ var menuItemComponent = {
4904
5225
  deleteItemConfirmRejectButtonPropsLabel: "Отмена",
4905
5226
  deleteItemConfirmAcceptButtonPropsLabel: "Удалить",
4906
5227
  moveUp: "Вверх",
4907
- moveDown: "Вниз"
5228
+ moveDown: "Вниз",
5229
+ setStartModule: "Сделать стартовой",
5230
+ unsetStartModule: "Убрать стартовую",
5231
+ setStartModuleSuccessMessage: "Стартовая страница обновлена",
5232
+ unsetStartModuleSuccessMessage: "Стартовая страница сброшена"
4908
5233
  };
4909
5234
  var menuItemEditDialogComponent = {
4910
5235
  header: "Редактировать элемент меню",
@@ -4928,7 +5253,7 @@ var menuItemCreateDialogComponent = {
4928
5253
  cancel: "Отмена",
4929
5254
  save: "Сохранить"
4930
5255
  };
4931
- var ru$b = {
5256
+ var ru$d = {
4932
5257
  menuComponent: menuComponent,
4933
5258
  menuItemComponent: menuItemComponent,
4934
5259
  menuItemEditDialogComponent: menuItemEditDialogComponent,
@@ -4939,7 +5264,7 @@ class MenuComponent {
4939
5264
  constructor() {
4940
5265
  // Registers the whole menu/l10n/menu.*.json bundle, which also covers
4941
5266
  // menuItemComponent, menuItemEditDialogComponent and menuItemCreateDialogComponent.
4942
- this.translations = provideTranslations({ en: en$b, ru: ru$b });
5267
+ this.translations = provideTranslations({ en: en$d, ru: ru$d });
4943
5268
  this.menuService = inject(MenuService);
4944
5269
  this.securityService = inject(SecurityService);
4945
5270
  this.translateService = inject(TranslateService);
@@ -4954,6 +5279,8 @@ class MenuComponent {
4954
5279
  if (!this.securityService.isAdmin()) {
4955
5280
  return;
4956
5281
  }
5282
+ $event.preventDefault();
5283
+ $event.stopPropagation();
4957
5284
  this.menuService.contextMenuItem = null;
4958
5285
  this.contextMenu.model = [
4959
5286
  {
@@ -4962,6 +5289,7 @@ class MenuComponent {
4962
5289
  command: (event) => this.newClick(event)
4963
5290
  }
4964
5291
  ];
5292
+ this.contextMenu.show($event);
4965
5293
  }
4966
5294
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
4967
5295
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: MenuComponent, isStandalone: true, selector: "app-menu", providers: [MenuApi], viewQueries: [{ propertyName: "menuItemCreateDialogComponent", first: true, predicate: MenuItemCreateDialogComponent, descendants: true }, { propertyName: "menuItemEditDialogComponent", first: true, predicate: MenuItemEditDialogComponent, descendants: true }, { propertyName: "contextMenu", first: true, predicate: ContextMenu, descendants: true }], ngImport: i0, template: ` <div #empty class="layout-sidebar" (contextmenu)="onContextMenu($event)">
@@ -4984,8 +5312,9 @@ class MenuComponent {
4984
5312
  }
4985
5313
  </ul>
4986
5314
  </div>
5315
+ <!-- Always rendered: choosing a start page is available to every user, not only to admins. -->
5316
+ <p-contextMenu />
4987
5317
  @if (securityService.isAdmin()) {
4988
- <p-contextMenu [target]="empty" />
4989
5318
  <menu-item-create-dialog />
4990
5319
  <menu-item-edit-dialog />
4991
5320
  }`, isInline: true, dependencies: [{ kind: "component", type: MenuItemComponent, selector: "[app-menuitem]", inputs: ["item", "index", "root", "parentKey", "menuItemCreateDialogComponent", "menuItemEditDialogComponent", "contextMenu"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "ngmodule", type: ContextMenuModule }, { kind: "component", type: i1$6.ContextMenu, selector: "p-contextMenu, p-contextmenu, p-context-menu", inputs: ["model", "triggerEvent", "target", "global", "style", "styleClass", "autoZIndex", "baseZIndex", "id", "breakpoint", "ariaLabel", "ariaLabelledBy", "pressDelay", "appendTo"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: DialogModule }, { kind: "ngmodule", type: InputTextModule }, { kind: "component", type: MenuItemCreateDialogComponent, selector: "menu-item-create-dialog", inputs: ["visible"], outputs: ["visibleChange"] }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: MenuItemEditDialogComponent, selector: "menu-item-edit-dialog", inputs: ["visible"], outputs: ["visibleChange"] }] }); }
@@ -5026,8 +5355,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
5026
5355
  }
5027
5356
  </ul>
5028
5357
  </div>
5358
+ <!-- Always rendered: choosing a start page is available to every user, not only to admins. -->
5359
+ <p-contextMenu />
5029
5360
  @if (securityService.isAdmin()) {
5030
- <p-contextMenu [target]="empty" />
5031
5361
  <menu-item-create-dialog />
5032
5362
  <menu-item-edit-dialog />
5033
5363
  }`
@@ -5058,6 +5388,147 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
5058
5388
  }]
5059
5389
  }], ctorParameters: () => [] });
5060
5390
 
5391
+ var blockLoader$1 = {
5392
+ loading: "Loading..."
5393
+ };
5394
+ var en$c = {
5395
+ blockLoader: blockLoader$1
5396
+ };
5397
+
5398
+ var blockLoader = {
5399
+ loading: "Загрузка..."
5400
+ };
5401
+ var ru$c = {
5402
+ blockLoader: blockLoader
5403
+ };
5404
+
5405
+ /**
5406
+ * Full screen blocker shown while the application switches between modules.
5407
+ *
5408
+ * Render it as a sibling of `.layout-wrapper`, not inside it: while the blocker is visible the
5409
+ * wrapper is marked `inert`, which would also disable an overlay nested in it.
5410
+ *
5411
+ * The blocker appears only when a transition outlasts {@link showDelayMs} and then stays for at
5412
+ * least {@link minVisibleMs}, so quick navigation does not flash a spinner.
5413
+ */
5414
+ class BlockLoaderComponent {
5415
+ static { this.showDelayMs = 150; }
5416
+ static { this.minVisibleMs = 300; }
5417
+ constructor() {
5418
+ this.translations = provideTranslations({ en: en$c, ru: ru$c });
5419
+ this.moduleLoadingService = inject(ModuleLoadingService);
5420
+ this.shownAt = 0;
5421
+ this.visible = signal(false, ...(ngDevMode ? [{ debugName: "visible" }] : []));
5422
+ effect(() => {
5423
+ const loading = this.moduleLoadingService.loading();
5424
+ untracked(() => (loading ? this.scheduleShow() : this.scheduleHide()));
5425
+ });
5426
+ effect(() => {
5427
+ this.blockInteraction(this.visible());
5428
+ });
5429
+ }
5430
+ ngOnDestroy() {
5431
+ this.clearTimers();
5432
+ this.blockInteraction(false);
5433
+ }
5434
+ scheduleShow() {
5435
+ this.clearTimer('hide');
5436
+ if (this.visible() || this.showHandle != null) {
5437
+ return;
5438
+ }
5439
+ this.showHandle = setTimeout(() => {
5440
+ this.showHandle = undefined;
5441
+ this.shownAt = Date.now();
5442
+ this.visible.set(true);
5443
+ }, BlockLoaderComponent.showDelayMs);
5444
+ }
5445
+ scheduleHide() {
5446
+ this.clearTimer('show');
5447
+ if (!this.visible() || this.hideHandle != null) {
5448
+ return;
5449
+ }
5450
+ const remaining = BlockLoaderComponent.minVisibleMs - (Date.now() - this.shownAt);
5451
+ if (remaining <= 0) {
5452
+ this.visible.set(false);
5453
+ return;
5454
+ }
5455
+ this.hideHandle = setTimeout(() => {
5456
+ this.hideHandle = undefined;
5457
+ this.visible.set(false);
5458
+ }, remaining);
5459
+ }
5460
+ clearTimer(timer) {
5461
+ const handle = timer === 'show' ? this.showHandle : this.hideHandle;
5462
+ if (handle == null) {
5463
+ return;
5464
+ }
5465
+ clearTimeout(handle);
5466
+ if (timer === 'show') {
5467
+ this.showHandle = undefined;
5468
+ }
5469
+ else {
5470
+ this.hideHandle = undefined;
5471
+ }
5472
+ }
5473
+ clearTimers() {
5474
+ this.clearTimer('show');
5475
+ this.clearTimer('hide');
5476
+ }
5477
+ /**
5478
+ * Takes the application out of the interaction and accessibility trees while the blocker is up,
5479
+ * so pointer, keyboard and screen reader input cannot reach a module that is still loading.
5480
+ */
5481
+ blockInteraction(blocked) {
5482
+ const wrapper = document.querySelector('.layout-wrapper');
5483
+ if (blocked) {
5484
+ wrapper?.setAttribute('inert', '');
5485
+ // Not the layout's `blocked-scroll` class: the layout drops it whenever it closes the menu.
5486
+ document.body.style.setProperty('overflow', 'hidden');
5487
+ }
5488
+ else {
5489
+ wrapper?.removeAttribute('inert');
5490
+ document.body.style.removeProperty('overflow');
5491
+ }
5492
+ }
5493
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: BlockLoaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5494
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: BlockLoaderComponent, isStandalone: true, selector: "block-loader", ngImport: i0, template: `
5495
+ @if (visible()) {
5496
+ <div
5497
+ class="animate-fadein fixed inset-0 z-[1200] flex flex-col items-center justify-center gap-4 bg-surface-0/70 dark:bg-surface-900/70"
5498
+ role="status"
5499
+ aria-live="polite"
5500
+ aria-busy="true"
5501
+ (pointerdown)="$event.preventDefault()"
5502
+ (contextmenu)="$event.preventDefault()">
5503
+ <p-progress-spinner [style]="{ width: '3rem', height: '3rem' }" animationDuration=".8s" strokeWidth="4" />
5504
+ <span class="text-color font-medium">{{ 'blockLoader.loading' | translate }}</span>
5505
+ </div>
5506
+ }
5507
+ `, isInline: true, dependencies: [{ kind: "component", type: ProgressSpinner, selector: "p-progressSpinner, p-progress-spinner, p-progressspinner", inputs: ["styleClass", "strokeWidth", "fill", "animationDuration", "ariaLabel"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }] }); }
5508
+ }
5509
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: BlockLoaderComponent, decorators: [{
5510
+ type: Component,
5511
+ args: [{
5512
+ selector: 'block-loader',
5513
+ standalone: true,
5514
+ imports: [ProgressSpinner, TranslatePipe],
5515
+ template: `
5516
+ @if (visible()) {
5517
+ <div
5518
+ class="animate-fadein fixed inset-0 z-[1200] flex flex-col items-center justify-center gap-4 bg-surface-0/70 dark:bg-surface-900/70"
5519
+ role="status"
5520
+ aria-live="polite"
5521
+ aria-busy="true"
5522
+ (pointerdown)="$event.preventDefault()"
5523
+ (contextmenu)="$event.preventDefault()">
5524
+ <p-progress-spinner [style]="{ width: '3rem', height: '3rem' }" animationDuration=".8s" strokeWidth="4" />
5525
+ <span class="text-color font-medium">{{ 'blockLoader.loading' | translate }}</span>
5526
+ </div>
5527
+ }
5528
+ `
5529
+ }]
5530
+ }], ctorParameters: () => [] });
5531
+
5061
5532
  class AppLayoutComponent {
5062
5533
  constructor() {
5063
5534
  this.layoutService = inject(LayoutService);
@@ -5148,14 +5619,16 @@ class AppLayoutComponent {
5148
5619
  </div>
5149
5620
  <div class="layout-mask animate-fadein"></div>
5150
5621
  </div>
5151
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: AppTopbar, selector: "app-topbar" }, { kind: "component", type: SidebarComponent, selector: "app-sidebar" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$5.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: FooterComponent, selector: "app-footer" }] }); }
5622
+ <!-- Outside the wrapper on purpose: the blocker marks the wrapper inert while it is visible. -->
5623
+ <block-loader />
5624
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: AppTopbar, selector: "app-topbar" }, { kind: "component", type: SidebarComponent, selector: "app-sidebar" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i1$5.RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: FooterComponent, selector: "app-footer" }, { kind: "component", type: BlockLoaderComponent, selector: "block-loader" }] }); }
5152
5625
  }
5153
5626
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AppLayoutComponent, decorators: [{
5154
5627
  type: Component,
5155
5628
  args: [{
5156
5629
  selector: 'app-layout',
5157
5630
  standalone: true,
5158
- imports: [CommonModule, AppTopbar, SidebarComponent, RouterModule, FooterComponent],
5631
+ imports: [CommonModule, AppTopbar, SidebarComponent, RouterModule, FooterComponent, BlockLoaderComponent],
5159
5632
  template: `
5160
5633
  <div class="layout-wrapper" [ngClass]="containerClass">
5161
5634
  <app-topbar></app-topbar>
@@ -5168,6 +5641,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
5168
5641
  </div>
5169
5642
  <div class="layout-mask animate-fadein"></div>
5170
5643
  </div>
5644
+ <!-- Outside the wrapper on purpose: the blocker marks the wrapper inert while it is visible. -->
5645
+ <block-loader />
5171
5646
  `,
5172
5647
  providers: [MenuService, MenuApi]
5173
5648
  }]
@@ -5256,7 +5731,7 @@ var notfound$1 = {
5256
5731
  description: "Requested resource is not available.",
5257
5732
  button: "Go to home"
5258
5733
  };
5259
- var en$a = {
5734
+ var en$b = {
5260
5735
  notfound: notfound$1
5261
5736
  };
5262
5737
 
@@ -5266,13 +5741,13 @@ var notfound = {
5266
5741
  description: "Запрашиваемый ресурс недоступен.",
5267
5742
  button: "На главную"
5268
5743
  };
5269
- var ru$a = {
5744
+ var ru$b = {
5270
5745
  notfound: notfound
5271
5746
  };
5272
5747
 
5273
5748
  class NotfoundComponent {
5274
5749
  constructor() {
5275
- this.translations = provideTranslations({ en: en$a, ru: ru$a });
5750
+ this.translations = provideTranslations({ en: en$b, ru: ru$b });
5276
5751
  }
5277
5752
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NotfoundComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5278
5753
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: NotfoundComponent, isStandalone: true, selector: "app-notfound", ngImport: i0, template: ` <app-floating-configurator />
@@ -5332,7 +5807,7 @@ var unauthorized$1 = {
5332
5807
  signInToContinue: "Sign in to continue",
5333
5808
  signIn: "Sign In"
5334
5809
  };
5335
- var en$9 = {
5810
+ var en$a = {
5336
5811
  unauthorized: unauthorized$1
5337
5812
  };
5338
5813
 
@@ -5341,13 +5816,13 @@ var unauthorized = {
5341
5816
  signInToContinue: "Войдите чтобы продолжить",
5342
5817
  signIn: "Войти"
5343
5818
  };
5344
- var ru$9 = {
5819
+ var ru$a = {
5345
5820
  unauthorized: unauthorized
5346
5821
  };
5347
5822
 
5348
5823
  class UnauthorizedComponent {
5349
5824
  constructor() {
5350
- this.translations = provideTranslations({ en: en$9, ru: ru$9 });
5825
+ this.translations = provideTranslations({ en: en$a, ru: ru$a });
5351
5826
  this.securityService = inject(SecurityService);
5352
5827
  this.route = inject(ActivatedRoute);
5353
5828
  }
@@ -5437,12 +5912,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
5437
5912
  }]
5438
5913
  }] });
5439
5914
 
5915
+ var unauthorized_component = /*#__PURE__*/Object.freeze({
5916
+ __proto__: null,
5917
+ UnauthorizedComponent: UnauthorizedComponent
5918
+ });
5919
+
5440
5920
  var access$1 = {
5441
5921
  title: "Access Denied",
5442
5922
  message: "You do not have the necessary permissions. Please contact admins.",
5443
5923
  button: "Go to Dashboard"
5444
5924
  };
5445
- var en$8 = {
5925
+ var en$9 = {
5446
5926
  access: access$1
5447
5927
  };
5448
5928
 
@@ -5451,13 +5931,13 @@ var access = {
5451
5931
  message: "У вас недостаточно прав. Пожалуйста, обратитесь к администраторам.",
5452
5932
  button: "На главную"
5453
5933
  };
5454
- var ru$8 = {
5934
+ var ru$9 = {
5455
5935
  access: access
5456
5936
  };
5457
5937
 
5458
5938
  class AccessComponent {
5459
5939
  constructor() {
5460
- this.translations = provideTranslations({ en: en$8, ru: ru$8 });
5940
+ this.translations = provideTranslations({ en: en$9, ru: ru$9 });
5461
5941
  }
5462
5942
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: AccessComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5463
5943
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: AccessComponent, isStandalone: true, selector: "app-access", ngImport: i0, template: `<div
@@ -5537,7 +6017,7 @@ var error$1 = {
5537
6017
  message: "Requested resource is not available.",
5538
6018
  button: "Go to Dashboard"
5539
6019
  };
5540
- var en$7 = {
6020
+ var en$8 = {
5541
6021
  error: error$1
5542
6022
  };
5543
6023
 
@@ -5546,13 +6026,13 @@ var error = {
5546
6026
  message: "Запрашиваемый ресурс недоступен.",
5547
6027
  button: "На главную"
5548
6028
  };
5549
- var ru$7 = {
6029
+ var ru$8 = {
5550
6030
  error: error
5551
6031
  };
5552
6032
 
5553
6033
  class ErrorComponent {
5554
6034
  constructor() {
5555
- this.translations = provideTranslations({ en: en$7, ru: ru$7 });
6035
+ this.translations = provideTranslations({ en: en$8, ru: ru$8 });
5556
6036
  }
5557
6037
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: ErrorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5558
6038
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: ErrorComponent, isStandalone: true, selector: "app-error", ngImport: i0, template: `<div
@@ -5627,6 +6107,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
5627
6107
  }]
5628
6108
  }] });
5629
6109
 
6110
+ var error_component = /*#__PURE__*/Object.freeze({
6111
+ __proto__: null,
6112
+ ErrorComponent: ErrorComponent
6113
+ });
6114
+
5630
6115
  var profileComponent$1 = {
5631
6116
  changePhoto: "Upload Photo",
5632
6117
  successfullyUploaded: "Uploaded successfully",
@@ -5639,7 +6124,7 @@ var profileComponent$1 = {
5639
6124
  photoDeleted: "Photo deleted",
5640
6125
  failedToDeletePhoto: "Failed to delete user photo"
5641
6126
  };
5642
- var en$6 = {
6127
+ var en$7 = {
5643
6128
  profileComponent: profileComponent$1
5644
6129
  };
5645
6130
 
@@ -5655,13 +6140,13 @@ var profileComponent = {
5655
6140
  photoDeleted: "Фото удалено",
5656
6141
  failedToDeletePhoto: "Не удалось удалить фото пользователя"
5657
6142
  };
5658
- var ru$6 = {
6143
+ var ru$7 = {
5659
6144
  profileComponent: profileComponent
5660
6145
  };
5661
6146
 
5662
6147
  class UserProfileComponent {
5663
6148
  constructor() {
5664
- this.translations = provideTranslations({ en: en$6, ru: ru$6 });
6149
+ this.translations = provideTranslations({ en: en$7, ru: ru$7 });
5665
6150
  this.userService = inject(UserService);
5666
6151
  this.msgService = inject(MsgService);
5667
6152
  this.translateService = inject(TranslateService);
@@ -5786,6 +6271,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
5786
6271
  }]
5787
6272
  }] });
5788
6273
 
6274
+ var userProfile_component = /*#__PURE__*/Object.freeze({
6275
+ __proto__: null,
6276
+ UserProfileComponent: UserProfileComponent
6277
+ });
6278
+
5789
6279
  var config$1 = {
5790
6280
  all: "All",
5791
6281
  applicationManagement: "Application management",
@@ -5802,7 +6292,7 @@ var config$1 = {
5802
6292
  timeZone: "Time zone",
5803
6293
  usePhoto256x256Pixel: "Use photo 256x256 pixel"
5804
6294
  };
5805
- var en$5 = {
6295
+ var en$6 = {
5806
6296
  config: config$1
5807
6297
  };
5808
6298
 
@@ -5822,13 +6312,13 @@ var config = {
5822
6312
  timeZone: "Часовой пояс",
5823
6313
  usePhoto256x256Pixel: "Используйте фото 256x256 пикселей"
5824
6314
  };
5825
- var ru$5 = {
6315
+ var ru$6 = {
5826
6316
  config: config
5827
6317
  };
5828
6318
 
5829
6319
  class ConfigComponent {
5830
6320
  constructor() {
5831
- this.translations = provideTranslations({ en: en$5, ru: ru$5 });
6321
+ this.translations = provideTranslations({ en: en$6, ru: ru$6 });
5832
6322
  this.layoutService = inject(LayoutService);
5833
6323
  this.l10nService = inject(L10nService);
5834
6324
  this.userService = inject(UserService);
@@ -6092,7 +6582,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
6092
6582
  }]
6093
6583
  }], ctorParameters: () => [] });
6094
6584
 
6095
- var en$4 = {
6585
+ var config_component = /*#__PURE__*/Object.freeze({
6586
+ __proto__: null,
6587
+ ConfigComponent: ConfigComponent
6588
+ });
6589
+
6590
+ var en$5 = {
6096
6591
  "db-migration": {
6097
6592
  migrationManager: "Migration manager",
6098
6593
  actions: {
@@ -6112,7 +6607,7 @@ var en$4 = {
6112
6607
  }
6113
6608
  };
6114
6609
 
6115
- var ru$4 = {
6610
+ var ru$5 = {
6116
6611
  "db-migration": {
6117
6612
  migrationManager: "Менеджер миграций",
6118
6613
  actions: {
@@ -6135,7 +6630,7 @@ var ru$4 = {
6135
6630
  class DbMigrationComponent extends BaseModuleComponent {
6136
6631
  constructor() {
6137
6632
  super(...arguments);
6138
- this.translations = provideTranslations({ en: en$4, ru: ru$4 });
6633
+ this.translations = provideTranslations({ en: en$5, ru: ru$5 });
6139
6634
  }
6140
6635
  async ngOnInit() {
6141
6636
  await super.ngOnInit();
@@ -6339,6 +6834,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
6339
6834
  }]
6340
6835
  }] });
6341
6836
 
6837
+ var dbMigration_component = /*#__PURE__*/Object.freeze({
6838
+ __proto__: null,
6839
+ DbMigrationComponent: DbMigrationComponent
6840
+ });
6841
+
6342
6842
  /* eslint-disable */
6343
6843
  /* tslint:disable */
6344
6844
  // @ts-nocheck
@@ -6435,7 +6935,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
6435
6935
  type: Injectable
6436
6936
  }] });
6437
6937
 
6438
- var en$3 = {
6938
+ var en$4 = {
6439
6939
  "app-modules": {
6440
6940
  title: "Modules",
6441
6941
  refreshTooltip: "Refresh",
@@ -6466,7 +6966,7 @@ var en$3 = {
6466
6966
  }
6467
6967
  };
6468
6968
 
6469
- var ru$3 = {
6969
+ var ru$4 = {
6470
6970
  "app-modules": {
6471
6971
  title: "Модули",
6472
6972
  refreshTooltip: "Обновить",
@@ -6497,7 +6997,7 @@ var ru$3 = {
6497
6997
  }
6498
6998
  };
6499
6999
 
6500
- L10nService.registerTranslations({ en: en$3, ru: ru$3 });
7000
+ L10nService.registerTranslations({ en: en$4, ru: ru$4 });
6501
7001
  class AppModulesComponent {
6502
7002
  constructor() {
6503
7003
  this.modules = [];
@@ -6744,6 +7244,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
6744
7244
  }]
6745
7245
  }], ctorParameters: () => [] });
6746
7246
 
7247
+ var appModules_component = /*#__PURE__*/Object.freeze({
7248
+ __proto__: null,
7249
+ AppModulesComponent: AppModulesComponent
7250
+ });
7251
+
6747
7252
  var ServiceType;
6748
7253
  (function (ServiceType) {
6749
7254
  ServiceType["Service"] = "Service";
@@ -6794,7 +7299,7 @@ var applications$1 = {
6794
7299
  deleteSuccess: "Application deleted"
6795
7300
  }
6796
7301
  };
6797
- var en$2 = {
7302
+ var en$3 = {
6798
7303
  applications: applications$1
6799
7304
  };
6800
7305
 
@@ -6842,11 +7347,11 @@ var applications = {
6842
7347
  deleteSuccess: "Приложение удалено"
6843
7348
  }
6844
7349
  };
6845
- var ru$2 = {
7350
+ var ru$3 = {
6846
7351
  applications: applications
6847
7352
  };
6848
7353
 
6849
- L10nService.registerTranslations({ en: en$2, ru: ru$2 });
7354
+ L10nService.registerTranslations({ en: en$3, ru: ru$3 });
6850
7355
  class ApplicationsComponent {
6851
7356
  get serviceTypeOptions() {
6852
7357
  return [
@@ -7553,6 +8058,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
7553
8058
  }]
7554
8059
  }], ctorParameters: () => [] });
7555
8060
 
8061
+ var applications_component = /*#__PURE__*/Object.freeze({
8062
+ __proto__: null,
8063
+ ApplicationsComponent: ApplicationsComponent
8064
+ });
8065
+
7556
8066
  /* eslint-disable */
7557
8067
  /* tslint:disable */
7558
8068
  // @ts-nocheck
@@ -7713,7 +8223,7 @@ var discussionComponent$1 = {
7713
8223
  addReaction: "Failed to add reaction."
7714
8224
  }
7715
8225
  };
7716
- var en$1 = {
8226
+ var en$2 = {
7717
8227
  discussionComponent: discussionComponent$1
7718
8228
  };
7719
8229
 
@@ -7757,14 +8267,14 @@ var discussionComponent = {
7757
8267
  addReaction: "Не удалось добавить реакцию."
7758
8268
  }
7759
8269
  };
7760
- var ru$1 = {
8270
+ var ru$2 = {
7761
8271
  discussionComponent: discussionComponent
7762
8272
  };
7763
8273
 
7764
8274
  class DiscussionComponent {
7765
8275
  constructor() {
7766
8276
  this.msgService = inject(MsgService);
7767
- this.translations = provideTranslations({ en: en$1, ru: ru$1 });
8277
+ this.translations = provideTranslations({ en: en$2, ru: ru$2 });
7768
8278
  this.discussionApi = inject(DiscussionApi);
7769
8279
  this.sanitizer = inject(DomSanitizer);
7770
8280
  this.translateService = inject(TranslateService);
@@ -8819,7 +9329,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8819
9329
  args: [{ required: true }]
8820
9330
  }] } });
8821
9331
 
8822
- var en = {
9332
+ var discussion_component = /*#__PURE__*/Object.freeze({
9333
+ __proto__: null,
9334
+ DiscussionComponent: DiscussionComponent
9335
+ });
9336
+
9337
+ var en$1 = {
8823
9338
  "iframe-module": {
8824
9339
  iframeModule: {
8825
9340
  urlPlaceholder: "Site URL",
@@ -8830,7 +9345,7 @@ var en = {
8830
9345
  }
8831
9346
  };
8832
9347
 
8833
- var ru = {
9348
+ var ru$1 = {
8834
9349
  "iframe-module": {
8835
9350
  iframeModule: {
8836
9351
  urlPlaceholder: "URL сайта",
@@ -8844,7 +9359,7 @@ var ru = {
8844
9359
  class IframeModuleComponent extends BaseModuleComponent {
8845
9360
  constructor() {
8846
9361
  super();
8847
- this.translations = provideTranslations({ en, ru });
9362
+ this.translations = provideTranslations({ en: en$1, ru: ru$1 });
8848
9363
  this.renderer = inject(Renderer2);
8849
9364
  this.translate = inject(TranslateService);
8850
9365
  this.iframeUrl = null;
@@ -8987,6 +9502,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
8987
9502
  args: ['iframe']
8988
9503
  }] } });
8989
9504
 
9505
+ var iframeModule_component = /*#__PURE__*/Object.freeze({
9506
+ __proto__: null,
9507
+ IframeModuleComponent: IframeModuleComponent
9508
+ });
9509
+
8990
9510
  const OIP_EXTENSION_EVENTS = {
8991
9511
  contextChange: 'oip:context-change',
8992
9512
  titleChange: 'oip:title-change',
@@ -9094,7 +9614,7 @@ class CustomElementExtensionModuleHostComponent extends BaseModuleComponent {
9094
9614
  async reloadExtension() {
9095
9615
  this.loadError = null;
9096
9616
  this.extensionMetadata = undefined;
9097
- await this.loadExtensionMetadata();
9617
+ await this.moduleLoadingService.track(this.loadExtensionMetadata());
9098
9618
  await this.renderExtension();
9099
9619
  }
9100
9620
  async loadExtensionMetadata() {
@@ -9110,7 +9630,11 @@ class CustomElementExtensionModuleHostComponent extends BaseModuleComponent {
9110
9630
  }));
9111
9631
  await this.loadExtensionTranslations(this.extensionMetadata);
9112
9632
  }
9113
- async renderExtension() {
9633
+ /** Blocks the UI while the extension is (re)mounted, including the queued render after the view init. */
9634
+ renderExtension() {
9635
+ return this.moduleLoadingService.track(this.renderExtensionInternal());
9636
+ }
9637
+ async renderExtensionInternal() {
9114
9638
  if (this.destroyed || !this.container || !this.extensionMetadata || !this.showContent) {
9115
9639
  return;
9116
9640
  }
@@ -9358,7 +9882,7 @@ class ExtensionModuleHostComponent extends BaseModuleComponent {
9358
9882
  async reloadExtension() {
9359
9883
  this.loadError = null;
9360
9884
  this.extensionMetadata = undefined;
9361
- await this.loadExtensionMetadata();
9885
+ await this.moduleLoadingService.track(this.loadExtensionMetadata());
9362
9886
  await this.renderExtension();
9363
9887
  }
9364
9888
  async onModuleInstanceChange() {
@@ -9386,7 +9910,11 @@ class ExtensionModuleHostComponent extends BaseModuleComponent {
9386
9910
  }));
9387
9911
  await this.loadExtensionTranslations(this.extensionMetadata);
9388
9912
  }
9389
- async renderExtension() {
9913
+ /** Blocks the UI while the extension is (re)mounted, including the queued render after the view init. */
9914
+ renderExtension() {
9915
+ return this.moduleLoadingService.track(this.renderExtensionInternal());
9916
+ }
9917
+ async renderExtensionInternal() {
9390
9918
  if (this.destroyed || !this.viewContainer) {
9391
9919
  return;
9392
9920
  }
@@ -9544,6 +10072,67 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9544
10072
  args: ['extensionContainer', { read: ViewContainerRef }]
9545
10073
  }] } });
9546
10074
 
10075
+ var extensionModuleHost_component = /*#__PURE__*/Object.freeze({
10076
+ __proto__: null,
10077
+ ExtensionModuleHostComponent: ExtensionModuleHostComponent
10078
+ });
10079
+
10080
+ var noModules$1 = {
10081
+ title: "No modules available",
10082
+ description: "There is no module you can open yet. Please contact the administrators to get access."
10083
+ };
10084
+ var en = {
10085
+ noModules: noModules$1
10086
+ };
10087
+
10088
+ var noModules = {
10089
+ title: "Нет доступных модулей",
10090
+ description: "Вам пока не доступен ни один модуль. Обратитесь к администраторам, чтобы получить доступ."
10091
+ };
10092
+ var ru = {
10093
+ noModules: noModules
10094
+ };
10095
+
10096
+ /**
10097
+ * Landing page shown when the user has no module instance to open.
10098
+ *
10099
+ * Rendered inside the shell on purpose: the menu and the top bar stay available, so an
10100
+ * administrator can keep working while a regular user sees why the page is empty.
10101
+ */
10102
+ class NoModulesComponent {
10103
+ constructor() {
10104
+ this.translations = provideTranslations({ en, ru });
10105
+ }
10106
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NoModulesComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
10107
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.16", type: NoModulesComponent, isStandalone: true, selector: "app-no-modules", ngImport: i0, template: `<div class="flex flex-col items-center justify-center text-center gap-4 py-20">
10108
+ <i class="pi pi-inbox text-5xl text-surface-400"></i>
10109
+ <h1 class="text-surface-900 dark:text-surface-0 font-bold text-2xl lg:text-3xl m-0">
10110
+ {{ 'noModules.title' | translate }}
10111
+ </h1>
10112
+ <div class="text-surface-600 dark:text-surface-200 max-w-2xl">{{ 'noModules.description' | translate }}</div>
10113
+ </div>`, isInline: true, dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i1$7.TranslatePipe, name: "translate" }] }); }
10114
+ }
10115
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: NoModulesComponent, decorators: [{
10116
+ type: Component,
10117
+ args: [{
10118
+ selector: 'app-no-modules',
10119
+ template: `<div class="flex flex-col items-center justify-center text-center gap-4 py-20">
10120
+ <i class="pi pi-inbox text-5xl text-surface-400"></i>
10121
+ <h1 class="text-surface-900 dark:text-surface-0 font-bold text-2xl lg:text-3xl m-0">
10122
+ {{ 'noModules.title' | translate }}
10123
+ </h1>
10124
+ <div class="text-surface-600 dark:text-surface-200 max-w-2xl">{{ 'noModules.description' | translate }}</div>
10125
+ </div>`,
10126
+ imports: [TranslateModule],
10127
+ standalone: true
10128
+ }]
10129
+ }] });
10130
+
10131
+ var noModules_component = /*#__PURE__*/Object.freeze({
10132
+ __proto__: null,
10133
+ NoModulesComponent: NoModulesComponent
10134
+ });
10135
+
9547
10136
  /**
9548
10137
  * A route guard that ensures the user is authenticated and has a valid access token.
9549
10138
  * If the access token is expired, it attempts to refresh the session.
@@ -9882,11 +10471,214 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
9882
10471
  type: Injectable
9883
10472
  }] });
9884
10473
 
10474
+ /**
10475
+ * Guard that requires an authenticated session and preserves the requested url as the return url.
10476
+ *
10477
+ * Use it on every application route rendered inside the oip shell instead of repeating the
10478
+ * `inject(AuthGuardService)` lambda.
10479
+ */
10480
+ const oipAuthGuard = (_, state) => inject(AuthGuardService).canActivate(state.url);
10481
+ /**
10482
+ * Redirects the empty route to the module instance the user lands on by default.
10483
+ *
10484
+ * The order is: the explicit `startRoute` of the host application, then the module the user marked
10485
+ * as their start page, then the first module available to them, and finally the no modules page.
10486
+ */
10487
+ const oipStartRedirectGuard = async (route) => {
10488
+ const router = inject(Router);
10489
+ const startPageService = inject(StartPageService);
10490
+ const moduleLoading = inject(ModuleLoadingService);
10491
+ const data = (route.data ?? {});
10492
+ const noModules = router.parseUrl(`/${data.noModulesPath ?? 'no-modules'}`);
10493
+ if (data.startRoute) {
10494
+ return router.parseUrl(data.startRoute);
10495
+ }
10496
+ try {
10497
+ // Resolving reads the menu over the network, so keep the blocker up until the target is known.
10498
+ return (await moduleLoading.track(startPageService.resolveStartUrl())) ?? noModules;
10499
+ }
10500
+ catch (error) {
10501
+ console.error('Failed to resolve the start module', error);
10502
+ return noModules;
10503
+ }
10504
+ };
10505
+ /**
10506
+ * Page shown when the user has no module instance to open.
10507
+ */
10508
+ function oipNoModulesRoute(path = 'no-modules') {
10509
+ return {
10510
+ path,
10511
+ loadComponent: () => Promise.resolve().then(function () { return noModules_component; }).then((m) => m.NoModulesComponent),
10512
+ canActivate: [oipAuthGuard]
10513
+ };
10514
+ }
10515
+ /**
10516
+ * Empty route redirecting to the start module of the current user.
10517
+ *
10518
+ * Registered last among the shell children so a host application can claim the empty path itself.
10519
+ */
10520
+ function oipStartRoute(path = '', data = {}) {
10521
+ return {
10522
+ path,
10523
+ pathMatch: 'full',
10524
+ canActivate: [oipAuthGuard, oipStartRedirectGuard],
10525
+ children: [],
10526
+ data
10527
+ };
10528
+ }
10529
+ /**
10530
+ * Access denied page. Referenced by {@link AuthGuardService} and {@link moduleAccessGuard} redirects,
10531
+ * so keep it registered unless the host application provides its own `access` route.
10532
+ */
10533
+ function oipAccessRoute(path = 'access') {
10534
+ return { path, component: AccessComponent };
10535
+ }
10536
+ /** Authentication error page. */
10537
+ function oipErrorRoute(path = 'error') {
10538
+ return {
10539
+ path,
10540
+ loadComponent: () => Promise.resolve().then(function () { return error_component; }).then((m) => m.ErrorComponent)
10541
+ };
10542
+ }
10543
+ /** Current user profile. */
10544
+ function oipProfileRoute(path = 'profile') {
10545
+ return {
10546
+ path,
10547
+ loadComponent: () => Promise.resolve().then(function () { return userProfile_component; }).then((m) => m.UserProfileComponent),
10548
+ canActivate: [oipAuthGuard]
10549
+ };
10550
+ }
10551
+ /** Application configuration. */
10552
+ function oipConfigRoute(path = 'config') {
10553
+ return {
10554
+ path,
10555
+ loadComponent: () => Promise.resolve().then(function () { return config_component; }).then((m) => m.ConfigComponent),
10556
+ canActivate: [oipAuthGuard]
10557
+ };
10558
+ }
10559
+ /** Registered applications, administrators only. */
10560
+ function oipApplicationsRoute(path = 'applications') {
10561
+ return {
10562
+ path,
10563
+ loadComponent: () => Promise.resolve().then(function () { return applications_component; }).then((m) => m.ApplicationsComponent),
10564
+ canActivate: [oipAuthGuard],
10565
+ data: { requireAdmin: true }
10566
+ };
10567
+ }
10568
+ /** Module registry, administrators only. */
10569
+ function oipModulesRoute(path = 'modules') {
10570
+ return {
10571
+ path,
10572
+ loadComponent: () => Promise.resolve().then(function () { return appModules_component; }).then((m) => m.AppModulesComponent),
10573
+ canActivate: [oipAuthGuard],
10574
+ data: { requireAdmin: true }
10575
+ };
10576
+ }
10577
+ /** Discussion module. The path must keep an `:id` segment. */
10578
+ function oipDiscussionRoute(path = 'discussion/:id') {
10579
+ return {
10580
+ path,
10581
+ loadComponent: () => Promise.resolve().then(function () { return discussion_component; }).then((m) => m.DiscussionComponent),
10582
+ canActivate: [oipAuthGuard]
10583
+ };
10584
+ }
10585
+ /** Database migration module. The path must keep an `:id` segment. */
10586
+ function oipDbMigrationRoute(path = 'db-migration/:id') {
10587
+ return {
10588
+ path,
10589
+ loadComponent: () => Promise.resolve().then(function () { return dbMigration_component; }).then((m) => m.DbMigrationComponent),
10590
+ canActivate: [oipAuthGuard]
10591
+ };
10592
+ }
10593
+ /** Iframe module host. The path must keep an `:id` segment. */
10594
+ function oipIframeModuleRoute(path = 'iframe-module/:id') {
10595
+ return {
10596
+ path,
10597
+ loadComponent: () => Promise.resolve().then(function () { return iframeModule_component; }).then((m) => m.IframeModuleComponent),
10598
+ canActivate: [oipAuthGuard]
10599
+ };
10600
+ }
10601
+ /** Extension module host. The path must keep the `:extensionKey` and `:id` segments. */
10602
+ function oipExtensionsRoute(path = 'extensions/:extensionKey/:id') {
10603
+ return {
10604
+ path,
10605
+ loadComponent: () => Promise.resolve().then(function () { return extensionModuleHost_component; }).then((m) => m.ExtensionModuleHostComponent),
10606
+ canActivate: [oipAuthGuard]
10607
+ };
10608
+ }
10609
+ function builtInRoute(toggle, factory, into) {
10610
+ if (toggle === false) {
10611
+ return;
10612
+ }
10613
+ into.push(typeof toggle === 'string' ? factory(toggle) : factory());
10614
+ }
10615
+ /**
10616
+ * Builds the standard oip route tree: an authenticated shell holding the application routes and the
10617
+ * built-in pages, followed by the unauthorized, not found and catch-all routes.
10618
+ *
10619
+ * The returned array is ordered so that the `**` route stays last; concatenating anything after it
10620
+ * makes those routes unreachable.
10621
+ *
10622
+ * @example
10623
+ * export const appRoutes = provideOipRoutes({
10624
+ * children: [
10625
+ * {
10626
+ * path: 'dashboard/:id',
10627
+ * loadComponent: () => import('./dashboard.component').then((m) => m.DashboardComponent),
10628
+ * canActivate: [oipAuthGuard]
10629
+ * }
10630
+ * ],
10631
+ * features: { dbMigration: 'legacy-migration/:id', modules: false },
10632
+ * startRoute: '/dashboard/1'
10633
+ * });
10634
+ */
10635
+ function provideOipRoutes(options = {}) {
10636
+ const features = options.features ?? {};
10637
+ const children = [...(options.children ?? [])];
10638
+ builtInRoute(features.access, oipAccessRoute, children);
10639
+ builtInRoute(features.error, oipErrorRoute, children);
10640
+ builtInRoute(features.profile, oipProfileRoute, children);
10641
+ builtInRoute(features.config, oipConfigRoute, children);
10642
+ builtInRoute(features.applications, oipApplicationsRoute, children);
10643
+ builtInRoute(features.modules, oipModulesRoute, children);
10644
+ builtInRoute(features.discussion, oipDiscussionRoute, children);
10645
+ builtInRoute(features.dbMigration, oipDbMigrationRoute, children);
10646
+ builtInRoute(features.iframeModule, oipIframeModuleRoute, children);
10647
+ builtInRoute(features.extensions, oipExtensionsRoute, children);
10648
+ builtInRoute(features.noModules, oipNoModulesRoute, children);
10649
+ // Registered last so a host application that declares its own empty path keeps it.
10650
+ if (features.start !== false && !children.some((route) => route.path === '')) {
10651
+ const noModulesPath = typeof features.noModules === 'string' ? features.noModules : 'no-modules';
10652
+ const startPath = typeof features.start === 'string' ? features.start : '';
10653
+ children.push(oipStartRoute(startPath, { startRoute: options.startRoute, noModulesPath }));
10654
+ }
10655
+ const notFoundPath = options.notFoundPath ?? 'notfound';
10656
+ const routes = [
10657
+ {
10658
+ path: '',
10659
+ component: options.layout ?? AppLayoutComponent,
10660
+ canActivate: [oipAuthGuard],
10661
+ canActivateChild: [moduleAccessGuard],
10662
+ children
10663
+ },
10664
+ {
10665
+ path: options.unauthorizedPath ?? 'unauthorized',
10666
+ loadComponent: () => Promise.resolve().then(function () { return unauthorized_component; }).then((m) => m.UnauthorizedComponent)
10667
+ },
10668
+ ...(options.rootRoutes ?? []),
10669
+ { path: notFoundPath, component: NotfoundComponent }
10670
+ ];
10671
+ if (options.wildcard !== false) {
10672
+ routes.push({ path: '**', redirectTo: `/${notFoundPath}` });
10673
+ }
10674
+ return routes;
10675
+ }
10676
+
9885
10677
  // Components
9886
10678
 
9887
10679
  /**
9888
10680
  * Generated bundle index. Do not edit.
9889
10681
  */
9890
10682
 
9891
- export { APP_INFO_TOKEN, APP_THEME_PRESETS, APP_THEME_PRESETS_MERGE_MODE, AccessComponent, AppConfiguratorComponent, AppFloatingConfiguratorComponent, AppInfoService, AppLayoutComponent, AppModulesComponent, AppTopbar, AppTopbarApplicationSwitcherComponent, ApplicationRegistryService, ApplicationsApi, ApplicationsComponent, AuthGuardService, BaseModuleComponent, BffSecurityService, ConfigComponent, ContentType, CustomElementExtensionModuleHostComponent, DbMigrationComponent, DiscussionComponent, ErrorComponent, ExtensionLoaderService, ExtensionModuleHostComponent, FolderModuleApi, FooterComponent, HttpClient, IframeModuleApi, IframeModuleComponent, L10nService, LOGO_COMPONENT_TOKEN, LayoutService, LogoComponent, LogoService, MenuComponent, MenuService, ModuleInstanceRightsService, MsgService, NotfoundComponent, NotificationApi, NotificationService, OIP_EXTENSION_EVENTS, SecurityApi, SecurityComponent, SecurityService, SidebarComponent, TableFilterService, TopBarService, UnauthorizedComponent, UserNotificationsComponent, UserProfileApi, UserProfileComponent, UserService, convertToPrimeNgDateFormat, defaultTheme, emitOipContextChange, emitOipError, emitOipNavigate, emitOipNotify, emitOipSettingsChange, emitOipTitleChange, langIntercept, mergeWithDefaults, moduleAccessGuard, provideAppInfo, provideAppThemes, provideLogoComponent, provideOip, provideTranslations, replaceDefaults };
10683
+ export { APP_INFO_TOKEN, APP_THEME_PRESETS, APP_THEME_PRESETS_MERGE_MODE, AccessComponent, AppConfiguratorComponent, AppFloatingConfiguratorComponent, AppInfoService, AppLayoutComponent, AppModulesComponent, AppTopbar, AppTopbarApplicationSwitcherComponent, ApplicationRegistryService, ApplicationsApi, ApplicationsComponent, AuthGuardService, BaseModuleComponent, BffSecurityService, BlockLoaderComponent, ConfigComponent, ContentType, CustomElementExtensionModuleHostComponent, DbMigrationComponent, DiscussionComponent, ErrorComponent, ExtensionLoaderService, ExtensionModuleHostComponent, FolderModuleApi, FooterComponent, HttpClient, IframeModuleApi, IframeModuleComponent, L10nService, LOGO_COMPONENT_TOKEN, LayoutService, LogoComponent, LogoService, MenuComponent, MenuService, ModuleInstanceRightsService, ModuleLoadingService, MsgService, NoModulesComponent, NotfoundComponent, NotificationApi, NotificationService, OIP_EXTENSION_EVENTS, SecurityApi, SecurityComponent, SecurityService, SidebarComponent, StartPageService, TableFilterService, TopBarService, UnauthorizedComponent, UserNotificationsComponent, UserProfileApi, UserProfileComponent, UserService, convertToPrimeNgDateFormat, defaultTheme, emitOipContextChange, emitOipError, emitOipNavigate, emitOipNotify, emitOipSettingsChange, emitOipTitleChange, langIntercept, mergeWithDefaults, moduleAccessGuard, oipAccessRoute, oipApplicationsRoute, oipAuthGuard, oipConfigRoute, oipDbMigrationRoute, oipDiscussionRoute, oipErrorRoute, oipExtensionsRoute, oipIframeModuleRoute, oipModulesRoute, oipNoModulesRoute, oipProfileRoute, oipStartRedirectGuard, oipStartRoute, provideAppInfo, provideAppThemes, provideLogoComponent, provideOip, provideOipRoutes, provideTranslations, replaceDefaults };
9892
10684
  //# sourceMappingURL=oip-common.mjs.map