aril 1.2.17 → 2.0.1-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/boot/bridge/src/mfe-bridge.d.ts +42 -2
  2. package/boot/config/apps/index.d.ts +7 -1
  3. package/boot/config/apps/src/custom-reuse-outlet.component.d.ts +37 -0
  4. package/boot/config/apps/src/custom-route-reuse-strategy.class.d.ts +156 -0
  5. package/boot/config/apps/src/nav-link-context-menu.service.d.ts +33 -0
  6. package/boot/config/apps/src/nav-link.directive.d.ts +29 -0
  7. package/boot/config/apps/src/nav.service.d.ts +198 -0
  8. package/boot/config/apps/src/route-close.service.d.ts +9 -0
  9. package/boot/config/apps/src/safe-navigate.d.ts +17 -0
  10. package/boot/config/apps/src/tab-aware-url-serializer.d.ts +22 -0
  11. package/boot/config/plugins/src/getNgZone.d.ts +9 -1
  12. package/boot/mfe/src/app.component.d.ts +15 -4
  13. package/boot/mfe/src/isolated-location-strategy.d.ts +57 -0
  14. package/esm2022/boot/bridge/src/mfe-bridge.mjs +36 -5
  15. package/esm2022/boot/config/api/src/api.service.mjs +12 -3
  16. package/esm2022/boot/config/apps/index.mjs +8 -2
  17. package/esm2022/boot/config/apps/src/apps.service.mjs +14 -6
  18. package/esm2022/boot/config/apps/src/custom-reuse-outlet.component.mjs +207 -0
  19. package/esm2022/boot/config/apps/src/custom-route-reuse-strategy.class.mjs +540 -0
  20. package/esm2022/boot/config/apps/src/nav-link-context-menu.service.mjs +105 -0
  21. package/esm2022/boot/config/apps/src/nav-link.directive.mjs +45 -0
  22. package/esm2022/boot/config/apps/src/nav.service.mjs +675 -0
  23. package/esm2022/boot/config/apps/src/route-close.service.mjs +19 -0
  24. package/esm2022/boot/config/apps/src/safe-navigate.mjs +50 -0
  25. package/esm2022/boot/config/apps/src/tab-aware-url-serializer.mjs +50 -0
  26. package/esm2022/boot/config/plugins/src/getNgZone.mjs +13 -5
  27. package/esm2022/boot/host/src/app.component.mjs +1 -2
  28. package/esm2022/boot/host/src/bootstrap.mjs +22 -7
  29. package/esm2022/boot/mfe/src/app.component.mjs +143 -39
  30. package/esm2022/boot/mfe/src/bootstrap.mjs +197 -20
  31. package/esm2022/boot/mfe/src/isolated-location-strategy.mjs +142 -0
  32. package/esm2022/keycloak/src/auth.interceptor.mjs +17 -2
  33. package/esm2022/provider/src/prodiveHost.mjs +3 -5
  34. package/esm2022/provider/src/prodiveHostRouter.mjs +88 -9
  35. package/esm2022/theme/layout/app/expandableMenu/expandable-menu.component.mjs +81 -19
  36. package/esm2022/theme/layout/app/favorite-pages/favorite-pages-sidebar.component.mjs +6 -4
  37. package/esm2022/theme/layout/app/general-search/general-search.component.mjs +4 -4
  38. package/esm2022/theme/layout/app/history/history-sidebar.component.mjs +6 -4
  39. package/esm2022/theme/layout/app/layout/app.layout.component.mjs +422 -20
  40. package/esm2022/theme/layout/app/layout/mfe.layout.component.mjs +24 -35
  41. package/esm2022/theme/layout/app/site-map/site-map-sidebar.component.mjs +6 -4
  42. package/esm2022/theme/layout/app/static-sidebar/static-sidebar.component.mjs +85 -27
  43. package/esm2022/theme/layout/app/topbar/app.topbar.component.mjs +3 -3
  44. package/esm2022/theme/layout/service/breadcrumb-publisher.service.mjs +86 -0
  45. package/esm2022/theme/layout/service/tab-session.service.mjs +126 -0
  46. package/esm2022/ui/table/src/table.component.mjs +10 -10
  47. package/esm2022/ui-business/ref-value/src/ref-value.component.mjs +15 -7
  48. package/esm2022/util/sync-active-tab-route/src/sync-active-tab-route.directive.mjs +29 -9
  49. package/fesm2022/aril-app.component-s14ruALV.mjs +183 -0
  50. package/fesm2022/aril-app.component-s14ruALV.mjs.map +1 -0
  51. package/fesm2022/aril-boot-bridge.mjs +35 -4
  52. package/fesm2022/aril-boot-bridge.mjs.map +1 -1
  53. package/fesm2022/aril-boot-config-api.mjs +11 -2
  54. package/fesm2022/aril-boot-config-api.mjs.map +1 -1
  55. package/fesm2022/aril-boot-config-apps.mjs +1678 -10
  56. package/fesm2022/aril-boot-config-apps.mjs.map +1 -1
  57. package/fesm2022/aril-boot-config-plugins.mjs +12 -4
  58. package/fesm2022/aril-boot-config-plugins.mjs.map +1 -1
  59. package/fesm2022/aril-boot-host.mjs +21 -7
  60. package/fesm2022/aril-boot-host.mjs.map +1 -1
  61. package/fesm2022/aril-boot-mfe-app.component-a34GeuUv.mjs +183 -0
  62. package/fesm2022/aril-boot-mfe-app.component-a34GeuUv.mjs.map +1 -0
  63. package/fesm2022/aril-boot-mfe-aril-boot-mfe-KFO_X7yR.mjs +631 -0
  64. package/fesm2022/aril-boot-mfe-aril-boot-mfe-KFO_X7yR.mjs.map +1 -0
  65. package/fesm2022/aril-boot-mfe.mjs +5 -3
  66. package/fesm2022/aril-boot-mfe.mjs.map +1 -1
  67. package/fesm2022/aril-keycloak.mjs +16 -1
  68. package/fesm2022/aril-keycloak.mjs.map +1 -1
  69. package/fesm2022/aril-provider.mjs +90 -12
  70. package/fesm2022/aril-provider.mjs.map +1 -1
  71. package/fesm2022/aril-theme-layout.mjs +2630 -2017
  72. package/fesm2022/aril-theme-layout.mjs.map +1 -1
  73. package/fesm2022/aril-ui-business-ref-value.mjs +14 -6
  74. package/fesm2022/aril-ui-business-ref-value.mjs.map +1 -1
  75. package/fesm2022/aril-ui-table.mjs +9 -9
  76. package/fesm2022/aril-ui-table.mjs.map +1 -1
  77. package/fesm2022/aril-util-sync-active-tab-route.mjs +28 -8
  78. package/fesm2022/aril-util-sync-active-tab-route.mjs.map +1 -1
  79. package/fesm2022/aril.mjs +354 -25
  80. package/fesm2022/aril.mjs.map +1 -1
  81. package/keycloak/src/auth.interceptor.d.ts +7 -0
  82. package/package.json +188 -188
  83. package/provider/src/prodiveHost.d.ts +1 -0
  84. package/theme/layout/app/expandableMenu/expandable-menu.component.d.ts +21 -4
  85. package/theme/layout/app/expandableMenu/expandable-menu.component.html +19 -5
  86. package/theme/layout/app/expandableMenu/expandable-menu.component.ts +69 -9
  87. package/theme/layout/app/favorite-pages/favorite-pages-sidebar.component.html +1 -0
  88. package/theme/layout/app/favorite-pages/favorite-pages-sidebar.component.ts +3 -1
  89. package/theme/layout/app/general-search/general-search.component.html +2 -1
  90. package/theme/layout/app/general-search/general-search.component.ts +2 -2
  91. package/theme/layout/app/history/history-sidebar.component.html +3 -1
  92. package/theme/layout/app/history/history-sidebar.component.ts +3 -1
  93. package/theme/layout/app/layout/app.layout.component.d.ts +105 -5
  94. package/theme/layout/app/layout/app.layout.component.html +102 -1
  95. package/theme/layout/app/layout/app.layout.component.scss +372 -0
  96. package/theme/layout/app/layout/app.layout.component.ts +452 -13
  97. package/theme/layout/app/layout/mfe.layout.component.d.ts +7 -5
  98. package/theme/layout/app/layout/mfe.layout.component.ts +13 -39
  99. package/theme/layout/app/site-map/site-map-sidebar.component.html +1 -0
  100. package/theme/layout/app/site-map/site-map-sidebar.component.ts +3 -1
  101. package/theme/layout/app/static-sidebar/static-sidebar.component.d.ts +26 -5
  102. package/theme/layout/app/static-sidebar/static-sidebar.component.html +11 -5
  103. package/theme/layout/app/static-sidebar/static-sidebar.component.ts +68 -13
  104. package/theme/layout/app/topbar/app.topbar.component.html +0 -1
  105. package/theme/layout/app/topbar/app.topbar.component.scss +1 -1
  106. package/theme/layout/service/breadcrumb-publisher.service.d.ts +24 -0
  107. package/theme/layout/service/breadcrumb-publisher.service.ts +95 -0
  108. package/theme/layout/service/tab-session.service.d.ts +52 -0
  109. package/theme/layout/service/tab-session.service.ts +138 -0
  110. package/theme/styles/layout/_breadcrumb.scss +95 -0
  111. package/theme/styles/layout/_content.scss +2 -2
  112. package/ui/table/src/table.component.d.ts +2 -2
  113. package/ui-business/ref-value/src/ref-value.component.d.ts +4 -2
  114. package/util/sync-active-tab-route/src/sync-active-tab-route.directive.d.ts +15 -2
  115. package/boot/config/apps/src/reuse-strategy.d.ts +0 -4
  116. package/esm2022/boot/config/apps/src/reuse-strategy.mjs +0 -9
  117. package/esm2022/theme/layout/app/breadcrumb/app.breadcrumb.component.mjs +0 -107
  118. package/fesm2022/aril-app.component-wxP3y8dg.mjs +0 -81
  119. package/fesm2022/aril-app.component-wxP3y8dg.mjs.map +0 -1
  120. package/fesm2022/aril-boot-mfe-app.component-7IjAmjz0.mjs +0 -80
  121. package/fesm2022/aril-boot-mfe-app.component-7IjAmjz0.mjs.map +0 -1
  122. package/fesm2022/aril-boot-mfe-aril-boot-mfe-KXDpUyv7.mjs +0 -315
  123. package/fesm2022/aril-boot-mfe-aril-boot-mfe-KXDpUyv7.mjs.map +0 -1
  124. package/theme/layout/app/breadcrumb/app.breadcrumb.component.d.ts +0 -25
  125. package/theme/layout/app/breadcrumb/app.breadcrumb.component.html +0 -8
  126. package/theme/layout/app/breadcrumb/app.breadcrumb.component.ts +0 -127
@@ -2,10 +2,9 @@ import { HttpClient } from '@angular/common/http';
2
2
  import { OnDestroy, OnInit, Signal } from '@angular/core';
3
3
  import { Router } from '@angular/router';
4
4
  import { TranslocoService } from '@ngneat/transloco';
5
- import { MenuItemAction, PluginMenuItem } from 'aril/boot/config/apps';
5
+ import { MenuItemAction, NavService, PluginMenuItem } from 'aril/boot/config/apps';
6
6
  import { TranslateJsonPipe } from 'aril/util/pipes';
7
7
  import { LayoutService } from '../../service/app.layout.service';
8
- import { AppMenuService } from '../../service/app.menu.service';
9
8
  import * as i0 from "@angular/core";
10
9
  interface MenuNode {
11
10
  key: string;
@@ -18,7 +17,6 @@ interface MenuNode {
18
17
  actionLoading?: boolean;
19
18
  }
20
19
  export declare class StaticSidebarComponent implements OnInit, OnDestroy {
21
- private readonly menuService;
22
20
  private readonly router;
23
21
  private readonly layoutService;
24
22
  private readonly translateJsonPipe;
@@ -28,10 +26,11 @@ export declare class StaticSidebarComponent implements OnInit, OnDestroy {
28
26
  menuNodes: import("@angular/core").WritableSignal<MenuNode[]>;
29
27
  filteredNodes: import("@angular/core").WritableSignal<MenuNode[]>;
30
28
  searchTerm: import("@angular/core").WritableSignal<string>;
29
+ navService: NavService;
31
30
  private routerSubscription;
32
31
  private actionSubscription;
33
32
  private readonly MAX_LENGTH;
34
- constructor(menuService: AppMenuService, router: Router, layoutService: LayoutService, translateJsonPipe: TranslateJsonPipe, translocoService: TranslocoService, http: HttpClient);
33
+ constructor(router: Router, layoutService: LayoutService, translateJsonPipe: TranslateJsonPipe, translocoService: TranslocoService, http: HttpClient);
35
34
  get isCollapsed(): boolean;
36
35
  get topLevelNodes(): MenuNode[];
37
36
  ngOnInit(): void;
@@ -44,6 +43,12 @@ export declare class StaticSidebarComponent implements OnInit, OnDestroy {
44
43
  convertToMenuNodes(items: PluginMenuItem[], parentKey?: string): MenuNode[];
45
44
  getMenuItems(): PluginMenuItem[];
46
45
  getLocalText(text: any): string;
46
+ /**
47
+ * Hash strategy için routerLink'i `#`-prefix'li href'e çevirir. routerLink config'te
48
+ * leading slash içerebilir (örn. anasayfa için `'/'`); naive `'#/' + routerLink`
49
+ * concatenation `#//` üretirdi — sağ tık "linki kopyala" UX'inde bu çirkin görünür.
50
+ */
51
+ getHashHref(routerLink: string | undefined | null): string;
47
52
  isActiveRouteForNode(node: MenuNode): boolean;
48
53
  isNodeOrChildActive(node: MenuNode): boolean;
49
54
  isTopLevelNodeActive(node: MenuNode): boolean;
@@ -51,10 +56,26 @@ export declare class StaticSidebarComponent implements OnInit, OnDestroy {
51
56
  toggleNode(node: MenuNode, level?: number): void;
52
57
  private closeOtherTopLevelNodes;
53
58
  private closeAllChildNodes;
54
- onMenuItemClick(): void;
59
+ /**
60
+ * Chrome-like menü click davranışı:
61
+ * - Normal click → aktif tab'da navigate (yeni tab açma)
62
+ * - Ctrl/Cmd + Click → arka planda yeni tab
63
+ * - Ctrl/Cmd + Shift + Click → ön planda yeni tab (yeni tab aç + oraya geç)
64
+ */
65
+ onMenuClick(event: MouseEvent, node: MenuNode): void;
66
+ /** Middle click (mouse button 1) → arka planda yeni tab. */
67
+ onMenuAuxClick(event: MouseEvent, node: MenuNode): void;
68
+ /** Middle click default scroll davranışını engelle. */
69
+ onMenuMousedown(event: MouseEvent): void;
70
+ private afterMenuClick;
55
71
  onActionItemClick(node: MenuNode, event: Event): void;
56
72
  private buildSuccessRoute;
57
73
  private getValueByPath;
74
+ /**
75
+ * Collapsed-mode UI-only handler: sidebar'ı aç, kardeş toplevel'ları kapat, children varsa expand.
76
+ * Navigation YAPMAZ — routerLink'li node'larda `(click)` template'te `onMenuClick` ile zincirleniyor;
77
+ * `afterMenuClick` da orada bir kez çağrılır.
78
+ */
58
79
  onCollapsedItemClick(node: MenuNode): void;
59
80
  onSearchChange(): void;
60
81
  private filterNodes;
@@ -46,9 +46,12 @@
46
46
  class="collapsed-item"
47
47
  [pTooltip]="getLocalText(node.label)"
48
48
  tooltipPosition="right"
49
- [routerLink]="node.routerLink"
49
+ [attr.href]="getHashHref(node.routerLink)"
50
50
  [class.active]="isTopLevelNodeActive(node)"
51
- (click)="onCollapsedItemClick(node)">
51
+ [arilNavLink]="{ tabId: '', navLink: node.routerLink, navName: getLocalText(node.label), icon: node.icon }"
52
+ (click)="onCollapsedItemClick(node); onMenuClick($event, node)"
53
+ (auxclick)="onMenuAuxClick($event, node)"
54
+ (mousedown)="onMenuMousedown($event)">
52
55
  @if (node.icon) {
53
56
  <i class="collapsed-icon" [class]="node.icon"></i>
54
57
  } @else {
@@ -103,14 +106,17 @@
103
106
  </div>
104
107
  } @else if (node.routerLink) {
105
108
  <a
106
- [routerLink]="node.routerLink"
109
+ [attr.href]="getHashHref(node.routerLink)"
110
+ [arilNavLink]="{ tabId: '', navLink: node.routerLink, navName: getLocalText(node.label), icon: node.icon }"
111
+ (click)="onMenuClick($event, node)"
112
+ (auxclick)="onMenuAuxClick($event, node)"
113
+ (mousedown)="onMenuMousedown($event)"
107
114
  class="node-item"
108
115
  [class.has-children]="node.children && node.children.length > 0"
109
116
  [class.is-page]="node.routerLink"
110
117
  [class.active]="isChildNodeActive(node)"
111
118
  [pTooltip]="!isMobile() && isTextTruncated(node.label, activeLang(), level) ? getLocalText(node.label) : ''"
112
- tooltipPosition="right"
113
- (click)="onMenuItemClick()">
119
+ tooltipPosition="right">
114
120
  <div class="node-content">
115
121
  @if (node.children && node.children.length > 0) {
116
122
  <div class="toggle-btn" (click)="$event.stopPropagation(); toggleNode(node, level)">
@@ -1,9 +1,9 @@
1
1
  import { NgClass, NgTemplateOutlet } from '@angular/common';
2
2
  import { HttpClient } from '@angular/common/http';
3
- import { Component, OnDestroy, OnInit, Signal, signal } from '@angular/core';
3
+ import { Component, inject, OnDestroy, OnInit, Signal, signal } from '@angular/core';
4
4
  import { toSignal } from '@angular/core/rxjs-interop';
5
5
  import { FormsModule } from '@angular/forms';
6
- import { NavigationEnd, Router, RouterLink } from '@angular/router';
6
+ import { NavigationEnd, Router } from '@angular/router';
7
7
 
8
8
  import { InputTextModule } from 'primeng/inputtext';
9
9
  import { ScrollPanelModule } from 'primeng/scrollpanel';
@@ -13,11 +13,10 @@ import { TranslocoModule, TranslocoService } from '@ngneat/transloco';
13
13
  import { Subscription, filter } from 'rxjs';
14
14
 
15
15
  import { bridge } from 'aril/boot/bridge';
16
- import { MenuItemAction, PluginMenuItem } from 'aril/boot/config/apps';
16
+ import { MenuItemAction, NavItem, NavLinkDirective, NavService, PluginMenuItem } from 'aril/boot/config/apps';
17
17
  import { TranslateJsonPipe } from 'aril/util/pipes';
18
18
 
19
19
  import { LayoutService } from '../../service/app.layout.service';
20
- import { AppMenuService } from '../../service/app.menu.service';
21
20
 
22
21
  interface MenuNode {
23
22
  key: string;
@@ -34,14 +33,14 @@ interface MenuNode {
34
33
  standalone: true,
35
34
  selector: 'app-static-sidebar',
36
35
  imports: [
37
- RouterLink,
38
36
  NgClass,
39
37
  NgTemplateOutlet,
40
38
  ScrollPanelModule,
41
39
  TooltipModule,
42
40
  FormsModule,
43
41
  TranslocoModule,
44
- InputTextModule
42
+ InputTextModule,
43
+ NavLinkDirective
45
44
  ],
46
45
  templateUrl: './static-sidebar.component.html',
47
46
  styleUrl: './static-sidebar.component.scss',
@@ -52,6 +51,7 @@ export class StaticSidebarComponent implements OnInit, OnDestroy {
52
51
  menuNodes = signal<MenuNode[]>([]);
53
52
  filteredNodes = signal<MenuNode[]>([]);
54
53
  searchTerm = signal<string>('');
54
+ navService: NavService = inject(NavService);
55
55
 
56
56
  private routerSubscription: Subscription | null = null;
57
57
  private actionSubscription: Subscription | null = null;
@@ -59,7 +59,6 @@ export class StaticSidebarComponent implements OnInit, OnDestroy {
59
59
  private readonly MAX_LENGTH = { level0: 25, level1: 20, level2: 16, level3: 13 };
60
60
 
61
61
  constructor(
62
- private readonly menuService: AppMenuService,
63
62
  private readonly router: Router,
64
63
  private readonly layoutService: LayoutService,
65
64
  private readonly translateJsonPipe: TranslateJsonPipe,
@@ -154,13 +153,23 @@ export class StaticSidebarComponent implements OnInit, OnDestroy {
154
153
  }
155
154
 
156
155
  getMenuItems(): PluginMenuItem[] {
157
- return bridge.isHostMode() ? bridge.hostMenuItems() : this.menuService.menuItems();
156
+ return bridge.hostMenuItems();
158
157
  }
159
158
 
160
159
  getLocalText(text: any): string {
161
160
  return this.translateJsonPipe.transform(text);
162
161
  }
163
162
 
163
+ /**
164
+ * Hash strategy için routerLink'i `#`-prefix'li href'e çevirir. routerLink config'te
165
+ * leading slash içerebilir (örn. anasayfa için `'/'`); naive `'#/' + routerLink`
166
+ * concatenation `#//` üretirdi — sağ tık "linki kopyala" UX'inde bu çirkin görünür.
167
+ */
168
+ getHashHref(routerLink: string | undefined | null): string {
169
+ if (!routerLink) return '#';
170
+ return routerLink.startsWith('/') ? '#' + routerLink : '#/' + routerLink;
171
+ }
172
+
164
173
  isActiveRouteForNode(node: MenuNode): boolean {
165
174
  if (!node.routerLink) return false;
166
175
  const currentUrl = this.router.url;
@@ -222,7 +231,51 @@ export class StaticSidebarComponent implements OnInit, OnDestroy {
222
231
  }
223
232
  }
224
233
 
225
- onMenuItemClick() {
234
+ /**
235
+ * Chrome-like menü click davranışı:
236
+ * - Normal click → aktif tab'da navigate (yeni tab açma)
237
+ * - Ctrl/Cmd + Click → arka planda yeni tab
238
+ * - Ctrl/Cmd + Shift + Click → ön planda yeni tab (yeni tab aç + oraya geç)
239
+ */
240
+ onMenuClick(event: MouseEvent, node: MenuNode): void {
241
+ if (!node.routerLink) return;
242
+ event.preventDefault();
243
+ const navItem: NavItem = {
244
+ tabId: '',
245
+ navLink: node.routerLink,
246
+ navName: this.getLocalText(node.label),
247
+ icon: node.icon
248
+ };
249
+ if (event.ctrlKey || event.metaKey) {
250
+ if (event.shiftKey) {
251
+ this.navService.openInNewTabAndFocus(navItem);
252
+ } else {
253
+ this.navService.openInBackgroundTab(navItem);
254
+ }
255
+ } else {
256
+ this.navService.navigateInCurrentTab(navItem);
257
+ }
258
+ this.afterMenuClick();
259
+ }
260
+
261
+ /** Middle click (mouse button 1) → arka planda yeni tab. */
262
+ onMenuAuxClick(event: MouseEvent, node: MenuNode): void {
263
+ if (event.button !== 1 || !node.routerLink) return;
264
+ event.preventDefault();
265
+ this.navService.openInBackgroundTab({
266
+ tabId: '',
267
+ navLink: node.routerLink,
268
+ navName: this.getLocalText(node.label),
269
+ icon: node.icon
270
+ });
271
+ }
272
+
273
+ /** Middle click default scroll davranışını engelle. */
274
+ onMenuMousedown(event: MouseEvent): void {
275
+ if (event.button === 1) event.preventDefault();
276
+ }
277
+
278
+ private afterMenuClick(): void {
226
279
  if (this.isMobile()) {
227
280
  this.layoutService.closeMobileMenu();
228
281
  }
@@ -252,7 +305,7 @@ export class StaticSidebarComponent implements OnInit, OnDestroy {
252
305
  node.actionLoading = false;
253
306
  const route = this.buildSuccessRoute(node.action!, response);
254
307
  this.router.navigate([route]);
255
- this.onMenuItemClick();
308
+ this.afterMenuClick();
256
309
  },
257
310
  error: () => {
258
311
  node.actionLoading = false;
@@ -287,6 +340,11 @@ export class StaticSidebarComponent implements OnInit, OnDestroy {
287
340
  }, obj);
288
341
  }
289
342
 
343
+ /**
344
+ * Collapsed-mode UI-only handler: sidebar'ı aç, kardeş toplevel'ları kapat, children varsa expand.
345
+ * Navigation YAPMAZ — routerLink'li node'larda `(click)` template'te `onMenuClick` ile zincirleniyor;
346
+ * `afterMenuClick` da orada bir kez çağrılır.
347
+ */
290
348
  onCollapsedItemClick(node: MenuNode) {
291
349
  if (this.isCollapsed) {
292
350
  this.layoutService.onMenuToggle();
@@ -296,9 +354,6 @@ export class StaticSidebarComponent implements OnInit, OnDestroy {
296
354
  node.expanded = true;
297
355
  }
298
356
  this.filteredNodes.set([...this.filteredNodes()]);
299
- if (node.routerLink) {
300
- this.onMenuItemClick();
301
- }
302
357
  }
303
358
 
304
359
  onSearchChange(): void {
@@ -82,4 +82,3 @@
82
82
  </div>
83
83
  </div>
84
84
 
85
- <!-- <app-breadcrumb class="topbar-breadcrumb"></app-breadcrumb> -->
@@ -6,7 +6,7 @@
6
6
  flex-wrap: nowrap;
7
7
  gap: 0.5rem;
8
8
  padding: 0 1rem;
9
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.075);
9
+ border-bottom: 1px solid var(--surface-border, #e5e7eb);
10
10
 
11
11
  @media (max-width: 768px) {
12
12
  padding: 0 0.5rem;
@@ -0,0 +1,24 @@
1
+ import { OnDestroy } from '@angular/core';
2
+ import * as i0 from "@angular/core";
3
+ /**
4
+ * MFE-side route ağacını dolaşıp aktif route zincirinin breadcrumb listesini
5
+ * `bridge.breadcrumbs` signal'ine yazar. Shell layout bu signal'i okuyup
6
+ * breadcrumb şeridini render eder; presenter shell'de, source-of-truth MFE'de.
7
+ *
8
+ * Eski `<app-breadcrumb>` component'inin logic'i bu service'e taşınmıştır.
9
+ * Geriye dönük uyumluluk için `breadcrumbUpdated` PubSub event'i hâlâ yayınlanır
10
+ * (history.service abone).
11
+ */
12
+ export declare class BreadcrumbPublisherService implements OnDestroy {
13
+ private readonly router;
14
+ private readonly breadcrumbService;
15
+ private readonly pubSubService;
16
+ private routerSubscription;
17
+ constructor();
18
+ ngOnDestroy(): void;
19
+ private publishFromCurrentRoute;
20
+ private collect;
21
+ private publish;
22
+ static ɵfac: i0.ɵɵFactoryDeclaration<BreadcrumbPublisherService, never>;
23
+ static ɵprov: i0.ɵɵInjectableDeclaration<BreadcrumbPublisherService>;
24
+ }
@@ -0,0 +1,95 @@
1
+ import { Injectable, OnDestroy, effect, inject } from '@angular/core';
2
+ import { ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
3
+
4
+ import { Subscription, filter } from 'rxjs';
5
+
6
+ import { Breadcrumb, bridge } from 'aril/boot/bridge';
7
+ import { PubSubService } from 'aril/util/pub-sub';
8
+
9
+ import { BreadcrumbService } from './breadcrumb.service';
10
+
11
+ /**
12
+ * MFE-side route ağacını dolaşıp aktif route zincirinin breadcrumb listesini
13
+ * `bridge.breadcrumbs` signal'ine yazar. Shell layout bu signal'i okuyup
14
+ * breadcrumb şeridini render eder; presenter shell'de, source-of-truth MFE'de.
15
+ *
16
+ * Eski `<app-breadcrumb>` component'inin logic'i bu service'e taşınmıştır.
17
+ * Geriye dönük uyumluluk için `breadcrumbUpdated` PubSub event'i hâlâ yayınlanır
18
+ * (history.service abone).
19
+ */
20
+ @Injectable({ providedIn: 'root' })
21
+ export class BreadcrumbPublisherService implements OnDestroy {
22
+ private readonly router = inject(Router);
23
+ private readonly breadcrumbService = inject(BreadcrumbService);
24
+ private readonly pubSubService = inject(PubSubService);
25
+
26
+ private routerSubscription: Subscription;
27
+
28
+ constructor() {
29
+ this.routerSubscription = this.router.events
30
+ .pipe(filter((event) => event instanceof NavigationEnd))
31
+ .subscribe(() => this.publishFromCurrentRoute());
32
+
33
+ effect(() => {
34
+ // keyValues sinyalini dinle; dinamik breadcrumb (örn. "{customerId}")
35
+ // değerleri sonradan geldikçe listeyi yeniden yayınla.
36
+ this.breadcrumbService.keyValues();
37
+ this.publishFromCurrentRoute();
38
+ });
39
+ }
40
+
41
+ ngOnDestroy(): void {
42
+ this.routerSubscription?.unsubscribe();
43
+ }
44
+
45
+ private publishFromCurrentRoute(): void {
46
+ const root = this.router.routerState.snapshot.root;
47
+ const breadcrumbs: Breadcrumb[] = [];
48
+ this.collect(root, [], breadcrumbs);
49
+ this.publish(breadcrumbs);
50
+ }
51
+
52
+ private collect(route: ActivatedRouteSnapshot, parentUrl: string[], breadcrumbs: Breadcrumb[]): void {
53
+ const routeUrl = parentUrl.concat(route.url.map((url) => url.path));
54
+ const breadcrumb = route.data['breadcrumb'];
55
+ const parentBreadcrumb = route.parent && route.parent.data ? route.parent.data['breadcrumb'] : null;
56
+
57
+ if (breadcrumb && breadcrumb !== parentBreadcrumb) {
58
+ const template = breadcrumb as string;
59
+ // Tüm `{key}` placeholder'larını sırayla resolve et — global regex.
60
+ // Eskiden tek placeholder regex'i (`exec`) ile yalnız ilk key alınıyordu;
61
+ // `'Customer {customerId} - Service {servicePointId}'` formatında `servicePointId`
62
+ // literal kalıyordu. Resolve olmayan key için `'...'` placeholder döner; statik
63
+ // kısımlar korunur (önceden tüm label `'...'`'a düşüyordu).
64
+ const label = template.replace(/\{([^}]+)\}/g, (_, key: string) => {
65
+ const value = this.breadcrumbService.getValue(key);
66
+ return value !== undefined && value !== null && value !== '' ? String(value) : '...';
67
+ });
68
+ breadcrumbs.push({ label, url: '/' + routeUrl.join('/') });
69
+ }
70
+
71
+ if (route.firstChild) {
72
+ this.collect(route.firstChild, routeUrl, breadcrumbs);
73
+ }
74
+ }
75
+
76
+ private publish(breadcrumbs: Breadcrumb[]): void {
77
+ bridge.breadcrumbs.set(breadcrumbs);
78
+
79
+ const lastLabel = breadcrumbs[breadcrumbs.length - 1]?.label ?? '';
80
+ if (bridge.pageTitle() || lastLabel) {
81
+ bridge.pageTitle.set(lastLabel);
82
+ }
83
+
84
+ this.pubSubService.publish({
85
+ name: 'breadcrumbUpdated',
86
+ data: {
87
+ path: this.router.url,
88
+ custom: {
89
+ breadcrumbs,
90
+ pageTitle: lastLabel
91
+ }
92
+ }
93
+ });
94
+ }
95
+ }
@@ -0,0 +1,52 @@
1
+ import * as i0 from "@angular/core";
2
+ /**
3
+ * Sabitlenmiş (pinned) tab'ları kullanıcı bazlı `localStorage` anahtarında kalıcı
4
+ * kılar. F5 / yeniden login / yeni sekme açılışında pinned tab'lar geri yüklenir;
5
+ * non-pinned tab'lar saklanmaz.
6
+ *
7
+ * **Akış**: `AppLayoutComponent` constructor'da (menü/home provider'ları set
8
+ * edildikten SONRA) `restore()` SENKRON çağrılır → pinned'ler `NavService.activeRoutes`'a
9
+ * eklenir → save effect armlanır. Save effect `restore()` ÖNCESİ kurulmaz; aksi
10
+ * halde Angular effect'in ilk (eager) tick'i boş `activeRoutes`'u yazıp saklı
11
+ * pinned'leri silerdi.
12
+ *
13
+ * **Duplicate önlemi**: Aktif tab pinned'di ise restore SAKLI `tabId` ile ekler;
14
+ * F5 sonrası korunan `history.state._tab` bununla eşleşir → handler türetmez, sadece
15
+ * focus eder. Aktif tab pinned değildiyse eşleşme olmaz → handler gelen URL'i yeni
16
+ * aktif tab olarak ekler (deep-link gereksinimi: "kayıtlıları yükle + URL'i ekle").
17
+ */
18
+ export declare class TabSessionService {
19
+ private readonly navService;
20
+ private readonly keycloak;
21
+ private readonly injector;
22
+ private readonly STORAGE_PREFIX;
23
+ /** `NavService.MAX_TABS` ile aynı tavan — restore ve save iki tarafta da sınırlanır. */
24
+ private readonly MAX_PERSISTED;
25
+ private restored;
26
+ /** Son yazılan payload — değişmediyse gereksiz `localStorage` write'ı atlanır. */
27
+ private lastWritten;
28
+ /**
29
+ * Kullanıcı bazlı storage anahtarı. Mevcut kod (`app.profilesidebar.component.ts`)
30
+ * `idTokenParsed.sub` kullanıyor; her iki token şeklini de kapsamak için fallback
31
+ * zinciri — kullanıcı çözülemezse `anonymous`.
32
+ */
33
+ private storageKey;
34
+ /**
35
+ * SENKRON restore. `AppLayoutComponent` constructor'dan, ilk `NavigationEnd`
36
+ * işlenmeden ÖNCE çağrılmalı — böylece `NavService.activeRoute` handler'ının
37
+ * `knownTab` guard'ı restore edilen tabId'leri görür. Idempotent: ikinci çağrı no-op.
38
+ */
39
+ restore(): void;
40
+ /**
41
+ * Save effect'ini `restore()` SONRASI kurar. İlk çalıştığında `activeRoutes` zaten
42
+ * restore edilmiş pinned'leri içerdiği için yazım idempotent kalır. Effect, service'in
43
+ * kendi `Injector`'ına bağlanır (field initializer / constructor dışında kurulduğu için
44
+ * explicit injector zorunlu).
45
+ */
46
+ private armSave;
47
+ private save;
48
+ private load;
49
+ private isValidPersistedTab;
50
+ static ɵfac: i0.ɵɵFactoryDeclaration<TabSessionService, never>;
51
+ static ɵprov: i0.ɵɵInjectableDeclaration<TabSessionService>;
52
+ }
@@ -0,0 +1,138 @@
1
+ import { Injectable, Injector, effect, inject } from '@angular/core';
2
+
3
+ import { KeycloakService } from 'keycloak-angular';
4
+
5
+ import { NavItem, NavService } from 'aril/boot/config/apps';
6
+
7
+ /**
8
+ * localStorage'a serialize edilen pinned tab kaydı. `pinned` alanı SAKLANMAZ
9
+ * (yüklenen her kayıt zaten pinned'dir). `tabId` SAKLANIR — F5 sonrası
10
+ * `window.history.state._tab` ile eşleşip `NavService.activeRoute` handler'ının
11
+ * aynı tab'ı tekrar türetmesini (duplicate) engeller.
12
+ */
13
+ interface PersistedTab {
14
+ tabId: string;
15
+ navLink: string;
16
+ navName: string;
17
+ icon?: string;
18
+ }
19
+
20
+ /**
21
+ * Sabitlenmiş (pinned) tab'ları kullanıcı bazlı `localStorage` anahtarında kalıcı
22
+ * kılar. F5 / yeniden login / yeni sekme açılışında pinned tab'lar geri yüklenir;
23
+ * non-pinned tab'lar saklanmaz.
24
+ *
25
+ * **Akış**: `AppLayoutComponent` constructor'da (menü/home provider'ları set
26
+ * edildikten SONRA) `restore()` SENKRON çağrılır → pinned'ler `NavService.activeRoutes`'a
27
+ * eklenir → save effect armlanır. Save effect `restore()` ÖNCESİ kurulmaz; aksi
28
+ * halde Angular effect'in ilk (eager) tick'i boş `activeRoutes`'u yazıp saklı
29
+ * pinned'leri silerdi.
30
+ *
31
+ * **Duplicate önlemi**: Aktif tab pinned'di ise restore SAKLI `tabId` ile ekler;
32
+ * F5 sonrası korunan `history.state._tab` bununla eşleşir → handler türetmez, sadece
33
+ * focus eder. Aktif tab pinned değildiyse eşleşme olmaz → handler gelen URL'i yeni
34
+ * aktif tab olarak ekler (deep-link gereksinimi: "kayıtlıları yükle + URL'i ekle").
35
+ */
36
+ @Injectable({ providedIn: 'root' })
37
+ export class TabSessionService {
38
+ private readonly navService = inject(NavService);
39
+ private readonly keycloak = inject(KeycloakService);
40
+ private readonly injector = inject(Injector);
41
+
42
+ private readonly STORAGE_PREFIX = 'tab-session:';
43
+ /** `NavService.MAX_TABS` ile aynı tavan — restore ve save iki tarafta da sınırlanır. */
44
+ private readonly MAX_PERSISTED = 20;
45
+
46
+ private restored = false;
47
+ /** Son yazılan payload — değişmediyse gereksiz `localStorage` write'ı atlanır. */
48
+ private lastWritten = '';
49
+
50
+ /**
51
+ * Kullanıcı bazlı storage anahtarı. Mevcut kod (`app.profilesidebar.component.ts`)
52
+ * `idTokenParsed.sub` kullanıyor; her iki token şeklini de kapsamak için fallback
53
+ * zinciri — kullanıcı çözülemezse `anonymous`.
54
+ */
55
+ private storageKey(): string {
56
+ const kc = this.keycloak.getKeycloakInstance();
57
+ const sub = kc?.tokenParsed?.sub ?? kc?.idTokenParsed?.sub ?? 'anonymous';
58
+ return this.STORAGE_PREFIX + sub;
59
+ }
60
+
61
+ /**
62
+ * SENKRON restore. `AppLayoutComponent` constructor'dan, ilk `NavigationEnd`
63
+ * işlenmeden ÖNCE çağrılmalı — böylece `NavService.activeRoute` handler'ının
64
+ * `knownTab` guard'ı restore edilen tabId'leri görür. Idempotent: ikinci çağrı no-op.
65
+ */
66
+ restore(): void {
67
+ if (this.restored) return;
68
+ const persisted = this.load();
69
+ const existingIds = new Set(this.navService.activeRoutes().map((t) => t.tabId));
70
+ for (const item of persisted) {
71
+ // Aynı tabId zaten ekliyse atla (idempotency / olası çift çağrı koruması).
72
+ if (existingIds.has(item.tabId)) continue;
73
+ this.navService.addToActiveRoutes({
74
+ tabId: item.tabId,
75
+ navLink: item.navLink,
76
+ navName: item.navName,
77
+ icon: item.icon,
78
+ pinned: true
79
+ });
80
+ }
81
+ this.restored = true;
82
+ this.armSave();
83
+ }
84
+
85
+ /**
86
+ * Save effect'ini `restore()` SONRASI kurar. İlk çalıştığında `activeRoutes` zaten
87
+ * restore edilmiş pinned'leri içerdiği için yazım idempotent kalır. Effect, service'in
88
+ * kendi `Injector`'ına bağlanır (field initializer / constructor dışında kurulduğu için
89
+ * explicit injector zorunlu).
90
+ */
91
+ private armSave(): void {
92
+ effect(
93
+ () => {
94
+ const pinned = this.navService.activeRoutes().filter((t) => t.pinned);
95
+ this.save(pinned);
96
+ },
97
+ { injector: this.injector }
98
+ );
99
+ }
100
+
101
+ private save(pinned: NavItem[]): void {
102
+ try {
103
+ const payload: PersistedTab[] = pinned
104
+ .slice(0, this.MAX_PERSISTED)
105
+ .map((t) => ({ tabId: t.tabId, navLink: t.navLink, navName: t.navName, icon: t.icon }));
106
+ const serialized = JSON.stringify(payload);
107
+ if (serialized === this.lastWritten) return;
108
+ this.lastWritten = serialized;
109
+ localStorage.setItem(this.storageKey(), serialized);
110
+ } catch {
111
+ /* quota / serialize hatası: kritik değil, persist sessizce atlanır */
112
+ }
113
+ }
114
+
115
+ private load(): PersistedTab[] {
116
+ try {
117
+ const raw = localStorage.getItem(this.storageKey());
118
+ if (!raw) return [];
119
+ const parsed: unknown = JSON.parse(raw);
120
+ if (!Array.isArray(parsed)) return [];
121
+ return parsed.filter((x): x is PersistedTab => this.isValidPersistedTab(x));
122
+ } catch {
123
+ return []; // bozuk JSON → yok say, uygulama normal açılır
124
+ }
125
+ }
126
+
127
+ private isValidPersistedTab(x: unknown): x is PersistedTab {
128
+ if (!x || typeof x !== 'object') return false;
129
+ const t = x as Record<string, unknown>;
130
+ return (
131
+ typeof t['tabId'] === 'string' &&
132
+ t['tabId'].length > 0 &&
133
+ typeof t['navLink'] === 'string' &&
134
+ t['navLink'].length > 0 &&
135
+ typeof t['navName'] === 'string'
136
+ );
137
+ }
138
+ }