aril 1.2.18 → 2.0.1-dev.1

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 (122) 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 +22 -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-business/ref-value/src/ref-value.component.mjs +15 -7
  47. package/esm2022/util/sync-active-tab-route/src/sync-active-tab-route.directive.mjs +29 -9
  48. package/fesm2022/aril-app.component-s14ruALV.mjs +183 -0
  49. package/fesm2022/aril-app.component-s14ruALV.mjs.map +1 -0
  50. package/fesm2022/aril-boot-bridge.mjs +35 -4
  51. package/fesm2022/aril-boot-bridge.mjs.map +1 -1
  52. package/fesm2022/aril-boot-config-api.mjs +11 -2
  53. package/fesm2022/aril-boot-config-api.mjs.map +1 -1
  54. package/fesm2022/aril-boot-config-apps.mjs +1678 -10
  55. package/fesm2022/aril-boot-config-apps.mjs.map +1 -1
  56. package/fesm2022/aril-boot-config-plugins.mjs +12 -4
  57. package/fesm2022/aril-boot-config-plugins.mjs.map +1 -1
  58. package/fesm2022/aril-boot-host.mjs +21 -7
  59. package/fesm2022/aril-boot-host.mjs.map +1 -1
  60. package/fesm2022/aril-boot-mfe-app.component-a34GeuUv.mjs +183 -0
  61. package/fesm2022/aril-boot-mfe-app.component-a34GeuUv.mjs.map +1 -0
  62. package/fesm2022/aril-boot-mfe-aril-boot-mfe-KFO_X7yR.mjs +631 -0
  63. package/fesm2022/aril-boot-mfe-aril-boot-mfe-KFO_X7yR.mjs.map +1 -0
  64. package/fesm2022/aril-boot-mfe.mjs +5 -3
  65. package/fesm2022/aril-boot-mfe.mjs.map +1 -1
  66. package/fesm2022/aril-keycloak.mjs +16 -1
  67. package/fesm2022/aril-keycloak.mjs.map +1 -1
  68. package/fesm2022/aril-provider.mjs +90 -12
  69. package/fesm2022/aril-provider.mjs.map +1 -1
  70. package/fesm2022/aril-theme-layout.mjs +2628 -2017
  71. package/fesm2022/aril-theme-layout.mjs.map +1 -1
  72. package/fesm2022/aril-ui-business-ref-value.mjs +14 -6
  73. package/fesm2022/aril-ui-business-ref-value.mjs.map +1 -1
  74. package/fesm2022/aril-util-sync-active-tab-route.mjs +28 -8
  75. package/fesm2022/aril-util-sync-active-tab-route.mjs.map +1 -1
  76. package/fesm2022/aril.mjs +354 -25
  77. package/fesm2022/aril.mjs.map +1 -1
  78. package/keycloak/src/auth.interceptor.d.ts +7 -0
  79. package/package.json +222 -222
  80. package/provider/src/prodiveHost.d.ts +1 -0
  81. package/theme/layout/app/expandableMenu/expandable-menu.component.d.ts +21 -4
  82. package/theme/layout/app/expandableMenu/expandable-menu.component.html +19 -5
  83. package/theme/layout/app/expandableMenu/expandable-menu.component.ts +69 -9
  84. package/theme/layout/app/favorite-pages/favorite-pages-sidebar.component.html +1 -0
  85. package/theme/layout/app/favorite-pages/favorite-pages-sidebar.component.ts +3 -1
  86. package/theme/layout/app/general-search/general-search.component.html +2 -1
  87. package/theme/layout/app/general-search/general-search.component.ts +2 -2
  88. package/theme/layout/app/history/history-sidebar.component.html +3 -1
  89. package/theme/layout/app/history/history-sidebar.component.ts +3 -1
  90. package/theme/layout/app/layout/app.layout.component.d.ts +105 -5
  91. package/theme/layout/app/layout/app.layout.component.html +102 -1
  92. package/theme/layout/app/layout/app.layout.component.scss +372 -0
  93. package/theme/layout/app/layout/app.layout.component.ts +452 -13
  94. package/theme/layout/app/layout/mfe.layout.component.d.ts +1 -5
  95. package/theme/layout/app/layout/mfe.layout.component.ts +12 -38
  96. package/theme/layout/app/site-map/site-map-sidebar.component.html +1 -0
  97. package/theme/layout/app/site-map/site-map-sidebar.component.ts +3 -1
  98. package/theme/layout/app/static-sidebar/static-sidebar.component.d.ts +26 -5
  99. package/theme/layout/app/static-sidebar/static-sidebar.component.html +11 -5
  100. package/theme/layout/app/static-sidebar/static-sidebar.component.ts +68 -13
  101. package/theme/layout/app/topbar/app.topbar.component.html +0 -1
  102. package/theme/layout/app/topbar/app.topbar.component.scss +1 -1
  103. package/theme/layout/service/breadcrumb-publisher.service.d.ts +24 -0
  104. package/theme/layout/service/breadcrumb-publisher.service.ts +95 -0
  105. package/theme/layout/service/tab-session.service.d.ts +52 -0
  106. package/theme/layout/service/tab-session.service.ts +138 -0
  107. package/theme/styles/layout/_breadcrumb.scss +95 -0
  108. package/theme/styles/layout/_content.scss +2 -2
  109. package/ui-business/ref-value/src/ref-value.component.d.ts +4 -2
  110. package/util/sync-active-tab-route/src/sync-active-tab-route.directive.d.ts +15 -2
  111. package/boot/config/apps/src/reuse-strategy.d.ts +0 -4
  112. package/esm2022/boot/config/apps/src/reuse-strategy.mjs +0 -9
  113. package/esm2022/theme/layout/app/breadcrumb/app.breadcrumb.component.mjs +0 -107
  114. package/fesm2022/aril-app.component-wxP3y8dg.mjs +0 -81
  115. package/fesm2022/aril-app.component-wxP3y8dg.mjs.map +0 -1
  116. package/fesm2022/aril-boot-mfe-app.component-7IjAmjz0.mjs +0 -80
  117. package/fesm2022/aril-boot-mfe-app.component-7IjAmjz0.mjs.map +0 -1
  118. package/fesm2022/aril-boot-mfe-aril-boot-mfe-KXDpUyv7.mjs +0 -315
  119. package/fesm2022/aril-boot-mfe-aril-boot-mfe-KXDpUyv7.mjs.map +0 -1
  120. package/theme/layout/app/breadcrumb/app.breadcrumb.component.d.ts +0 -25
  121. package/theme/layout/app/breadcrumb/app.breadcrumb.component.html +0 -8
  122. package/theme/layout/app/breadcrumb/app.breadcrumb.component.ts +0 -127
@@ -1,25 +1,49 @@
1
+ import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
1
2
  import { NgClass } from '@angular/common';
2
- import { Component, OnDestroy, Renderer2, ViewChild } from '@angular/core';
3
- import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
3
+ import {
4
+ AfterViewInit,
5
+ Component,
6
+ ElementRef,
7
+ HostListener,
8
+ OnDestroy,
9
+ Renderer2,
10
+ ViewChild,
11
+ computed,
12
+ effect,
13
+ inject,
14
+ signal,
15
+ untracked
16
+ } from '@angular/core';
17
+ import { NavigationEnd, Router, RouterLink, RouterOutlet } from '@angular/router';
4
18
 
19
+ import { MenuItem } from 'primeng/api';
20
+ import { BreadcrumbModule } from 'primeng/breadcrumb';
5
21
  import { ConfirmDialogModule } from 'primeng/confirmdialog';
6
22
  import { ConfirmPopupModule } from 'primeng/confirmpopup';
23
+ import { ContextMenu, ContextMenuModule } from 'primeng/contextmenu';
7
24
  import { DialogModule } from 'primeng/dialog';
8
25
  import { MessagesModule } from 'primeng/messages';
9
26
  import { ToastModule } from 'primeng/toast';
27
+ import { TooltipModule } from 'primeng/tooltip';
10
28
 
29
+ import { TranslocoModule, TranslocoService } from '@ngneat/transloco';
11
30
  import { Subscription, filter } from 'rxjs';
12
31
 
32
+ import { bridge } from 'aril/boot/bridge';
33
+ import { NavItem, NavLinkContextMenuService, NavLinkDirective, NavService, PluginMenuItem } from 'aril/boot/config/apps';
34
+ import { TranslateJsonPipe } from 'aril/util/pipes';
35
+
13
36
  import { LayoutService } from '../../service/app.layout.service';
14
37
  import { AppMenuService } from '../../service/app.menu.service';
15
- import { AppProfileSidebarComponent } from '../profileSidebar/app.profilesidebar.component';
38
+ import { TabSessionService } from '../../service/tab-session.service';
39
+ import { ExpandableMenuComponent } from '../expandableMenu/expandable-menu.component';
40
+ import { FavoritePagesSidebarComponent } from '../favorite-pages/favorite-pages-sidebar.component';
16
41
  import { HistorySidebarComponent } from '../history/history-sidebar.component';
42
+ import { NotificationsSidebarComponent } from '../notifications/notifications-sidebar.component';
43
+ import { AppProfileSidebarComponent } from '../profileSidebar/app.profilesidebar.component';
17
44
  import { SiteMapSidebarComponent } from '../site-map/site-map-sidebar.component';
18
- import { FavoritePagesSidebarComponent } from '../favorite-pages/favorite-pages-sidebar.component';
19
- import { AppTopbarComponent } from '../topbar/app.topbar.component';
20
- import { ExpandableMenuComponent } from '../expandableMenu/expandable-menu.component';
21
45
  import { StaticSidebarComponent } from '../static-sidebar/static-sidebar.component';
22
- import { NotificationsSidebarComponent } from '../notifications/notifications-sidebar.component';
46
+ import { AppTopbarComponent } from '../topbar/app.topbar.component';
23
47
 
24
48
  @Component({
25
49
  standalone: true,
@@ -27,6 +51,7 @@ import { NotificationsSidebarComponent } from '../notifications/notifications-si
27
51
  imports: [
28
52
  NgClass,
29
53
  RouterOutlet,
54
+ RouterLink,
30
55
  ConfirmDialogModule,
31
56
  ConfirmPopupModule,
32
57
  DialogModule,
@@ -39,11 +64,19 @@ import { NotificationsSidebarComponent } from '../notifications/notifications-si
39
64
  FavoritePagesSidebarComponent,
40
65
  ExpandableMenuComponent,
41
66
  StaticSidebarComponent,
42
- NotificationsSidebarComponent
67
+ NotificationsSidebarComponent,
68
+ DragDropModule,
69
+ BreadcrumbModule,
70
+ ContextMenuModule,
71
+ TooltipModule,
72
+ TranslocoModule,
73
+ NavLinkDirective
43
74
  ],
44
- templateUrl: './app.layout.component.html'
75
+ templateUrl: './app.layout.component.html',
76
+ styleUrls: ['./app.layout.component.scss'],
77
+ providers: [TranslateJsonPipe]
45
78
  })
46
- export class AppLayoutComponent implements OnDestroy {
79
+ export class AppLayoutComponent implements OnDestroy, AfterViewInit {
47
80
  overlayMenuOpenSubscription: Subscription;
48
81
  menuOutsideClickListener: any;
49
82
  menuScrollListener: any;
@@ -51,13 +84,387 @@ export class AppLayoutComponent implements OnDestroy {
51
84
  @ViewChild(ExpandableMenuComponent) expandableMenuComponent!: ExpandableMenuComponent;
52
85
  @ViewChild(StaticSidebarComponent) staticSidebarComponent!: StaticSidebarComponent;
53
86
  @ViewChild(AppTopbarComponent) appTopbar!: AppTopbarComponent;
87
+ @ViewChild('tabsContainer', { static: false }) tabsContainer?: ElementRef<HTMLDivElement>;
88
+ @ViewChild('tabContextMenu') tabContextMenu?: ContextMenu;
89
+ @ViewChild('navLinkContextMenu') navLinkContextMenu?: ContextMenu;
90
+ navService: NavService = inject(NavService);
91
+ private readonly translocoService: TranslocoService = inject(TranslocoService);
92
+ private readonly navLinkContextMenuService: NavLinkContextMenuService = inject(NavLinkContextMenuService);
93
+ private readonly tabSession: TabSessionService = inject(TabSessionService);
94
+
95
+ tabContextMenuItems: MenuItem[] = [];
96
+ /** Tab bar yatay scroll konum/genişlik durumu. Chevron butonlarının görünürlüğünü kontrol eder. */
97
+ canScrollLeft = signal(false);
98
+ canScrollRight = signal(false);
99
+
100
+
101
+ /**
102
+ * Breadcrumb segmentleri — `bridge.breadcrumbs()` üzerinden okunur.
103
+ * Source-of-truth aktif MFE'de çalışan `BreadcrumbPublisherService`'tir;
104
+ * MFE her NavigationEnd'de kendi route ağacından zinciri çıkarıp bridge'e yazar.
105
+ * Shell yalnızca presenter — başa "Anasayfa" item'ı ekleyip son item'ı aktif işaretler.
106
+ */
107
+ breadcrumbItems = computed<(MenuItem & { active?: boolean; isHome?: boolean })[]>(() => {
108
+ const crumbs = bridge.breadcrumbs();
109
+ const homeItem: MenuItem & { isHome: boolean } = {
110
+ icon: 'pi pi-home',
111
+ isHome: true
112
+ };
113
+ if (!crumbs.length) {
114
+ return [homeItem];
115
+ }
116
+ return [
117
+ homeItem,
118
+ ...crumbs.map((c, idx) => {
119
+ const isLast = idx === crumbs.length - 1;
120
+ return {
121
+ label: c.label,
122
+ // Aktif (son) item'da url verme — PrimeNG dış `.p-menuitem-link`'e href eklemesin,
123
+ // kullanıcı zaten o sayfada, tekrar tıklanmasının anlamı yok.
124
+ url: isLast ? undefined : c.url,
125
+ active: isLast
126
+ };
127
+ })
128
+ ];
129
+ });
130
+
131
+ /**
132
+ * Aktif olmayan breadcrumb segmentine tıklama → mevcut tab'ı parent route ile günceller.
133
+ * `<a [routerLink]>` directive'i `href`'i Router'ın location strategy'sine göre
134
+ * (hash/path) doğru formatlar — sağ tık "linki kopyala" davranışı çalışır.
135
+ * Default Router navigate'ini `preventDefault()` ile durdurup `navigateInCurrentTab`
136
+ * üzerinden ilerletiyoruz; böylece yeni sekme açılmaz.
137
+ */
138
+ onBreadcrumbClick(event: MouseEvent, item: { label?: string; url?: string }): void {
139
+ event.preventDefault();
140
+ if (!item.url) return;
141
+ const navLink = item.url.startsWith('/') ? item.url.slice(1) : item.url;
142
+ this.navService.navigateInCurrentTab({
143
+ tabId: '',
144
+ navLink,
145
+ navName: item.label ?? ''
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Home icon → root menu item (`apps.service.ts:51`'de tanımlı `{ root: true, routerLink: '/' }`).
151
+ * Tek source-of-truth — etiket çevirisi `TranslateJsonPipe` ile aktif dilden okunur.
152
+ * Mevcut tab güncellenir, yeni sekme açılmaz.
153
+ */
154
+ onHomeClick(event: MouseEvent): void {
155
+ event.preventDefault();
156
+ this.navService.navigateInCurrentTab(this.homeNavItem());
157
+ }
158
+
159
+ /**
160
+ * Anasayfa için NavItem — breadcrumb home ikonunun hem `(click)` handler'ı hem
161
+ * `[arilNavLink]` context menu directive'i tarafından paylaşılır.
162
+ */
163
+ readonly homeNavItem = computed<NavItem>(() => {
164
+ // NavService ile AYNI menü kaynağı (`bridge.hostMenuItems`) — `menuService.menuItems()`
165
+ // host `root` öğesini içermiyor (farklı/boş kaynak); `setMenuItemsProvider` de bridge'i kullanıyor.
166
+ const items = bridge.hostMenuItems();
167
+ const rootItem = items.find((i: PluginMenuItem) => i.root);
168
+ // Pipe runtime'da object kabul ediyor (translate-json.pipe.ts:18) ama tip imzası `string` —
169
+ // static-sidebar'ın `getLocalText(any)` pattern'iyle aynı şekilde cast.
170
+ const navName = rootItem ? this.translateJsonPipe.transform(rootItem.label as any) : '';
171
+ return {
172
+ tabId: '',
173
+ navLink: rootItem?.routerLink || '/',
174
+ navName,
175
+ icon: rootItem?.icon
176
+ };
177
+ });
178
+
179
+ /**
180
+ * Tab icon resolution sırası:
181
+ * 1. NavItem.icon (sidebar click'inden iletilmişse — en doğru)
182
+ * 2. Menu config'inde navLink'i en uzun prefix olarak match eden item.icon
183
+ * 3. Generic fallback (`pi pi-file`)
184
+ *
185
+ * Best-match için longest-prefix kullanılır: `wdm/meters/123` için hem `wdm` hem
186
+ * `wdm/meters` match'lerse, daha spesifik `wdm/meters` kazanır. Böylece deep link
187
+ * refresh'inde (NavService.firstCheckForRoute path'inde icon set edilemediği zaman)
188
+ * doğru remote/section ikonu otomatik bulunur.
189
+ */
190
+ getTabIcon(item: NavItem): string {
191
+ if (item.icon) return item.icon;
192
+ if (!item.navLink) return 'pi pi-file';
193
+ // `bridge.hostMenuItems()` — homeNavItem/NavService ile aynı menü kaynağı. `menuService`
194
+ // host `root`/MFE menüsünü içermediği için (boş kaynak) buradaki ikon eşleşmesi de oradan.
195
+ return this.findIconForRoute(bridge.hostMenuItems(), item.navLink) ?? 'pi pi-file';
196
+ }
197
+
198
+ private findIconForRoute(items: PluginMenuItem[], navLink: string): string | undefined {
199
+ // Path leading slash içerebilir (Router URL), menu config routerLink'leri genelde
200
+ // leading slash'sız — ikisini normalize edip karşılaştır.
201
+ const normalize = (p: string): string => (p.startsWith('/') ? p.slice(1) : p);
202
+ const normalizedPath = normalize(navLink);
203
+ let best: { length: number; icon: string } | undefined;
204
+ const visit = (list: PluginMenuItem[]): void => {
205
+ for (const it of list) {
206
+ if (it.routerLink && it.icon) {
207
+ const r = normalize(it.routerLink);
208
+ if (r && (normalizedPath === r || normalizedPath.startsWith(r + '/'))) {
209
+ const length = r.length;
210
+ if (!best || length > best.length) best = { length, icon: it.icon };
211
+ }
212
+ }
213
+ if (it.items?.length) visit(it.items);
214
+ }
215
+ };
216
+ visit(items);
217
+ return best?.icon;
218
+ }
219
+
220
+ drop(event: CdkDragDrop<any[]>) {
221
+ const currentRoutes = [...this.navService.activeRoutes()];
222
+ moveItemInArray(currentRoutes, event.previousIndex, event.currentIndex);
223
+ this.navService.reorderActiveRoutes(currentRoutes);
224
+ }
225
+
226
+ isActive(item: NavItem): boolean {
227
+ // Aynı `navLink`'in birden fazla tab'i olabileceği için tabId tek doğru kimlik.
228
+ // `activeTabId` history.state üzerinden NavService tarafından senkronize edilir.
229
+ return this.navService.activeTabId() === item.tabId;
230
+ }
231
+
232
+ closeTab(event: Event, item: NavItem) {
233
+ event.stopPropagation();
234
+ if (item.pinned) return;
235
+ this.navService.closeNavigation(item);
236
+ }
237
+
238
+ /** Tab sol-click → o tab'a geç. NavService Router'a `?_tab` URL'i ve history state'i verir. */
239
+ onTabClick(event: MouseEvent, item: NavItem): void {
240
+ if (event.button !== 0) return; // sadece sol tık
241
+ this.navService.navigateToTab(item);
242
+ }
243
+
244
+ /** Middle click (button 1) → tab kapat (Chrome standardı). Sabit tab'lar kapatılmaz. */
245
+ onTabAuxClick(event: MouseEvent, item: NavItem): void {
246
+ if (event.button !== 1) return;
247
+ event.preventDefault();
248
+ if (item.pinned) return;
249
+ this.navService.closeNavigation(item);
250
+ }
251
+
252
+ /** Middle click default scroll davranışını engelle. */
253
+ onTabMousedown(event: MouseEvent): void {
254
+ if (event.button === 1) event.preventDefault();
255
+ }
54
256
 
257
+ /**
258
+ * Tab right-click → context menu. Sabit tab'larda "Kapat" disabled; pin/unpin item'ı
259
+ * Chrome'daki gibi en üstte yer alır. `closeOthers`/`closeToRight`/`closeAll` zaten
260
+ * NavService seviyesinde pinned'i koruyor — UI'da ekstra disabled gerekmez.
261
+ */
262
+ onTabContextMenu(event: MouseEvent, item: NavItem): void {
263
+ event.preventDefault();
264
+ const t = (key: string): string => this.translocoService.translate(key);
265
+ const pinLabel = item.pinned ? t('tab.unpin') : t('tab.pin');
266
+ this.tabContextMenuItems = [
267
+ { label: pinLabel, icon: 'pi pi-thumbtack', command: () => this.navService.togglePin(item) },
268
+ { label: t('tab.duplicate'), icon: 'pi pi-clone', command: () => this.navService.duplicateTab(item) },
269
+ { separator: true },
270
+ {
271
+ label: t('tab.close'),
272
+ icon: 'pi pi-times',
273
+ command: () => this.navService.closeNavigation(item),
274
+ disabled: !!item.pinned
275
+ },
276
+ {
277
+ label: t('tab.closeOthers'),
278
+ icon: 'pi pi-times-circle',
279
+ command: () => this.navService.closeOthers(item),
280
+ disabled: this.navService.activeRoutes().filter((r) => !r.pinned && r.tabId !== item.tabId).length === 0
281
+ },
282
+ {
283
+ label: t('tab.closeToRight'),
284
+ icon: 'pi pi-angle-double-right',
285
+ command: () => this.navService.closeToRight(item),
286
+ disabled: this.isLastTab(item)
287
+ },
288
+ { separator: true },
289
+ {
290
+ label: t('tab.closeAll'),
291
+ icon: 'pi pi-trash',
292
+ command: () => this.navService.closeAll(),
293
+ disabled: this.navService.activeRoutes().every((r) => r.pinned)
294
+ }
295
+ ];
296
+ this.tabContextMenu?.show(event);
297
+ }
298
+
299
+ private isLastTab(item: NavItem): boolean {
300
+ const routes = this.navService.activeRoutes();
301
+ // `tabId` ile karşılaştır — aynı navLink'in birden fazla tab'i olabileceği için
302
+ // `navLink` ambiguous olur, kimlik bazlı arama tek doğru yöntem.
303
+ return routes[routes.length - 1]?.tabId === item.tabId;
304
+ }
305
+
306
+ /**
307
+ * Tek kalan tab anasayfaysa true → kapatma (✕) butonu gizlenir (NavService de kapatmayı
308
+ * reddeder). "Her zaman en az 1 tab açık" kuralının UI tarafı.
309
+ */
310
+ isLastHomeTab(item: NavItem): boolean {
311
+ const routes = this.navService.activeRoutes();
312
+ return routes.length === 1 && item.navLink === (this.homeNavItem().navLink || '/');
313
+ }
314
+
315
+ /** "+" butonu → ana sayfaya navigate (yeni tab varsa onu aç, yoksa eklenir). */
316
+ onAddTab(): void {
317
+ this.router.navigateByUrl('');
318
+ }
319
+
320
+ scrollTabsLeft(): void {
321
+ this.tabsContainer?.nativeElement.scrollBy({ left: -200, behavior: 'smooth' });
322
+ }
323
+
324
+ scrollTabsRight(): void {
325
+ this.tabsContainer?.nativeElement.scrollBy({ left: 200, behavior: 'smooth' });
326
+ }
327
+
328
+ private updateScrollState(): void {
329
+ const el = this.tabsContainer?.nativeElement;
330
+ if (!el) return;
331
+ this.canScrollLeft.set(el.scrollLeft > 0);
332
+ this.canScrollRight.set(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
333
+ }
334
+
335
+ /**
336
+ * Klavye kısayolları (Chrome paritesi):
337
+ * - `Alt+W` → aktif tab kapat (Ctrl+W browser-reserved)
338
+ * - `Ctrl+Tab` / `Ctrl+Shift+Tab` → sonraki/önceki tab (denemeli — bazı tarayıcılar override etmez)
339
+ * - `Ctrl+PageDown` / `Ctrl+PageUp` → sonraki/önceki tab (yedek)
340
+ * - `Alt+1..9` → ilgili index'teki tab (Ctrl+1..9 browser-reserved)
341
+ * - `Ctrl+Shift+T` veya `Alt+Shift+T` → son kapatılan tab'ı geri aç
342
+ */
343
+ @HostListener('document:keydown', ['$event'])
344
+ onDocumentKeydown(event: KeyboardEvent): void {
345
+ if (this.isTypingInInput(event)) return;
346
+ const key = event.key;
347
+ const lower = key.toLowerCase();
348
+
349
+ if (event.altKey && !event.shiftKey && !event.ctrlKey && lower === 'w') {
350
+ const current = this.navService.activeRoute();
351
+ const item = current ? this.navService.activeRoutes().find((r) => r.navLink === current) : undefined;
352
+ if (item) {
353
+ event.preventDefault();
354
+ this.navService.closeNavigation(item);
355
+ }
356
+ return;
357
+ }
358
+
359
+ if (event.ctrlKey && (key === 'Tab' || key === 'PageDown' || key === 'PageUp')) {
360
+ event.preventDefault();
361
+ if (key === 'PageUp' || (key === 'Tab' && event.shiftKey)) {
362
+ this.navService.goToPrevTab();
363
+ } else {
364
+ this.navService.goToNextTab();
365
+ }
366
+ return;
367
+ }
368
+
369
+ if (event.altKey && !event.ctrlKey && !event.shiftKey && /^[1-9]$/.test(key)) {
370
+ event.preventDefault();
371
+ this.navService.goToTabByIndex(parseInt(key, 10) - 1);
372
+ return;
373
+ }
374
+ }
375
+
376
+ private isTypingInInput(event: KeyboardEvent): boolean {
377
+ const target = event.target as HTMLElement | null;
378
+ if (!target) return false;
379
+ const tag = target.tagName?.toLowerCase();
380
+ return tag === 'input' || tag === 'textarea' || tag === 'select' || !!target.isContentEditable;
381
+ }
382
+
383
+ ngAfterViewInit(): void {
384
+ this.updateScrollState();
385
+ const el = this.tabsContainer?.nativeElement;
386
+ if (el) {
387
+ el.addEventListener('scroll', () => this.updateScrollState(), { passive: true });
388
+ if (typeof ResizeObserver !== 'undefined') {
389
+ const ro = new ResizeObserver(() => this.updateScrollState());
390
+ ro.observe(el);
391
+ this.resizeObserver = ro;
392
+ }
393
+ }
394
+ // Global nav-link context menu'yu service'e tanıt — tüm `[arilNavLink]` directive'leri
395
+ // bu instance'ı paylaşır. ngAfterViewInit'te yapılır çünkü ViewChild burada kesin hazırdır.
396
+ if (this.navLinkContextMenu) {
397
+ this.navLinkContextMenuService.register(this.navLinkContextMenu);
398
+ }
399
+ }
400
+
401
+ private resizeObserver: ResizeObserver | null = null;
55
402
  constructor(
56
- private menuService: AppMenuService,
403
+ private readonly menuService: AppMenuService,
57
404
  public layoutService: LayoutService,
58
405
  public renderer: Renderer2,
59
- public router: Router
406
+ public router: Router,
407
+ private readonly translateJsonPipe: TranslateJsonPipe
60
408
  ) {
409
+ // NavService bridge'i doğrudan bilmez (cyclic dep önlemi); host shell `bridge.hostMenuItems()`
410
+ // üzerinden provider bağlar. Refresh sonrası adres çubuğuna yapıştırılmış URL'in tab adı/ikonu
411
+ // menüdeki gerçek değerlere göre çözülsün diye.
412
+ this.navService.setMenuItemsProvider(() => bridge.hostMenuItems());
413
+ this.navService.setHomeNavItemProvider(() => this.homeNavItem());
414
+
415
+ // Pinned tab'ları localStorage'dan geri yükle. Provider'lar set edildikten SONRA
416
+ // (restore edilen tab'lar reconcileTabsWithMenu ile menüden revize edilebilsin),
417
+ // ilk NavigationEnd handler'ından ÖNCE (senkron) — knownTab guard restore'u görsün.
418
+ this.tabSession.restore();
419
+
420
+ // Menü (bridge.hostMenuItems) async yüklenir; ilk açılış/refresh path'inde home tab
421
+ // fallback isim/iconla eklenmiş olabilir. Menü signal'i değişince tab'ları menüyle revize
422
+ // et → anasayfa adı/ikonu gerçek `root` öğesinden gelir, `tab.home` fallback'inde kalmaz.
423
+ effect(
424
+ () => {
425
+ bridge.hostMenuItems();
426
+ // `untracked`: reconcileTabsWithMenu içinde `activeRoutes()` okunuyor; reactive
427
+ // context'te kalırsa effect activeRoutes'a da abone olur → her tab mutasyonunda
428
+ // (aç/kapat/sürükle/pin) gereksiz menü-DFS çalışır. Yalnız hostMenuItems'a bağlı kalsın.
429
+ untracked(() => this.navService.reconcileTabsWithMenu());
430
+ },
431
+ { allowSignalWrites: true }
432
+ );
433
+
434
+ // NavService.activeTabId → bridge.activeTabId mirror. Plugin'lerin Router proxy'leri
435
+ // (provideHostRouter) bu signal'ı okuyup navigate çağrılarına _tab state enjekte ediyor.
436
+ effect(
437
+ () => {
438
+ bridge.activeTabId.set(this.navService.activeTabId());
439
+ },
440
+ { allowSignalWrites: true }
441
+ );
442
+
443
+ // MFE içi sayfa navigation'ları host Router'a yansımıyor (IsolatedLocationStrategy
444
+ // browser hash'ı değiştirmiyor) → NavService tab navLink'i güncel kalmıyordu. MFE
445
+ // AppComponent `bridge.activeMFEUrl`'a (kaynak tabId ile) yazıyor; bu effect ile
446
+ // dinleyip `syncActiveTabNavLinkForTab(tabId, url)` ile SADECE kaynak tab'ı update
447
+ // ederiz. Tab değiştirip geri dönüldüğünde doğru URL'e gidilir, cross-tab yok.
448
+ //
449
+ // **`untracked`**: `syncActiveTabNavLinkForTab` içinde `activeRoutes()` okunur;
450
+ // reactive context'te kalırsa effect activeRoutes'a da abone olur → tab.navLink
451
+ // değişikliği effect'i tekrar tetikler → eski `activeMFEUrl` değeriyle navLink'i
452
+ // geri çevirir (kullanıcının anasayfaya geçişi sürekli detail/meters'a dönerdi).
453
+ // Untracked sarması bu recursive loop'u kırar.
454
+ // `allowSignalWrites`: `activeRoutes.update` (signal write), effect içinden default
455
+ // `NG0600` yasaklı — bilinçli izin.
456
+ effect(
457
+ () => {
458
+ const data = bridge.activeMFEUrl();
459
+ if (data?.tabId && data?.url) {
460
+ untracked(() =>
461
+ this.navService.syncActiveTabNavLinkForTab(data.tabId, data.url, data.title)
462
+ );
463
+ }
464
+ },
465
+ { allowSignalWrites: true }
466
+ );
467
+
61
468
  this.overlayMenuOpenSubscription = this.layoutService.overlayOpen$.subscribe(() => {
62
469
  if (!this.menuOutsideClickListener) {
63
470
  this.menuOutsideClickListener = this.renderer.listen('document', 'click', (event) => {
@@ -78,9 +485,39 @@ export class AppLayoutComponent implements OnDestroy {
78
485
  }
79
486
  });
80
487
 
81
- this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe(() => {
488
+ this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe((event) => {
82
489
  if (!this.layoutService.mfeAppItemClicked()) this.hideMenu();
490
+ // Shell root route'a geçince (örn. `/`) MFE Router tetiklenmediği için
491
+ // `BreadcrumbPublisherService` set yapmaz, eski breadcrumb kalır. Manuel temizle.
492
+ // URL `?_tab=...` içerebileceği için query string'i sıyırıp path karşılaştır.
493
+ const url = (event as NavigationEnd).urlAfterRedirects;
494
+ const path = url.split('?')[0];
495
+ if (path === '/' || path === '') {
496
+ bridge.breadcrumbs.set([]);
497
+ }
498
+ });
499
+
500
+ // Tab listesi değiştiğinde scroll state'ini yeniden hesapla — DOM güncellemesinden sonra
501
+ // `queueMicrotask` ile bir sonraki frame'i bekliyoruz (yeni eklenen tab clientWidth/scrollWidth'i etkiler).
502
+ effect(() => {
503
+ this.navService.activeRoutes();
504
+ queueMicrotask(() => this.updateScrollState());
83
505
  });
506
+
507
+ // Tab geçişinde breadcrumb'ı geçici olarak boşalt — yeni aktif MFE'nin
508
+ // `BreadcrumbPublisherService`'i yayın yapana kadar eski tab'ın breadcrumb'ı
509
+ // kullanıcıya görünmesin (kısa süreli stale UI'yi engeller).
510
+ let prevTabId: string | undefined;
511
+ effect(
512
+ () => {
513
+ const tabId = this.navService.activeTabId();
514
+ if (prevTabId !== undefined && prevTabId !== tabId) {
515
+ bridge.breadcrumbs.set([]);
516
+ }
517
+ prevTabId = tabId;
518
+ },
519
+ { allowSignalWrites: true }
520
+ );
84
521
  }
85
522
 
86
523
  blockBodyScroll(): void {
@@ -165,5 +602,7 @@ export class AppLayoutComponent implements OnDestroy {
165
602
  if (this.menuOutsideClickListener) {
166
603
  this.menuOutsideClickListener();
167
604
  }
605
+
606
+ this.resizeObserver?.disconnect();
168
607
  }
169
608
  }
@@ -1,10 +1,6 @@
1
- import { Signal } from '@angular/core';
2
- import { Router } from '@angular/router';
3
1
  import * as i0 from "@angular/core";
4
2
  export declare class MFELayoutComponent {
5
- private router;
6
- loading: Signal<boolean>;
7
- constructor(router: Router);
3
+ constructor();
8
4
  static ɵfac: i0.ɵɵFactoryDeclaration<MFELayoutComponent, never>;
9
5
  static ɵcmp: i0.ɵɵComponentDeclaration<MFELayoutComponent, "mfe-layout", never, {}, {}, never, never, true, never>;
10
6
  }
@@ -1,13 +1,5 @@
1
- import { Component, Signal } from '@angular/core';
2
- import { toSignal } from '@angular/core/rxjs-interop';
3
- import {
4
- NavigationCancel,
5
- NavigationEnd,
6
- NavigationError,
7
- NavigationSkipped,
8
- Router,
9
- RouterOutlet
10
- } from '@angular/router';
1
+ import { Component, inject } from '@angular/core';
2
+ import { RouterOutlet } from '@angular/router';
11
3
 
12
4
  import { ConfirmDialogModule } from 'primeng/confirmdialog';
13
5
  import { ConfirmPopupModule } from 'primeng/confirmpopup';
@@ -15,17 +7,13 @@ import { DialogModule } from 'primeng/dialog';
15
7
  import { MessagesModule } from 'primeng/messages';
16
8
  import { ToastModule } from 'primeng/toast';
17
9
 
18
- import { map } from 'rxjs';
19
- import { AppBreadcrumbComponent } from '../breadcrumb/app.breadcrumb.component';
10
+ import { BreadcrumbPublisherService } from '../../service/breadcrumb-publisher.service';
20
11
 
21
12
  @Component({
22
13
  standalone: true,
23
14
  selector: 'mfe-layout',
24
15
  template: `
25
- <app-breadcrumb class="topbar-breadcrumb"></app-breadcrumb>
26
- @if (!loading()) {
27
- <router-outlet />
28
- }
16
+ <router-outlet />
29
17
 
30
18
  <p-toast key="toast-root"></p-toast>
31
19
  <p-dialog key="dialog-root"></p-dialog>
@@ -33,28 +21,14 @@ import { AppBreadcrumbComponent } from '../breadcrumb/app.breadcrumb.component';
33
21
  <p-confirmPopup key="confirmPopup-root"></p-confirmPopup>
34
22
  <p-confirmDialog key="confirmDialog-root"></p-confirmDialog>
35
23
  `,
36
- imports: [RouterOutlet, ConfirmDialogModule, ConfirmPopupModule, DialogModule, MessagesModule, ToastModule, AppBreadcrumbComponent]
24
+ imports: [RouterOutlet, ConfirmDialogModule, ConfirmPopupModule, DialogModule, MessagesModule, ToastModule]
37
25
  })
38
26
  export class MFELayoutComponent {
39
- loading: Signal<boolean>;
40
-
41
- constructor(private router: Router) {
42
- this.loading = toSignal(
43
- this.router.events.pipe(
44
- map((event) => {
45
- if (
46
- event instanceof NavigationEnd ||
47
- event instanceof NavigationCancel ||
48
- event instanceof NavigationError ||
49
- event instanceof NavigationSkipped
50
- )
51
- return false;
52
-
53
- return true;
54
- })
55
- ),
56
- { initialValue: true }
57
- );
27
+ constructor() {
28
+ // Side-effect injection — `BreadcrumbPublisherService` kendi constructor'ında Router
29
+ // subscription'ını kurar (breadcrumb yayınını başlatır). Field'a ATAMIYORUZ; DI'nın
30
+ // service'i instantiate etmesi yeterli. Field'a atasaydık `noUnusedLocals` (TS6133)
31
+ // strict tüketici build'lerinde (örn. yeap-mw) hata verirdi.
32
+ inject(BreadcrumbPublisherService);
58
33
  }
59
-
60
- }
34
+ }
@@ -89,6 +89,7 @@
89
89
  [class.has-children]="node.children && node.children.length > 0"
90
90
  [class.is-page]="node.routerLink"
91
91
  [routerLink]="node.routerLink"
92
+ [arilNavLink]="node.routerLink ? { tabId: '', navLink: node.routerLink, navName: (node.label | translateJson), icon: node.icon } : null"
92
93
  (click)="!node.routerLink ? toggleNode(node) : (visible = false)">
93
94
  <div class="node-content">
94
95
  @if (node.children && node.children.length > 0) {
@@ -10,6 +10,7 @@ import { TooltipModule } from 'primeng/tooltip';
10
10
  import { TranslocoModule, TranslocoService } from '@ngneat/transloco';
11
11
 
12
12
  import { bridge } from 'aril/boot/bridge';
13
+ import { NavLinkDirective } from 'aril/boot/config/apps';
13
14
  import { ButtonComponent } from 'aril/ui/button';
14
15
  import { TextComponent } from 'aril/ui/text';
15
16
  import { TranslateJsonPipe } from 'aril/util/pipes';
@@ -41,7 +42,8 @@ interface MenuNode {
41
42
  TextComponent,
42
43
  TranslocoModule,
43
44
  TranslateJsonPipe,
44
- FontAwesomeModule
45
+ FontAwesomeModule,
46
+ NavLinkDirective
45
47
  ],
46
48
  templateUrl: './site-map-sidebar.component.html',
47
49
  styleUrls: ['./site-map-sidebar.component.scss']