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
@@ -1,10 +1,16 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, Injectable, Renderer2, ElementRef, Component, ViewEncapsulation, ViewChild, isDevMode } from '@angular/core';
3
- import { Router, BaseRouteReuseStrategy } from '@angular/router';
2
+ import { inject, Injectable, ElementRef, Component, ViewChild, Input, Renderer2, ViewEncapsulation, isDevMode, signal, effect, DestroyRef, Directive, HostListener } from '@angular/core';
3
+ import * as i1 from '@angular/router';
4
+ import { Router, NavigationEnd, DefaultUrlSerializer } from '@angular/router';
4
5
  import { WebComponentWrapper, startsWith } from '@angular-architects/module-federation-tools';
5
6
  import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
6
7
  import { map, catchError, of } from 'rxjs';
7
8
  import { ForbiddenComponent, NotFoundComponent } from 'aril/boot/pages';
9
+ import { loadRemoteModule } from '@angular-architects/module-federation-runtime';
10
+ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
11
+ import { MessageService } from 'primeng/api';
12
+ import { TranslocoService } from '@ngneat/transloco';
13
+ import { filter, tap, map as map$1 } from 'rxjs/operators';
8
14
 
9
15
  const routePermissions = new Map();
10
16
  function setRoutePermissions(menuItems) {
@@ -52,6 +58,209 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.2", ngImpor
52
58
  args: [{ providedIn: 'root' }]
53
59
  }], ctorParameters: () => [] });
54
60
 
61
+ // eslint-disable-next-line @angular-eslint/component-class-suffix
62
+ class ArilWebComponentWrapper {
63
+ constructor(route) {
64
+ this.route = route;
65
+ this.isDestroyed = false;
66
+ // setupEvents tarafından bağlanan listener'ları izleriz — ngOnDestroy/yeniden bind sırasında
67
+ // doğru şekilde kaldırılmaları için.
68
+ this.registeredEvents = [];
69
+ // Route reuse stratejisi ile wrapper arasındaki sözleşme:
70
+ // - `needsRestore`: strategy retrieve sırasında set eder; ngAfterViewInit element'i view'a ekler.
71
+ // - `propsUpdated`: restore sonrası populateProps çağrısının ngOnChanges ile çakışmasını
72
+ // engellemek için kullanılır (idempotency flag).
73
+ this.needsRestore = false;
74
+ this.propsUpdated = false;
75
+ }
76
+ ngOnInit() {
77
+ // Component tekrar eklendiğinde (reuse durumunda)
78
+ // Element restore işlemini dene (view hazır olmayabilir, o durumda ngAfterViewInit'te tekrar denenecek)
79
+ if (this.element) {
80
+ console.log('[ArilWC] ngOnInit: Element mevcut, restore deneniyor...');
81
+ // setTimeout ile bir sonraki tick'te dene (view henüz hazır olmayabilir)
82
+ setTimeout(() => {
83
+ this.restoreElement();
84
+ }, 0);
85
+ }
86
+ }
87
+ ngOnChanges() {
88
+ if (!this.element || this.isDestroyed)
89
+ return;
90
+ this.populateProps();
91
+ }
92
+ populateProps() {
93
+ if (!this.element || !this.props)
94
+ return;
95
+ // Custom element'e dinamik prop set ediyoruz; HTMLElement bunun için index signature
96
+ // taşımıyor, bu yüzden Record cast'i ile yazıyoruz.
97
+ const target = this.element;
98
+ for (const prop in this.props) {
99
+ target[prop] = this.props[prop];
100
+ }
101
+ }
102
+ setupEvents() {
103
+ if (!this.element)
104
+ return;
105
+ // İdempotent: önce eski listener'ları kaldır, sonra yenilerini bağla.
106
+ this.teardownEvents();
107
+ if (!this.events)
108
+ return;
109
+ for (const event in this.events) {
110
+ const handler = this.events[event];
111
+ this.element.addEventListener(event, handler);
112
+ this.registeredEvents.push({ name: event, handler });
113
+ }
114
+ }
115
+ teardownEvents() {
116
+ if (!this.element) {
117
+ this.registeredEvents = [];
118
+ return;
119
+ }
120
+ for (const { name, handler } of this.registeredEvents) {
121
+ this.element.removeEventListener(name, handler);
122
+ }
123
+ this.registeredEvents = [];
124
+ }
125
+ restoreElement() {
126
+ if (!this.element) {
127
+ console.warn('restoreElement: element yok');
128
+ return;
129
+ }
130
+ if (!this.vc?.nativeElement) {
131
+ console.warn('restoreElement: vc.nativeElement yok, tekrar denenecek');
132
+ // View henüz hazır değilse, bir sonraki lifecycle hook'ta tekrar dene
133
+ return;
134
+ }
135
+ // Hidden container'dan element'i al
136
+ const hiddenContainer = this.getHiddenContainer();
137
+ if (hiddenContainer.contains(this.element)) {
138
+ // Element hidden container'da, geri al
139
+ hiddenContainer.removeChild(this.element);
140
+ this.vc.nativeElement.appendChild(this.element);
141
+ this.populateProps();
142
+ console.log('Web component hidden container\'dan geri alındı ve DOM\'a eklendi');
143
+ }
144
+ else if (!this.vc.nativeElement.contains(this.element)) {
145
+ // Element başka bir yerdeyse veya hiçbir yerde değilse, direkt ekle
146
+ if (this.element.parentNode) {
147
+ this.element.parentNode.removeChild(this.element);
148
+ }
149
+ this.vc.nativeElement.appendChild(this.element);
150
+ this.populateProps();
151
+ console.log('Web component DOM\'a geri eklendi');
152
+ }
153
+ else {
154
+ // Element zaten DOM'da, sadece props'ları güncelle
155
+ this.populateProps();
156
+ console.log('Web component zaten DOM\'da, props güncellendi');
157
+ }
158
+ }
159
+ getHiddenContainer() {
160
+ const slot = globalThis;
161
+ let container = slot.__arilWebComponentHiddenContainer;
162
+ if (!container) {
163
+ container = document.createElement('div');
164
+ container.style.display = 'none';
165
+ container.style.position = 'absolute';
166
+ container.style.left = '-9999px';
167
+ container.style.top = '-9999px';
168
+ container.style.visibility = 'hidden';
169
+ document.body.appendChild(container);
170
+ slot.__arilWebComponentHiddenContainer = container;
171
+ }
172
+ return container;
173
+ }
174
+ async ngAfterContentInit() {
175
+ const options = this.options ?? this.route.snapshot.data;
176
+ if (!options) {
177
+ console.warn('Web component options bulunamadı');
178
+ return;
179
+ }
180
+ // Eğer element zaten varsa (reuse durumunda), restore işlemi ngAfterViewInit'te yapılacak
181
+ if (this.element) {
182
+ return;
183
+ }
184
+ try {
185
+ await loadRemoteModule(options);
186
+ this.element = document.createElement(options.elementName);
187
+ // Tab izolasyonu için tab kimliğini element'e attribute olarak yaz. MFE bootstrap.ts
188
+ // `connectedCallback`'te bu attribute'u okuyup kendine ait yeni bir Angular Application
189
+ // yaratır → her tab kendi Injector + Router'ına sahip olur. Subscribe ile sonraki
190
+ // `_tab` değişimlerini de yakala (firstCheckForRoute replaceUrl sonrası).
191
+ this.routeSub = this.route.queryParams.subscribe((params) => {
192
+ const tabId = params['_tab'] ?? '';
193
+ if (tabId && this.element && this.element.getAttribute('aril-tab-id') !== tabId) {
194
+ this.element.setAttribute('aril-tab-id', tabId);
195
+ }
196
+ });
197
+ this.populateProps();
198
+ this.setupEvents();
199
+ if (this.vc?.nativeElement) {
200
+ this.vc.nativeElement.appendChild(this.element);
201
+ }
202
+ }
203
+ catch (error) {
204
+ console.error('Web component yüklenirken hata:', error);
205
+ }
206
+ }
207
+ ngAfterViewInit() {
208
+ // View hazır olduğunda, eğer element varsa (reuse durumunda) restore et
209
+ if (this.element) {
210
+ console.log('ngAfterViewInit: Element mevcut, restore deneniyor...');
211
+ // Strategy `needsRestore` flag'i set etmişse: element hidden container'dan çıkarılmış
212
+ // ama view henüz hazır olmadığı için view'a eklenememiş. Şimdi ekle.
213
+ if (this.needsRestore) {
214
+ this.needsRestore = false;
215
+ if (this.vc?.nativeElement && !this.vc.nativeElement.contains(this.element)) {
216
+ this.vc.nativeElement.appendChild(this.element);
217
+ this.populateProps();
218
+ this.propsUpdated = true;
219
+ console.log('Web component element component view\'a eklendi (ngAfterViewInit)');
220
+ }
221
+ }
222
+ else if (!this.propsUpdated) {
223
+ // Normal restore işlemi (eğer props güncellenmemişse)
224
+ this.restoreElement();
225
+ this.propsUpdated = true;
226
+ }
227
+ else {
228
+ // Element zaten view'da, sadece props'ları güncelle
229
+ this.populateProps();
230
+ }
231
+ }
232
+ }
233
+ ngOnDestroy() {
234
+ // Route reuse stratejisi kullanıldığında, component destroy edilmez
235
+ // Route reuse stratejisi store() metodunda element'i hidden container'a taşıyacak.
236
+ // Yine de gerçekten destroy edildiğimiz senaryolarda (farklı remote, tab cleanup vs.)
237
+ // event listener'ları + queryParams subscription sızdırmamak için kaldırırız.
238
+ this.teardownEvents();
239
+ this.routeSub?.unsubscribe();
240
+ this.routeSub = undefined;
241
+ this.isDestroyed = true;
242
+ }
243
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: ArilWebComponentWrapper, deps: [{ token: i1.ActivatedRoute }], target: i0.ɵɵFactoryTarget.Component }); }
244
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.1.2", type: ArilWebComponentWrapper, selector: "mft-wc-wrapper", inputs: { options: "options", props: "props", events: "events" }, viewQueries: [{ propertyName: "vc", first: true, predicate: ["vc"], descendants: true, read: ElementRef, static: true }], usesOnChanges: true, ngImport: i0, template: '<div #vc></div>', isInline: true }); }
245
+ }
246
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: ArilWebComponentWrapper, decorators: [{
247
+ type: Component,
248
+ args: [{
249
+ // eslint-disable-next-line @angular-eslint/component-selector
250
+ selector: 'mft-wc-wrapper',
251
+ template: '<div #vc></div>',
252
+ }]
253
+ }], ctorParameters: () => [{ type: i1.ActivatedRoute }], propDecorators: { vc: [{
254
+ type: ViewChild,
255
+ args: ['vc', { read: ElementRef, static: true }]
256
+ }], options: [{
257
+ type: Input
258
+ }], props: [{
259
+ type: Input
260
+ }], events: [{
261
+ type: Input
262
+ }] } });
263
+
55
264
  class ShadowDOMWrapperComponent extends WebComponentWrapper {
56
265
  constructor() {
57
266
  super(...arguments);
@@ -117,11 +326,18 @@ class MicroAppService {
117
326
  }));
118
327
  }
119
328
  buildRoutes(options) {
120
- const routes = options.map((options) => {
329
+ const routes = options.map((opt) => {
330
+ if (!opt.remoteName) {
331
+ // `remoteName` boşsa Strategy `getRouteKey` doğru tab key üretemiyor → tab izolasyonu
332
+ // silent kırılıyordu. Fail-fast: config hatasını boot-time'da yakala, runtime'da
333
+ // kullanıcı boş tab'lar görmesin.
334
+ throw new Error(`[MicroAppService.buildRoutes] PluginConfig.remoteName eksik. ` +
335
+ `Route oluşturulamaz — config kontrol et: ${JSON.stringify(opt)}`);
336
+ }
121
337
  return {
122
- matcher: startsWith(options.remoteName),
123
- component: options.shadowDomPassive ? WebComponentWrapper : ShadowDOMWrapperComponent,
124
- data: options,
338
+ matcher: startsWith(opt.remoteName),
339
+ component: opt.shadowDomPassive ? ArilWebComponentWrapper : ShadowDOMWrapperComponent,
340
+ data: opt,
125
341
  canActivate: [AuthGuard]
126
342
  };
127
343
  });
@@ -178,17 +394,1469 @@ var Apps;
178
394
  Apps["DMS"] = "dms";
179
395
  })(Apps || (Apps = {}));
180
396
 
181
- class ArilReuseStrategy extends BaseRouteReuseStrategy {
397
+ class RouteCloseService {
398
+ constructor() {
399
+ this.deleteRoute = signal({ key: null });
400
+ }
401
+ deleteStoredRoute(key) {
402
+ this.deleteRoute.update(() => ({ key }));
403
+ }
404
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: RouteCloseService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
405
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: RouteCloseService, providedIn: 'root' }); }
406
+ }
407
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: RouteCloseService, decorators: [{
408
+ type: Injectable,
409
+ args: [{
410
+ providedIn: 'root',
411
+ }]
412
+ }] });
413
+
414
+ /**
415
+ * Tab-bazlı navigation için custom `RouteReuseStrategy`.
416
+ *
417
+ * ## Tab izolasyonu (`_tab` query param)
418
+ * `NavService` her tab'a unique `tabId` üretir ve URL'e `?_tab=<tabId>` query param
419
+ * ekler. Strategy bu param'ı route key'in parçası yapar:
420
+ * - Matcher (web component) route'lar: `mfe:${remoteName}:${tabId}`
421
+ * - Normal route'lar: `pathFromRoot` + tüm queryParams (zaten `_tab` dahil)
422
+ *
423
+ * Sonuç: aynı sayfanın iki tab'i `storedRoutes` üzerinde farklı slot'lara yazılır
424
+ * → bağımsız componentRef'ler, bağımsız state. `shouldReuseRoute` matcher karşılaştırması
425
+ * `tabId` farkını da dikkate alır — tab değişiminde fresh component oluşur.
426
+ *
427
+ * ## Mimari: iki bağımsız instance
428
+ * Bu strategy hem `aril/boot/host/src/bootstrap.ts`'te hem her MFE remote'unun
429
+ * `aril/boot/mfe/src/bootstrap.ts`'inde provider olarak veriliyor. Class
430
+ * `providedIn: 'root'` olduğu için **her root injector'da AYRI instance** yaşar.
431
+ * İki instance bağımsız `storedRoutes` map'i tutar ve birbirinden habersizdir:
432
+ *
433
+ * - **Host instance**: matcher route'ları yönetir; key `mfe:${remoteName}:${tabId}`.
434
+ * Tab kapanıp `mfe:wdm:abc123` silindiğinde effect remote'a ait `#0/wdm/...`
435
+ * prefix'li VE `_tab=abc123` içeren child key'leri de temizler.
436
+ * - **MFE instance** (her remote'un kendi içinde): normal route'lar için çalışır.
437
+ * Query params zaten `getRouteKey` çıktısına dahil olduğu için tab izolasyonu
438
+ * doğal olarak gelir — ek özel davranış gerekmez.
439
+ *
440
+ * ## Plugin custom element preservation
441
+ * `preserveCustomElementsInTree` detached view'daki `<md-search>` gibi custom
442
+ * element'leri hidden container'a taşır — element document'tan ayrılmaz, Angular
443
+ * Elements `disconnectedCallback` 10ms destroy timer'ı tetiklenmez, plugin
444
+ * componentRef'i ve dahili state'i (form input, search sonuçları) korunur.
445
+ *
446
+ * ## Bilinen sınırlamalar
447
+ * - `shouldDetach` matcher route'lar için tabId yokken false döner (tabsız orphan
448
+ * handle oluşmasın), diğer durumlarda `true` — tüm detached route'lar cache'lenir.
449
+ * Memory leak'i sınırlamak için `NavService` tab sayısını `MAX_TABS` ile kısıtlar
450
+ * ve son tab kapatıldığında effect remote'un tüm child key'lerini temizler. Yine de
451
+ * tab içinde açılan detail/edit gibi parametreli alt sayfaların handle'ları, parent
452
+ * tab kapanana kadar storedRoutes'ta kalır.
453
+ */
454
+ class CustomRouteReuseStrategy {
455
+ constructor() {
456
+ this.routeCloseService = inject(RouteCloseService);
457
+ this.storedRoutes = {};
458
+ /**
459
+ * `(detachedTree as any).__arilPreservedCustomEls = [...]` runtime property
460
+ * eklemek yerine handle'ı key olarak kullanan bir WeakMap tutuyoruz —
461
+ * type-safe ve handle GC'ye uğradığında bellek otomatik temizlenir.
462
+ */
463
+ this.preservedCustomEls = new WeakMap();
464
+ effect(() => {
465
+ const key = this.routeCloseService.deleteRoute().key;
466
+ if (!key)
467
+ return;
468
+ console.log(`[Strategy] cleanup tetiklendi key="${key}" storedKeys=[${Object.keys(this.storedRoutes).join(',')}]`);
469
+ this.processCleanupKey(key);
470
+ // Cross-injector yayın: `RouteCloseService` `providedIn: 'root'` olduğu için host ve
471
+ // her MFE remote'unda AYRI instance yaşar. Host'tan tetiklenen cleanup MFE strategy'sini
472
+ // otomatik uyandırmaz. Window event ile sinyal yayınlayıp MFE strategy'sinin kendi
473
+ // `storedRoutes`'unu temizlemesini sağlıyoruz (aksi takdirde MFE'de hidden container'a
474
+ // taşınmış plugin element'leri orphan kalır).
475
+ if (key.startsWith('mfe:')) {
476
+ const parsed = this.parseMatcherKey(key);
477
+ if (parsed) {
478
+ window.dispatchEvent(new CustomEvent('aril:remote-cleanup', {
479
+ detail: { remoteName: parsed.remoteName, tabId: parsed.tabId }
480
+ }));
481
+ }
482
+ }
483
+ });
484
+ // Karşı yönde (cross-injector): host strategy'nin yayınladığı `aril:remote-cleanup`
485
+ // event'ini her MFE strategy instance'ı yakalar, kendi `storedRoutes`'undaki o remote'a
486
+ // (ve varsa o tabId'ye) ait key'leri temizler. Host strategy de bu listener'a sahip
487
+ // ama kendi `#0/${remoteName}/...` key'leri olmadığı için no-op çalışır (güvenli).
488
+ const cleanupHandler = (e) => {
489
+ const detail = e.detail;
490
+ const remoteName = detail?.remoteName;
491
+ if (!remoteName)
492
+ return;
493
+ const tabId = detail?.tabId ?? null;
494
+ const prefix = `#0/${remoteName}`;
495
+ const keys = Object.keys(this.storedRoutes).filter((k) => {
496
+ if (!k.startsWith(prefix))
497
+ return false;
498
+ // tabId verildiyse SADECE o tab'a ait child handle'ları temizle. Aksi takdirde
499
+ // aynı remote'un başka tab'lerinin state'i de yanlışlıkla silinir.
500
+ if (tabId && !k.includes(`_tab=${tabId}`))
501
+ return false;
502
+ return true;
503
+ });
504
+ if (!keys.length)
505
+ return;
506
+ console.log(`[Strategy] cross-injector cleanup remoteName="${remoteName}" tabId="${tabId ?? '*'}" keys=[${keys.join(',')}]`);
507
+ for (const k of keys) {
508
+ this.cleanupStoredHandle(this.storedRoutes[k]);
509
+ delete this.storedRoutes[k];
510
+ }
511
+ };
512
+ window.addEventListener('aril:remote-cleanup', cleanupHandler);
513
+ // Strategy `providedIn: 'root'` → root injector destroy edildiğinde listener'ı kaldır.
514
+ // Pratikte uzun ömürlü ama test ortamı ve gelecekteki iframe vb. multi-window
515
+ // senaryolarda orphan listener bırakmamak için defensive.
516
+ inject(DestroyRef).onDestroy(() => window.removeEventListener('aril:remote-cleanup', cleanupHandler));
517
+ }
518
+ /**
519
+ * Injector destroy edildiğinde — hibrit modda her tab'ın child `EnvironmentInjector`'ı tab
520
+ * kapanınca `destroy()` edilir; bu strategy `providedIn:'root'` olduğundan child injector'da
521
+ * (root-scope) materialize olmuş instance da yıkılır — `storedRoutes`'ta kalan TÜM detached
522
+ * handle'ları temizler. Aksi halde `preserveCustomElementsInTree` ile hidden container'a
523
+ * taşınmış plugin custom element'leri (örn. `<md-search>`) orphan kalır (memory leak): MFE
524
+ * internal route key'leri `_tab` taşımadığı için `aril:remote-cleanup` cross-injector temizliği
525
+ * onlara ulaşamaz. Injector-destroy bazlı bu süpürme garantili ve idempotenttir.
526
+ */
527
+ ngOnDestroy() {
528
+ const keys = Object.keys(this.storedRoutes);
529
+ if (keys.length)
530
+ console.log(`[Strategy] ngOnDestroy → ${keys.length} stored handle temizleniyor (tab kapandı)`);
531
+ for (const key of keys) {
532
+ this.cleanupStoredHandle(this.storedRoutes[key]);
533
+ delete this.storedRoutes[key];
534
+ }
535
+ }
536
+ /**
537
+ * Verilen key için direct + child cleanup uygular. Effect içinden ayrıştırılmış helper.
538
+ */
539
+ processCleanupKey(key) {
540
+ // Doğrudan eşleşen key varsa onu sil.
541
+ if (this.storedRoutes[key]) {
542
+ console.log(`[Strategy] cleanup → direct match "${key}"`);
543
+ this.cleanupStoredHandle(this.storedRoutes[key]);
544
+ delete this.storedRoutes[key];
545
+ }
546
+ // Matcher key silindiğinde (`mfe:wdm:abc` gibi) o remote'a ait — varsa o tab'a ait —
547
+ // internal route handle'larını da temizle. Convention: matcher key
548
+ // `mfe:${remoteName}` veya `mfe:${remoteName}:${tabId}`, normal key'ler
549
+ // `#0/${remoteName}/...?...&_tab=${tabId}` formatında.
550
+ if (key.startsWith('mfe:')) {
551
+ const parsed = this.parseMatcherKey(key);
552
+ if (!parsed)
553
+ return;
554
+ const { remoteName, tabId } = parsed;
555
+ const prefix = `#0/${remoteName}`;
556
+ for (const k of Object.keys(this.storedRoutes)) {
557
+ if (!k.startsWith(prefix))
558
+ continue;
559
+ // tabId verildiyse sadece o tab'a ait child handle'ları temizle
560
+ if (tabId && !k.includes(`_tab=${tabId}`))
561
+ continue;
562
+ console.log(`[Strategy] cleanup → child match "${k}"`);
563
+ this.cleanupStoredHandle(this.storedRoutes[k]);
564
+ delete this.storedRoutes[k];
565
+ }
566
+ }
567
+ }
568
+ /**
569
+ * `mfe:${remoteName}` veya `mfe:${remoteName}:${tabId}` formatındaki matcher key'i parse eder.
570
+ * tabId yoksa `null` döner (geriye dönük uyumluluk: legacy çağrılar tabId vermeyebilir).
571
+ */
572
+ parseMatcherKey(key) {
573
+ if (!key.startsWith('mfe:'))
574
+ return null;
575
+ const tail = key.substring(4);
576
+ const colonIdx = tail.indexOf(':');
577
+ return {
578
+ remoteName: colonIdx === -1 ? tail : tail.substring(0, colonIdx),
579
+ tabId: colonIdx === -1 ? null : tail.substring(colonIdx + 1)
580
+ };
581
+ }
582
+ /**
583
+ * Stored handle'ı tamamen temizle: hidden container'daki preserved element'leri (plugin
584
+ * custom element'leri + ArilWebComponentWrapper'ın `<app-xxx>` element'i) DOM'dan kaldır,
585
+ * sonra Angular `componentRef.destroy()` ile view tree'sini destroy et.
586
+ *
587
+ * `componentRef.destroy()` tek başına yeterli değil: Angular sadece view tree'sini bilir,
588
+ * bizim `preserveCustomElementsInTree` ve `preserveWebComponentElement` ile hidden
589
+ * container'a manuel taşınan element'lerden haberi yoktur → onlar orphan kalır.
590
+ */
591
+ cleanupStoredHandle(handle) {
592
+ if (!handle)
593
+ return; // Angular Router'ın `store(route, null)` ile bıraktığı orphan key'ler için defensive guard.
594
+ const hiddenContainer = this.getHiddenContainer();
595
+ // 1. preserveCustomElementsInTree ile taşınan plugin element'leri (örn. <md-search>)
596
+ const preserved = this.preservedCustomEls.get(handle);
597
+ if (preserved) {
598
+ for (const { el } of preserved) {
599
+ if (hiddenContainer.contains(el)) {
600
+ hiddenContainer.removeChild(el);
601
+ }
602
+ }
603
+ this.preservedCustomEls.delete(handle);
604
+ }
605
+ // 2. preserveWebComponentElement ile taşınan wrapper element'i (örn. <app-wdm>)
606
+ const componentRef = handle.componentRef;
607
+ const instance = componentRef?.instance;
608
+ if (instance?.element) {
609
+ // Flag'i temizle ki removeChild sonrası tetiklenen disconnectedCallback Application'ı
610
+ // gerçekten destroy etsin — aksi halde MFEAppElement orphan kalır (memory leak).
611
+ delete instance.element.__arilPreserveDuringDetach;
612
+ if (hiddenContainer.contains(instance.element)) {
613
+ hiddenContainer.removeChild(instance.element);
614
+ }
615
+ }
616
+ // 3. Angular view tree'sini destroy et
617
+ componentRef?.destroy();
618
+ }
619
+ /**
620
+ * Matcher (MFE) route'lar için tabId zorunlu. `getTabIdFromRoute` URL queryParams +
621
+ * history.state'i okur. Hiçbir kaynak tabId vermezse (ilk navigation interim'i) detach
622
+ * edilmez; `firstCheckForRoute` `_tab` ekleyip yeniden navigate edecek.
623
+ */
624
+ shouldDetach(route) {
625
+ if (this.isMatcherRoute(route) && !this.getTabIdFromRoute(route)) {
626
+ console.log(`[Strategy] shouldDetach key="${this.getRouteKey(route)}" → false (matcher route, tabId yok)`);
627
+ return false;
628
+ }
629
+ const routeKey = this.getRouteKey(route);
630
+ console.log(`[Strategy] shouldDetach key="${routeKey}" → true`);
631
+ return true;
632
+ }
633
+ store(route, detachedTree) {
634
+ const routeKey = this.getRouteKey(route);
635
+ console.log(`[Strategy] store key="${routeKey}" handle=`, !!detachedTree);
636
+ if (!detachedTree) {
637
+ delete this.storedRoutes[routeKey];
638
+ return;
639
+ }
640
+ if (this.isMatcherRoute(route) && !this.getTabIdFromRoute(route)) {
641
+ console.log(`[Strategy] store SKIP key="${routeKey}" (matcher route, tabId yok)`);
642
+ return;
643
+ }
644
+ this.storedRoutes[routeKey] = detachedTree;
645
+ this.preserveWebComponentElement(detachedTree);
646
+ this.preserveCustomElementsInTree(detachedTree);
647
+ }
648
+ shouldAttach(route) {
649
+ if (this.isMatcherRoute(route) && !this.getTabIdFromRoute(route)) {
650
+ console.log(`[Strategy] shouldAttach key="${this.getRouteKey(route)}" → false (matcher route, tabId yok)`);
651
+ return false;
652
+ }
653
+ const routeKey = this.getRouteKey(route);
654
+ const shouldAttach = !!this.storedRoutes[routeKey];
655
+ console.log(`[Strategy] shouldAttach key="${routeKey}" → ${shouldAttach} (storedKeys=[${Object.keys(this.storedRoutes).join(',')}])`);
656
+ return shouldAttach;
657
+ }
658
+ retrieve(route) {
659
+ if (this.isMatcherRoute(route) && !this.getTabIdFromRoute(route)) {
660
+ return null;
661
+ }
662
+ const routeKey = this.getRouteKey(route);
663
+ const handle = this.storedRoutes[routeKey];
664
+ console.log(`[Strategy] retrieve key="${routeKey}" → ${handle ? 'HAS' : 'NULL'}`);
665
+ if (handle) {
666
+ this.restoreWebComponentElement(handle);
667
+ this.restoreCustomElementsInTree(handle);
668
+ }
669
+ return handle || null;
670
+ }
671
+ /**
672
+ * Web component element'ini hidden container'a taşır (state korunması için).
673
+ *
674
+ * Element'e `__arilPreserveDuringDetach` flag'i yazılır: `removeChild` + `appendChild`
675
+ * arasında MFE custom element'in `disconnectedCallback`'i tetikleniyor; flag oradaki
676
+ * destroy timer'ını **deterministic** şekilde bypass eder. Eskiden 50ms hardcoded timer
677
+ * yarış koşulu üretiyordu (yavaş cihazlarda Application istemsiz destroy oluyordu).
678
+ */
679
+ preserveWebComponentElement(detachedTree) {
680
+ try {
681
+ const componentRef = detachedTree.componentRef;
682
+ if (!componentRef)
683
+ return;
684
+ const componentInstance = componentRef.instance;
685
+ if (!componentInstance?.element)
686
+ return;
687
+ const element = componentInstance.element;
688
+ const hiddenContainer = this.getHiddenContainer();
689
+ // Flag — disconnectedCallback bunu okuyup destroy'ı atlayacak. removeChild'tan
690
+ // ÖNCE set ediliyor ki callback ilk tetiklendiğinde flag yerinde olsun.
691
+ element.__arilPreserveDuringDetach = true;
692
+ // Eğer element zaten bir parent'a sahipse, ondan ayır
693
+ if (element.parentNode && element.parentNode !== hiddenContainer) {
694
+ element.parentNode.removeChild(element);
695
+ }
696
+ // Hidden container'a ekle (eğer zaten orada değilse)
697
+ if (!hiddenContainer.contains(element)) {
698
+ hiddenContainer.appendChild(element);
699
+ console.log('Web component element hidden container\'a taşındı (state korunuyor)');
700
+ }
701
+ }
702
+ catch (error) {
703
+ console.warn('Web component element preserve edilirken hata:', error);
704
+ }
705
+ }
706
+ /**
707
+ * Web component element'ini hidden container'dan çıkar ve component view'a eklemeyi dene.
708
+ *
709
+ * `__arilPreserveDuringDetach` flag'i temizlenir — eğer bu retrieve sonrası element
710
+ * gerçek anlamda destroy edilirse `disconnectedCallback` flag yok kabul edip Application
711
+ * cleanup'ını yürütecek.
712
+ */
713
+ restoreWebComponentElement(detachedTree) {
714
+ try {
715
+ const componentRef = detachedTree.componentRef;
716
+ if (!componentRef)
717
+ return;
718
+ const componentInstance = componentRef.instance;
719
+ if (!componentInstance?.element)
720
+ return;
721
+ const element = componentInstance.element;
722
+ const hiddenContainer = this.getHiddenContainer();
723
+ // Eğer element hidden container'da ise, çıkar
724
+ if (hiddenContainer.contains(element)) {
725
+ hiddenContainer.removeChild(element);
726
+ delete element.__arilPreserveDuringDetach;
727
+ console.log('Web component element hidden container\'dan çıkarıldı');
728
+ // Component view'ın native element'ine eklemeyi dene
729
+ const tryRestore = () => {
730
+ const vc = componentInstance.vc;
731
+ if (vc?.nativeElement) {
732
+ vc.nativeElement.appendChild(element);
733
+ console.log('Web component element component view\'a eklendi (retrieve sırasında)');
734
+ // Props'ları güncellemek için flag set et (component kendi populateProps'unu çağıracak)
735
+ componentInstance.propsUpdated = false;
736
+ return true;
737
+ }
738
+ return false;
739
+ };
740
+ // Hemen dene
741
+ if (!tryRestore()) {
742
+ // View henüz hazır değilse, bir sonraki tick'te tekrar dene
743
+ setTimeout(() => {
744
+ if (!tryRestore()) {
745
+ // Hala başarısızsa, flag set et (ngAfterViewInit'te tekrar denenecek)
746
+ componentInstance.needsRestore = true;
747
+ console.log('Web component element restore edilecek (view hazır değil, ngAfterViewInit\'te tekrar denenecek)');
748
+ }
749
+ }, 0);
750
+ }
751
+ }
752
+ }
753
+ catch (error) {
754
+ console.warn('Web component element restore edilirken hata:', error);
755
+ }
756
+ }
757
+ /**
758
+ * Global hidden container'ı oluşturur veya döndürür
759
+ */
760
+ getHiddenContainer() {
761
+ const slot = globalThis;
762
+ let container = slot.__arilWebComponentHiddenContainer;
763
+ if (!container) {
764
+ container = document.createElement('div');
765
+ container.style.display = 'none';
766
+ container.style.position = 'absolute';
767
+ container.style.left = '-9999px';
768
+ container.style.top = '-9999px';
769
+ container.style.visibility = 'hidden';
770
+ document.body.appendChild(container);
771
+ slot.__arilWebComponentHiddenContainer = container;
772
+ }
773
+ return container;
774
+ }
775
+ /**
776
+ * Detached route view'ı içindeki plugin/MFE custom element'lerini (tag adında `-`
777
+ * bulunan ve `customElements`'a kayıtlı olanları) hidden container'a taşır ve
778
+ * yerlerine birer Comment placeholder bırakır. Element document'tan ayrılmaz,
779
+ * Angular Elements `disconnectedCallback` 10ms destroy timer'ı kanat çırpmaz —
780
+ * dolayısıyla plugin'in `ComponentRef` ve dahili state'i (form değerleri, search
781
+ * sonuçları vs.) sonraki retrieve'e kadar korunur.
782
+ */
783
+ preserveCustomElementsInTree(detachedTree) {
784
+ try {
785
+ const componentRef = detachedTree.componentRef;
786
+ const rootEl = componentRef?.location?.nativeElement;
787
+ if (!rootEl)
788
+ return;
789
+ const hiddenContainer = this.getHiddenContainer();
790
+ const all = rootEl.querySelectorAll('*');
791
+ const preservedList = [];
792
+ for (const el of Array.from(all)) {
793
+ const tag = el.tagName.toLowerCase();
794
+ if (!tag.includes('-'))
795
+ continue;
796
+ if (!window.customElements?.get(tag))
797
+ continue;
798
+ const node = el;
799
+ const placeholder = document.createComment(`aril-preserved:${tag}`);
800
+ node.parentNode?.insertBefore(placeholder, node);
801
+ hiddenContainer.appendChild(node);
802
+ preservedList.push({ el: node, placeholder });
803
+ }
804
+ if (preservedList.length) {
805
+ this.preservedCustomEls.set(detachedTree, preservedList);
806
+ console.log(`[Strategy] preserveCustomElementsInTree: ${preservedList.length} element taşındı`);
807
+ }
808
+ }
809
+ catch (error) {
810
+ console.warn('Custom element preserve edilirken hata:', error);
811
+ }
812
+ }
813
+ /**
814
+ * preserveCustomElementsInTree ile taşınan custom element'leri orijinal
815
+ * placeholder'larının yerine geri koyar.
816
+ */
817
+ restoreCustomElementsInTree(detachedTree) {
818
+ try {
819
+ const preservedList = this.preservedCustomEls.get(detachedTree);
820
+ if (!preservedList?.length)
821
+ return;
822
+ for (const { el, placeholder } of preservedList) {
823
+ if (placeholder.parentNode) {
824
+ placeholder.parentNode.replaceChild(el, placeholder);
825
+ }
826
+ else {
827
+ // Placeholder ağaçtan koparılmış — parent view tamamen destroy edilmiş olabilir
828
+ // (örn. remote tab kapanmış). Element'i hidden container'da bırakmamak için cleanup.
829
+ console.warn('[Strategy] restore: placeholder parent yok, custom element cleanup ediliyor', el.tagName);
830
+ if (el.parentNode === this.getHiddenContainer()) {
831
+ this.getHiddenContainer().removeChild(el);
832
+ }
833
+ }
834
+ }
835
+ this.preservedCustomEls.delete(detachedTree);
836
+ console.log(`[Strategy] restoreCustomElementsInTree: ${preservedList.length} element geri yerleştirildi`);
837
+ }
838
+ catch (error) {
839
+ console.warn('Custom element restore edilirken hata:', error);
840
+ }
841
+ }
842
+ isMatcherRoute(route) {
843
+ return !!route?.routeConfig?.matcher;
844
+ }
845
+ getRemoteName(route) {
846
+ return route?.routeConfig?.data?.remoteName ?? null;
847
+ }
848
+ /**
849
+ * URL `?_tab=<tabId>` query param'ını oku — `NavService` her tab için unique değer üretir.
850
+ * `IsolatedLocationStrategy` sayesinde host Router'a tabsız navigation gelmez, sadece
851
+ * `tabState({_tab})`'lı tab tıklamaları gelir → `route.queryParams['_tab']` her zaman dolu.
852
+ */
853
+ getTabIdFromRoute(route) {
854
+ const v = route?.queryParams?.['_tab'];
855
+ return typeof v === 'string' && v.length > 0 ? v : null;
856
+ }
857
+ /**
858
+ * Route key'ini oluşturur. Matcher (web component) route'lar için remoteName + tabId
859
+ * bazlı key kullanılır — aynı remote'un her tab'i ayrı MFE custom element instance'ına
860
+ * sahiptir (bağımsız state).
861
+ *
862
+ * Normal route'lar için `pathFromRoot` üzerinden tam yol üretilir, ardından query params
863
+ * eklenir (zaten `_tab` dahil) — nested boş-path (`path: ''`) child route'larının aynı `""`
864
+ * key'iyle çakışmasını engeller ve aynı path'in iki tab'i farklı key alır.
865
+ */
866
+ getRouteKey(route) {
867
+ if (this.isMatcherRoute(route)) {
868
+ const remoteName = this.getRemoteName(route);
869
+ if (remoteName) {
870
+ const tabId = this.getTabIdFromRoute(route);
871
+ return tabId ? `mfe:${remoteName}:${tabId}` : `mfe:${remoteName}`;
872
+ }
873
+ }
874
+ // Tam path: kök → intermediate → self. Boş segmentleri index ile koruyarak hiyerarşi
875
+ // pozisyonunu key'e dahil ederiz (kardeş empty-path route'lar farklı key alır).
876
+ const fullPath = route.pathFromRoot
877
+ .map((r, i) => {
878
+ const seg = r.url.map((s) => s.path).join('/');
879
+ return seg.length > 0 ? seg : `#${i}`;
880
+ })
881
+ .join('/');
882
+ const queryParams = this.serializeQueryParams(route.queryParams);
883
+ const fragment = route.fragment ? '#' + route.fragment : '';
884
+ return fullPath + queryParams + fragment;
885
+ }
886
+ /**
887
+ * Query param'ları URL-safe şekilde serileştirir. `URLSearchParams` constructor'una
888
+ * doğrudan object verirsek array değerler `?ids=1,2,3` gibi tek key olarak yazılır;
889
+ * Angular Router'ın `?ids=1&ids=2&ids=3` davranışıyla uyumsuz olabilir. `append` ile
890
+ * her array elemanı ayrı entry olur ve null/undefined değerler atlanır.
891
+ */
892
+ serializeQueryParams(params) {
893
+ // `sort()` ile determinism — aynı sayfanın farklı insertion-order'lı queryParams'ı
894
+ // (deep link share, server-side render, JS-generated URL'ler) aynı route key üretir.
895
+ // Aksi takdirde `?b=1&a=2` ve `?a=2&b=1` iki ayrı handle olarak cache'lenir → tab
896
+ // izolasyonu aynı sayfanın iki ayrı instance'ını açar.
897
+ const keys = Object.keys(params).sort();
898
+ if (!keys.length)
899
+ return '';
900
+ const out = new URLSearchParams();
901
+ for (const k of keys) {
902
+ const v = params[k];
903
+ if (v == null)
904
+ continue;
905
+ if (Array.isArray(v)) {
906
+ v.forEach((vv) => vv != null && out.append(k, String(vv)));
907
+ }
908
+ else {
909
+ out.append(k, String(v));
910
+ }
911
+ }
912
+ const str = out.toString();
913
+ return str ? '?' + str : '';
914
+ }
182
915
  shouldReuseRoute(future, curr) {
183
- if (curr.firstChild?.routeConfig?.data?.['remoteEntry'] === future.firstChild?.routeConfig?.data?.['remoteEntry'])
916
+ const futureMatcher = this.isMatcherRoute(future);
917
+ const currMatcher = this.isMatcherRoute(curr);
918
+ if (futureMatcher && currMatcher) {
919
+ const fr = this.getRemoteName(future);
920
+ const cr = this.getRemoteName(curr);
921
+ const ft = this.getTabIdFromRoute(future);
922
+ const ct = this.getTabIdFromRoute(curr);
923
+ // Aynı remote olsa bile farklı tab'lerse fresh component instantiate edilmeli —
924
+ // aksi takdirde iki tab tek componentRef'i paylaşır ve state izolasyonu kaybolur.
925
+ const shouldReuse = !!fr && fr === cr && ft === ct;
926
+ console.log(`[Strategy] shouldReuseRoute matcher → ${shouldReuse}. future=${fr}/${ft} curr=${cr}/${ct}`);
927
+ return shouldReuse;
928
+ }
929
+ if (futureMatcher !== currMatcher) {
930
+ console.log('[Strategy] shouldReuseRoute mixed → false');
931
+ return false;
932
+ }
933
+ const shouldReuse = future.routeConfig === curr.routeConfig;
934
+ const futureKey = this.getRouteKey(future);
935
+ const currKey = this.getRouteKey(curr);
936
+ const futureComp = future.routeConfig?.component?.name ?? future.component?.name ?? 'none';
937
+ const currComp = curr.routeConfig?.component?.name ?? curr.component?.name ?? 'none';
938
+ console.log(`[Strategy] shouldReuseRoute normal → ${shouldReuse}. future="${futureKey}"(${futureComp}) curr="${currKey}"(${currComp})`);
939
+ return shouldReuse;
940
+ }
941
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: CustomRouteReuseStrategy, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
942
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: CustomRouteReuseStrategy, providedIn: 'root' }); }
943
+ }
944
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: CustomRouteReuseStrategy, decorators: [{
945
+ type: Injectable,
946
+ args: [{
947
+ providedIn: 'root'
948
+ }]
949
+ }], ctorParameters: () => [] });
950
+
951
+ /**
952
+ * `Router.navigateByUrl` wrapper'ı — `AbortError`'ı caller için "navigation cancelled"
953
+ * sinyali olarak değerlendirip Promise'i `false` ile resolve eder.
954
+ *
955
+ * **Neden**: Angular Router pending bir navigation cycle aktifken yeni `navigateByUrl`
956
+ * çağrılırsa, eski cycle'ı **kasıtlı olarak iptal eder** ("son navigation kazanır" semantiği)
957
+ * ve eski Promise'ı `AbortError: Transition was skipped` ile reject eder. Bu hata değil,
958
+ * Router'ın akış kontrolüdür — eski navigation zaten anlamsız çünkü kullanıcı yeni bir
959
+ * hedefe yöneldi. Caller'ın bu rejection'ı silently handle etmesi BEKLENİR. Aksi halde
960
+ * "Uncaught (in promise) AbortError" console gürültüsü oluşur.
961
+ *
962
+ * Bu helper bütün NavService + MFE bootstrap relay + AppLayoutComponent çağrılarını
963
+ * tek noktadan tutarlı handle eder. Diğer hata türleri (`NavigationError`, runtime exception)
964
+ * normal şekilde reject edilir — gerçekten beklenmeyen sorunlar yutulmaz.
965
+ */
966
+ function safeNavigate(router, url, extras) {
967
+ return router.navigateByUrl(url, extras).catch((err) => {
968
+ // Yalnızca "iptal" semantiği false ile resolve edilir — genuine hatalar (guard reject,
969
+ // runtime exception, network down) rethrow edilir ki global `ErrorHandler` veya caller
970
+ // `.catch` zinciri yakalasın. Silent yutmak production debug'ı imkansızlaştırıyordu.
971
+ if (isCancelledNavigation(err)) {
184
972
  return false;
185
- return future?.routeConfig === curr?.routeConfig && JSON.stringify(future?.params) === JSON.stringify(curr?.params);
973
+ }
974
+ throw err;
975
+ });
976
+ }
977
+ /**
978
+ * Angular Router cycle iptali pek çok şekilde rejection üretebilir:
979
+ * - `Error` instance'ı, `name='AbortError'` veya plain `name='Error'`
980
+ * - `message` 'Transition was skipped' veya 'Navigation Cancel' içerir
981
+ * - `code` numerik `NavigationCancellationCode` (Router 16+)
982
+ * Tüm bu varyasyonları "iptal edildi, normal" olarak yorumlayıp yutarız.
983
+ */
984
+ function isCancelledNavigation(err) {
985
+ if (!err || typeof err !== 'object')
986
+ return false;
987
+ const e = err;
988
+ if (e.name === 'AbortError')
989
+ return true;
990
+ if (typeof e.message === 'string') {
991
+ const m = e.message;
992
+ if (m.includes('Transition was skipped') || m.includes('NavigationCancel'))
993
+ return true;
186
994
  }
995
+ // Angular Router NavigationCancellationCode enum: SupersededByNewNavigation = 2
996
+ if (typeof e.code === 'number' && e.code === 2)
997
+ return true;
998
+ return false;
999
+ }
1000
+
1001
+ /**
1002
+ * Router'a verilecek URL'e `_tab` query param ekler. Tab navLink'leri CLEAN saklandığı
1003
+ * için her navigation'da bu helper'la `_tab` eklenir. UrlSerializer serialize sırasında
1004
+ * `_tab`'i strip eder → adres çubuğunda görünmez ama Router internal queryParams'ında
1005
+ * kalır → strategy doğru çalışır.
1006
+ *
1007
+ * Path'in başında leading slash garanti edilir (Router URL formatına uygunluk için).
1008
+ */
1009
+ function withTabId(rawNavLink, tabId) {
1010
+ const { path, query, fragment } = splitUrl(rawNavLink ?? '');
1011
+ const params = new URLSearchParams(query);
1012
+ params.set('_tab', tabId);
1013
+ return `${path}?${params.toString()}${fragment}`;
1014
+ }
1015
+ /**
1016
+ * NavLink'i clean formatta normalize eder: leading slash garantisi, query string'ten
1017
+ * `_tab` strip, fragment korunur. `activeRoute()` UrlSerializer override'ı sayesinde
1018
+ * clean URL döner; tab storage da clean olmalı ki karşılaştırma çalışsın.
1019
+ */
1020
+ function cleanNavLink(rawNavLink) {
1021
+ const { path, query, fragment } = splitUrl(rawNavLink ?? '');
1022
+ if (!query)
1023
+ return path + fragment;
1024
+ const params = new URLSearchParams(query);
1025
+ params.delete('_tab');
1026
+ const remaining = params.toString();
1027
+ return remaining ? `${path}?${remaining}${fragment}` : path + fragment;
1028
+ }
1029
+ /**
1030
+ * URL string'ini path/query/fragment olarak parçalar. Fragment (`#`) ilk önce ayrılır
1031
+ * ki query (`?`) içindeki `#` ile karışmasın. Path'in başına leading slash garanti
1032
+ * edilir (Router URL formatı için zorunlu).
1033
+ *
1034
+ * `URL` constructor'ı relative URL ile çalışmaz, manuel split `URLSearchParams`'a
1035
+ * uygun temiz query string verir. Fragment olduğu gibi (öncesinde `#` ile) döner.
1036
+ */
1037
+ function splitUrl(raw) {
1038
+ const fragmentIdx = raw.indexOf('#');
1039
+ const fragment = fragmentIdx >= 0 ? raw.slice(fragmentIdx) : '';
1040
+ const beforeFragment = fragmentIdx >= 0 ? raw.slice(0, fragmentIdx) : raw;
1041
+ const queryIdx = beforeFragment.indexOf('?');
1042
+ let path = queryIdx >= 0 ? beforeFragment.slice(0, queryIdx) : beforeFragment;
1043
+ const query = queryIdx >= 0 ? beforeFragment.slice(queryIdx + 1) : '';
1044
+ if (!path.startsWith('/'))
1045
+ path = '/' + path;
1046
+ return { path, query, fragment };
1047
+ }
1048
+ /** Router'a verilen extras.state objesi — `_tab`'i history state'inde tutar. */
1049
+ function tabState(tabId) {
1050
+ return { state: { _tab: tabId } };
1051
+ }
1052
+ /**
1053
+ * NavLink'in remote (ilk path segment) adını döndürür. Query param'lardan ve leading
1054
+ * slash'tan etkilenmez — `/mw/app-management?_tab=abc` → `mw`.
1055
+ */
1056
+ function extractRemoteName(navLink) {
1057
+ if (!navLink)
1058
+ return null;
1059
+ let path = navLink.split('?')[0];
1060
+ if (path.startsWith('/'))
1061
+ path = path.slice(1);
1062
+ return path.split('/')[0] || null;
187
1063
  }
1064
+ class NavService {
1065
+ constructor() {
1066
+ this.router = inject(Router);
1067
+ this.routeCloseService = inject(RouteCloseService);
1068
+ this.messageService = inject(MessageService);
1069
+ this.translocoService = inject(TranslocoService);
1070
+ // i18n çevirileri lazy (async HTTP) yüklenir; `firstCheckForRoute`/`openHomeTab` çeviri
1071
+ // yüklenmeden `translate('tab.home')` ederse raw key ("tab.home") kalır. Çeviri yüklenince
1072
+ // fallback-isimli tab'ları (özellikle anasayfa) menüyle revize et → "Anasayfa" yerine oturur.
1073
+ this.i18nReloadSub = this.translocoService.events$
1074
+ .pipe(
1075
+ // Yalnız root/global i18n (scope YOK) yüklenince reconcile et — 'tab.home' orada.
1076
+ // Feature lazy scope'ları (her MFE sayfası kendi scope'unu yükler) gereksiz menü-DFS
1077
+ // tetiklemesin; reconcile yalnız home/menü adlarının bağlı olduğu global çeviri içindir.
1078
+ filter((e) => e.type === 'translationLoadSuccess' && !e.payload?.scope), takeUntilDestroyed())
1079
+ .subscribe(() => this.reconcileTabsWithMenu());
1080
+ this.firstCheckForRoute = false;
1081
+ /**
1082
+ * Aynı anda açık olabilecek tab sayısı tavanı. Tab bar'ın UX'i ve `CustomRouteReuseStrategy`
1083
+ * `storedRoutes` birikimi için üst sınır. Aşılırsa yeni navigasyon engellenir.
1084
+ */
1085
+ this.MAX_TABS = 20;
1086
+ /**
1087
+ * Menu items'i NavService'ten almak için provider. `aril/boot/bridge`'i doğrudan import
1088
+ * etmek ng-packagr'da `aril/boot/bridge → aril/boot/config/apps` döngüsü tetikliyordu
1089
+ * (bridge `PluginMenuItem` type'ını import ediyor). Bunun yerine host shell
1090
+ * (`AppLayoutComponent`) `setMenuItemsProvider(() => bridge.hostMenuItems())` ile bağlar.
1091
+ * Provider yoksa label/icon resolve'u atlanır, URL segment fallback'i devreye girer.
1092
+ */
1093
+ this.menuItemsProvider = null;
1094
+ /**
1095
+ * Host shell anasayfa NavItem'ını sağlar — `AppLayoutComponent.homeNavItem` (menüdeki
1096
+ * `root: true` öğesinden türetilir: navLink/navName/icon). Tüm tab'lar kapatıldığında
1097
+ * liste boş kalmasın diye `openHomeTab` bunu kullanır; provider yoksa `/` + i18n fallback.
1098
+ */
1099
+ this.homeNavItemProvider = null;
1100
+ /**
1101
+ * URL adres çubuğundan görülen aktif route — `TabAwareUrlSerializer` `_tab`'i strip
1102
+ * ettiği için bu CLEAN URL'dir (ör. `/crm/address-management/search`). Tab navLink'leri
1103
+ * de clean saklandığı için `r.navLink === activeRoute()` doğrudan karşılaştırılır.
1104
+ */
1105
+ this.activeRoute = toSignal(this.router.events.pipe(filter((e) => e instanceof NavigationEnd), tap((e) => {
1106
+ // Her NavigationEnd'de history state'ten aktif tabId'yi okuyup signal'a yaz.
1107
+ // `extras.state` ile yapılan navigation'larda Angular history.state'e `_tab`'i koyar;
1108
+ // browser back/forward navigasyonlarında da history.state korunur.
1109
+ const tabIdFromState = window.history.state?.['_tab'];
1110
+ if (typeof tabIdFromState === 'string' && tabIdFromState.length > 0) {
1111
+ this.activeTabId.set(tabIdFromState);
1112
+ }
1113
+ const currentUrl = e.urlAfterRedirects || e.url;
1114
+ // NOT: Aktif tab'ın navLink'ini güncelleme `syncActiveTabNavLinkForTab` ile
1115
+ // `bridge.activeMFEUrl` üzerinden yapılır (kaynağa-bağlı, cross-tab safe).
1116
+ // Tab `activeRoutes`'ta YOKSA türet: (a) ilk açılış/refresh deep-link, VEYA
1117
+ // (b) browser back/forward ile KAPATILMIŞ tab'ın history entry'sine dönüş
1118
+ // (kapatma host history'yi silmez → geri tuşu o URL'e gider, tab yeniden açılmalı).
1119
+ // Bilinen tab (normal tıklama / yeni tab / açık tab'a geri) → yalnız activeTabId
1120
+ // güncellenir (yukarıda), mevcut tab'a focus; türetme yapılmaz.
1121
+ const knownTab = typeof tabIdFromState === 'string' &&
1122
+ tabIdFromState.length > 0 &&
1123
+ this.activeRoutes().some((r) => r.tabId === tabIdFromState);
1124
+ if (!knownTab) {
1125
+ // Refresh sonrası deep link → URL'den nav item türet ve activeRoutes'a ekle.
1126
+ // URL'de `_tab` yoksa (UrlSerializer strip ettiği için adres çubuğundan paylaşılan
1127
+ // link `_tab` taşımaz) yeni tabId üret + history state'e yaz; navigateByUrl
1128
+ // çağrısı ile state injekte ederiz, adres çubuğu temiz kalır.
1129
+ if (currentUrl) {
1130
+ const stateTabId = window.history.state?.['_tab'];
1131
+ const navLink = cleanNavLink(currentUrl);
1132
+ // İlk navigation'da (F5/açılış) gelen URL zaten activeRoutes'taki bir tab ile
1133
+ // eşleşiyorsa (özellikle restore edilmiş pinned tab) YENİ tab TÜRETME — o tab'ı
1134
+ // kullan. F5 sonrası Angular initial navigation `history.state._tab`'ı koruMAZ →
1135
+ // `knownTab` yakalayamaz; navLink eşleşmesi restore-duplicate'ini önler. Yalnız ilk
1136
+ // check'te + state'te `_tab` yokken: sonraki navigationlarda kasıtlı duplicate tab ve
1137
+ // browser-back ile kapatılan tab'ın re-derive davranışı korunur.
1138
+ const existing = !this.firstCheckForRoute && !stateTabId
1139
+ ? this.activeRoutes().find((r) => r.navLink === navLink)
1140
+ : undefined;
1141
+ let tabId = typeof stateTabId === 'string' && stateTabId.length > 0 ? stateTabId : existing?.tabId ?? '';
1142
+ let needsStateInject = false;
1143
+ if (!tabId) {
1144
+ tabId = this.generateTabId();
1145
+ needsStateInject = true;
1146
+ }
1147
+ else if (existing) {
1148
+ // Eşleşen tab'ın `tabId`'sini URL/state'e yansıt — adres çubuğu `_tab` taşımadığı
1149
+ // için RouteReuse `mfe:<remote>:<tabId>` key tutarlılığı state inject ister.
1150
+ needsStateInject = true;
1151
+ }
1152
+ this.activeTabId.set(tabId);
1153
+ if (!existing) {
1154
+ const menuInfo = this.resolveNavInfoFromMenu(navLink);
1155
+ const fallbackName = () => {
1156
+ let path = navLink;
1157
+ if (path.startsWith('/'))
1158
+ path = path.slice(1);
1159
+ const parts = path.split('/').filter((s) => s.length > 0);
1160
+ const last = parts[parts.length - 1] || 'Page';
1161
+ return last.charAt(0).toUpperCase() + last.slice(1).replace(/-/g, ' ');
1162
+ };
1163
+ this.addToActiveRoutes({
1164
+ tabId,
1165
+ navLink,
1166
+ navName: menuInfo?.navName || fallbackName(),
1167
+ icon: menuInfo?.icon
1168
+ });
1169
+ }
1170
+ if (needsStateInject) {
1171
+ // Mevcut navigation cycle bitiminden SONRA yeni navigation tetikle —
1172
+ // `queueMicrotask` Router'ın navigation kuyruğu finalize olmadan çalışıp
1173
+ // `AbortError: Transition was skipped`'a yol açıyordu. `setTimeout(0)` macrotask
1174
+ // ile current cycle biter, Router idle olur. `this.navigate` zaten AbortError'ı
1175
+ // yutuyor — concurrent navigation race'leri sessizce iptal edilir.
1176
+ setTimeout(() => {
1177
+ safeNavigate(this.router, withTabId(navLink, tabId), {
1178
+ replaceUrl: true,
1179
+ state: { _tab: tabId }
1180
+ }).catch(() => { });
1181
+ }, 0);
1182
+ }
1183
+ }
1184
+ this.firstCheckForRoute = true;
1185
+ }
1186
+ }), map$1((w) => w.urlAfterRedirects || w.url)));
1187
+ this.activeRoutes = signal([]);
1188
+ /** History state'ten okunan aktif tab kimliği — `isActive(tab) = tab.tabId === activeTabId()`. */
1189
+ this.activeTabId = signal('');
1190
+ }
1191
+ /** RFC4122 v4 UUID — collision-free, internal state taşımaz. */
1192
+ generateTabId() {
1193
+ return crypto.randomUUID();
1194
+ }
1195
+ setMenuItemsProvider(provider) {
1196
+ this.menuItemsProvider = provider;
1197
+ // Provider geç set edildiyse (örn. shell layout NavigationEnd'den sonra mount oldu),
1198
+ // activeRoutes'ta default/fallback isimle eklenmiş ilk tab'ı menüyle revize et.
1199
+ this.reconcileTabsWithMenu();
1200
+ }
1201
+ setHomeNavItemProvider(provider) {
1202
+ this.homeNavItemProvider = provider;
1203
+ // Refresh/ilk açılış path'inde fallback ("Page") isim/ikonla eklenmiş anasayfa tab'ı
1204
+ // varsa, provider hazır olunca menüyle (home dahil) revize et.
1205
+ this.reconcileTabsWithMenu();
1206
+ }
1207
+ /**
1208
+ * Chrome bookmark click davranışı: aktif tab'ın navLink/navName'i güncellenir,
1209
+ * o tab'a navigate edilir. Yeni tab açılmaz. Mevcut tab'ın `tabId`'si KORUNUR —
1210
+ * Router'a `?_tab=tabId` ile navigate edilir, history state'e yazılır; navLink
1211
+ * storage'da CLEAN saklanır.
1212
+ */
1213
+ navigateInCurrentTab(navItem) {
1214
+ const idx = this.findActiveTabIndex();
1215
+ if (idx === -1) {
1216
+ // Aktif tab yok — fallback olarak yeni tab aç.
1217
+ this.openInNewTabAndFocus(navItem);
1218
+ return;
1219
+ }
1220
+ const existingTab = this.activeRoutes()[idx];
1221
+ const tabId = existingTab.tabId;
1222
+ const newNavLink = cleanNavLink(navItem.navLink);
1223
+ // Aynı navLink'e tıklama → yine de navigate et (`onSameUrlNavigation: 'reload'`).
1224
+ if (existingTab.navLink === newNavLink) {
1225
+ safeNavigate(this.router, withTabId(newNavLink, tabId), tabState(tabId));
1226
+ return;
1227
+ }
1228
+ this.activeRoutes.update((routes) => {
1229
+ const next = [...routes];
1230
+ // Mevcut tab'ın `tabId` VE `pinned` durumu korunur — menüden gelen navItem `pinned`
1231
+ // taşımaz; `{ ...navItem }` ile ezilirse sabitlenmiş tab unpin olurdu (tab içeriğini
1232
+ // değiştirmek sabitlemeyi bozmamalı).
1233
+ next[idx] = { ...navItem, tabId, navLink: newNavLink, pinned: existingTab.pinned };
1234
+ return next;
1235
+ });
1236
+ safeNavigate(this.router, withTabId(newNavLink, tabId), tabState(tabId));
1237
+ }
1238
+ /**
1239
+ * Arka planda yeni tab aç — aktif tab değişmez, navigate edilmez. `tabId` her zaman
1240
+ * yeni üretilir (ya da caller verdiyse o kullanılır), exists check YOKTUR; aynı path
1241
+ * birden fazla tab'da bağımsız yaşayabilir.
1242
+ */
1243
+ openInBackgroundTab(navItem) {
1244
+ if (this.activeRoutes().length >= this.MAX_TABS) {
1245
+ this.notifyMaxTabsReached();
1246
+ return;
1247
+ }
1248
+ const tabId = navItem.tabId || this.generateTabId();
1249
+ const navLink = cleanNavLink(navItem.navLink);
1250
+ this.activeRoutes.update((val) => [...val, { ...navItem, tabId, navLink }]);
1251
+ }
1252
+ /**
1253
+ * Yeni tab aç ve o tab'a geç. Exists check YOK — aynı path'in iki ayrı tab'i kabul edilir;
1254
+ * her tab kendi `tabId`'siyle ayrı componentRef instantiate eder.
1255
+ */
1256
+ openInNewTabAndFocus(navItem) {
1257
+ if (this.activeRoutes().length >= this.MAX_TABS) {
1258
+ this.notifyMaxTabsReached();
1259
+ return;
1260
+ }
1261
+ const tabId = navItem.tabId || this.generateTabId();
1262
+ const navLink = cleanNavLink(navItem.navLink);
1263
+ this.activeRoutes.update((val) => [...val, { ...navItem, tabId, navLink }]);
1264
+ safeNavigate(this.router, withTabId(navLink, tabId), tabState(tabId));
1265
+ }
1266
+ /** Programatik tab ekleme; activeRoute observable refresh path'i bunu kullanır. */
1267
+ addToActiveRoutes(navItem) {
1268
+ if (this.activeRoutes().length >= this.MAX_TABS) {
1269
+ // Programmatic çağrı — toast göstermek yerine sessizce return.
1270
+ return;
1271
+ }
1272
+ const tabId = navItem.tabId || this.generateTabId();
1273
+ const navLink = cleanNavLink(navItem.navLink);
1274
+ // Signal change detection reference identity ile çalışır — yeni array dön.
1275
+ this.activeRoutes.update((val) => [...val, { ...navItem, tabId, navLink }]);
1276
+ }
1277
+ /**
1278
+ * Aktif tab'ı `activeTabId` üzerinden bulur. Aynı `navLink`'in birden fazla tab'i
1279
+ * olabileceği için navLink karşılaştırması ambiguous; tabId tek doğru kimlik.
1280
+ */
1281
+ findActiveTabIndex() {
1282
+ const tabId = this.activeTabId();
1283
+ if (!tabId)
1284
+ return -1;
1285
+ return this.activeRoutes().findIndex((r) => r.tabId === tabId);
1286
+ }
1287
+ /** Tab limiti aşıldığında kullanıcıyı toast ile bilgilendirir. */
1288
+ notifyMaxTabsReached() {
1289
+ this.messageService.add({
1290
+ severity: 'warn',
1291
+ summary: this.translocoService.translate('tab.maxReachedSummary', { max: this.MAX_TABS }),
1292
+ detail: this.translocoService.translate('tab.maxReachedDetail'),
1293
+ key: 'toast-root'
1294
+ });
1295
+ }
1296
+ /** Anasayfa navLink'i — host menü root item'ından (yoksa '/'), clean format. */
1297
+ homeNavLink() {
1298
+ return cleanNavLink(this.homeNavItemProvider?.().navLink || '/');
1299
+ }
1300
+ /** Verilen tab anasayfaya mı ait. */
1301
+ isHomeTab(item) {
1302
+ return item.navLink === this.homeNavLink();
1303
+ }
1304
+ /**
1305
+ * Anasayfa tab'ı açıp ona navigate eder — tab listesi HİÇ boş kalmamalı (en az 1 tab).
1306
+ * NavItem host menü root item'ından gelir; yoksa `/` + i18n fallback ('tab.home', 'pi pi-home').
1307
+ */
1308
+ openHomeTab() {
1309
+ const base = this.homeNavItemProvider?.();
1310
+ const navLink = cleanNavLink(base?.navLink || '/');
1311
+ const tabId = this.generateTabId();
1312
+ const home = {
1313
+ tabId,
1314
+ navLink,
1315
+ navName: base?.navName || this.translocoService.translate('tab.home'),
1316
+ icon: base?.icon || 'pi pi-home'
1317
+ };
1318
+ this.activeRoutes.update((val) => [...val, home]);
1319
+ return safeNavigate(this.router, withTabId(navLink, tabId), tabState(tabId));
1320
+ }
1321
+ closeNavigation(navItem) {
1322
+ // `tabId` ile bul — aynı path'in birden fazla tab'i olabileceği için `navLink`
1323
+ // karşılaştırması ambiguous olur, kimlik bazlı arama tek doğru yöntem.
1324
+ const idx = this.activeRoutes().findIndex((r) => r.tabId === navItem.tabId);
1325
+ console.log(`[NavService] closeNavigation tabId="${navItem.tabId}" navLink="${navItem.navLink}" index=${idx}`);
1326
+ if (idx === -1)
1327
+ return;
1328
+ const closedItem = this.activeRoutes()[idx];
1329
+ // Son kalan tab anasayfaysa kapatma — her zaman en az 1 (anasayfa) tab açık kalmalı.
1330
+ if (this.activeRoutes().length === 1 && this.isHomeTab(closedItem))
1331
+ return;
1332
+ // Signal change detection reference identity ile çalışır — yeni array dön.
1333
+ this.activeRoutes.update((val) => val.filter((_, i) => i !== idx));
1334
+ const cleanup = () => this.cleanupTabHandle(closedItem);
1335
+ const closingActiveTab = this.activeTabId() === closedItem.tabId;
1336
+ if (closingActiveTab) {
1337
+ // Aktif tab kapatılıyor — başka tab'a (veya home'a) navigate edip cleanup'ı SONRA tetikle.
1338
+ // Aksi takdirde strategy henüz `store('mfe:${remoteName}:${tabId}', handle)`'i yapmadan
1339
+ // `deleteStoredRoute` çağrılır ve cleanup boş hit eder; ardından detach tamamlanınca
1340
+ // element hidden container'da orphan kalır (race condition).
1341
+ const remaining = this.activeRoutes();
1342
+ const willNavigateItem = remaining.length > 0 ? remaining[idx] || remaining[idx - 1] : null;
1343
+ if (willNavigateItem) {
1344
+ safeNavigate(this.router, withTabId(willNavigateItem.navLink, willNavigateItem.tabId), tabState(willNavigateItem.tabId)).finally(cleanup);
1345
+ }
1346
+ else {
1347
+ // Hiç tab kalmadı — boş bırakma, anasayfa tab'ı aç (en az 1 tab kuralı).
1348
+ this.openHomeTab().finally(cleanup);
1349
+ }
1350
+ }
1351
+ else {
1352
+ // Aktif değildi — navigation gerekmez. Ancak arka planda açılıp (`openInBackgroundTab`)
1353
+ // hiç görüntülenmemiş tab henüz Router tarafından `store()` edilmemiş olabilir; cleanup'ı
1354
+ // bir microtask geciktirip olası store cycle'ına fırsat veriyoruz (orphan handle önlemi).
1355
+ void Promise.resolve().then(cleanup);
1356
+ }
1357
+ }
1358
+ /**
1359
+ * Bir tab hariç diğer tüm tab'ları kapatır. Aktif tab kapatılanlardan ise `keepItem`'a geçer.
1360
+ * **Sabit tab'lar korunur** (Chrome davranışı) — pinned olanlar her zaman açık kalır.
1361
+ */
1362
+ closeOthers(keepItem) {
1363
+ const all = this.activeRoutes();
1364
+ const toClose = all.filter((r) => r.tabId !== keepItem.tabId && !r.pinned);
1365
+ if (toClose.length === 0)
1366
+ return;
1367
+ const survivorIds = new Set([keepItem.tabId, ...all.filter((r) => r.pinned).map((r) => r.tabId)]);
1368
+ this.activeRoutes.set(all.filter((r) => survivorIds.has(r.tabId)));
1369
+ const cleanupAll = () => toClose.forEach((t) => this.cleanupTabHandle(t));
1370
+ if (this.activeTabId() !== keepItem.tabId) {
1371
+ safeNavigate(this.router, withTabId(keepItem.navLink, keepItem.tabId), tabState(keepItem.tabId)).finally(cleanupAll);
1372
+ }
1373
+ else {
1374
+ cleanupAll();
1375
+ }
1376
+ }
1377
+ /**
1378
+ * Verilen tab'ın sağındaki tüm tab'ları kapatır. **Sabit tab'lar korunur** —
1379
+ * pinned bir tab sağda olsa bile kapatılmaz (Chrome davranışı).
1380
+ */
1381
+ closeToRight(item) {
1382
+ const all = this.activeRoutes();
1383
+ const idx = all.findIndex((r) => r.tabId === item.tabId);
1384
+ if (idx === -1)
1385
+ return;
1386
+ const toClose = all.slice(idx + 1).filter((r) => !r.pinned);
1387
+ if (toClose.length === 0)
1388
+ return;
1389
+ const toCloseIds = new Set(toClose.map((r) => r.tabId));
1390
+ this.activeRoutes.set(all.filter((r) => !toCloseIds.has(r.tabId)));
1391
+ const activeWasClosed = toClose.some((r) => r.tabId === this.activeTabId());
1392
+ const cleanupAll = () => toClose.forEach((t) => this.cleanupTabHandle(t));
1393
+ if (activeWasClosed) {
1394
+ safeNavigate(this.router, withTabId(item.navLink, item.tabId), tabState(item.tabId)).finally(cleanupAll);
1395
+ }
1396
+ else {
1397
+ cleanupAll();
1398
+ }
1399
+ }
1400
+ /**
1401
+ * Sabit olmayan tüm tab'ları kapatır. **Sabit tab'lar korunur** — kullanıcı "Tümünü Kapat"
1402
+ * isterse bile pinned tab'lar açık kalır (Chrome davranışı). Hiç pinned tab yoksa ana sayfaya
1403
+ * navigate edilir.
1404
+ */
1405
+ closeAll() {
1406
+ const all = this.activeRoutes();
1407
+ if (all.length === 0)
1408
+ return;
1409
+ const toClose = all.filter((r) => !r.pinned);
1410
+ if (toClose.length === 0)
1411
+ return;
1412
+ const survivors = all.filter((r) => r.pinned);
1413
+ this.activeRoutes.set(survivors);
1414
+ const cleanupAll = () => toClose.forEach((t) => this.cleanupTabHandle(t));
1415
+ // Aktif tab kapatılanlardan ise: ya bir pinned tab'a geç (yaşıyorsa), yoksa ana sayfaya.
1416
+ const activeWasClosed = toClose.some((r) => r.tabId === this.activeTabId());
1417
+ if (activeWasClosed) {
1418
+ if (survivors.length > 0) {
1419
+ const target = survivors[0];
1420
+ safeNavigate(this.router, withTabId(target.navLink, target.tabId), tabState(target.tabId)).finally(cleanupAll);
1421
+ }
1422
+ else {
1423
+ // Pinned survivor yok — boş bırakma, anasayfa tab'ı aç (en az 1 tab kuralı).
1424
+ this.openHomeTab().finally(cleanupAll);
1425
+ }
1426
+ }
1427
+ else {
1428
+ cleanupAll();
1429
+ }
1430
+ }
1431
+ /**
1432
+ * Tab sabitleme toggle — Chrome-vari pinning. Pinned tab'lar tab bar'ın **soluna** taşınır
1433
+ * (insertion sırasını koruyarak), kompakt görünür, `closeOthers`/`closeToRight`/`closeAll`
1434
+ * çağrılarından korunur. Aynı pinned tıklanırsa sabitlemeyi kaldırır → normal alanın **sonuna**
1435
+ * taşınır.
1436
+ */
1437
+ togglePin(item) {
1438
+ const all = this.activeRoutes();
1439
+ const idx = all.findIndex((r) => r.tabId === item.tabId);
1440
+ if (idx === -1)
1441
+ return;
1442
+ const target = all[idx];
1443
+ const newPinned = !target.pinned;
1444
+ // Önce target'ı array'den çıkar, sonra pinned/normal bloğunun **sonuna** ekle.
1445
+ // Bu Chrome davranışı: pinlenince pinned bloğun sonuna (en sağa), unpinlenince normal
1446
+ // bloğun başına (pinned'in hemen sağına).
1447
+ const without = all.filter((_, i) => i !== idx);
1448
+ const pinnedSegment = without.filter((r) => r.pinned);
1449
+ const normalSegment = without.filter((r) => !r.pinned);
1450
+ const updated = { ...target, pinned: newPinned };
1451
+ // Pin → pinned bloğun sonuna; unpin → normal bloğun başına. Her iki durumda da `updated`,
1452
+ // pinned ve normal segmentlerin SINIRINA yerleşir (pozisyon aynı) → tek ifade yeterli.
1453
+ const next = [...pinnedSegment, updated, ...normalSegment];
1454
+ this.activeRoutes.set(next);
1455
+ }
1456
+ /** Sıradaki tab'a (circular) geçer. */
1457
+ goToNextTab() {
1458
+ const all = this.activeRoutes();
1459
+ if (all.length === 0)
1460
+ return;
1461
+ const idx = this.findActiveTabIndex();
1462
+ const nextIdx = idx === -1 ? 0 : (idx + 1) % all.length;
1463
+ const target = all[nextIdx];
1464
+ safeNavigate(this.router, withTabId(target.navLink, target.tabId), tabState(target.tabId));
1465
+ }
1466
+ /** Önceki tab'a (circular) geçer. */
1467
+ goToPrevTab() {
1468
+ const all = this.activeRoutes();
1469
+ if (all.length === 0)
1470
+ return;
1471
+ const idx = this.findActiveTabIndex();
1472
+ const prevIdx = idx === -1 ? all.length - 1 : (idx - 1 + all.length) % all.length;
1473
+ const target = all[prevIdx];
1474
+ safeNavigate(this.router, withTabId(target.navLink, target.tabId), tabState(target.tabId));
1475
+ }
1476
+ /** Index'teki tab'a geçer (Alt+1..9 için 0-tabanlı index). */
1477
+ goToTabByIndex(index) {
1478
+ const all = this.activeRoutes();
1479
+ if (index < 0 || index >= all.length)
1480
+ return;
1481
+ const target = all[index];
1482
+ safeNavigate(this.router, withTabId(target.navLink, target.tabId), tabState(target.tabId));
1483
+ }
1484
+ /**
1485
+ * Verilen tab'a navigate — UI'dan (tab tıklama) çağrılır. Router'a `_tab` ile URL ve
1486
+ * `extras.state` ile tabId verir; UrlSerializer adres çubuğunu temiz tutar.
1487
+ */
1488
+ navigateToTab(item) {
1489
+ if (!item?.navLink || !item?.tabId)
1490
+ return;
1491
+ const url = withTabId(item.navLink, item.tabId);
1492
+ console.log(`[NavService] navigateToTab tabId="${item.tabId}" navLink="${item.navLink}" → url="${url}"`);
1493
+ safeNavigate(this.router, url, tabState(item.tabId));
1494
+ }
1495
+ /**
1496
+ * Belirli bir tab'ın `navLink`'ini ve (varsa) `navName`'ini günceller. **Kaynağa-bağlı**
1497
+ * (per-tab) sync — caller hangi tab'ın URL'i olduğunu söyler, NavService aktif tab
1498
+ * varsayımında bulunmaz. Hidden container'daki async MFE NavigationEnd'leri aktif tab'ın
1499
+ * state'ini KONTAMİNE EDEMEZ.
1500
+ *
1501
+ * `title` parametresi MFE Router deepest route'unun `Route.title` veya `data.tabName`
1502
+ * değerinden gelir → kullanıcı detail/edit gibi sub-sayfalara navigate olduğunda tab
1503
+ * adı dinamik güncellenir. `title` undefined ise tab.navName değişmez (statik kalır).
1504
+ *
1505
+ * AppLayoutComponent effect'ten `bridge.activeMFEUrl` (`{tabId, url, title}`) ile çağrılır.
1506
+ */
1507
+ syncActiveTabNavLinkForTab(tabId, rawUrl, title) {
1508
+ if (!tabId || !rawUrl)
1509
+ return;
1510
+ const cleanUrl = cleanNavLink(rawUrl);
1511
+ const target = this.activeRoutes().find((t) => t.tabId === tabId);
1512
+ if (!target)
1513
+ return;
1514
+ const linkChanged = target.navLink !== cleanUrl;
1515
+ const nameChanged = !!title && target.navName !== title;
1516
+ if (!linkChanged && !nameChanged)
1517
+ return;
1518
+ const newNavLink = linkChanged ? cleanUrl : target.navLink;
1519
+ const newName = nameChanged ? title : target.navName;
1520
+ console.log(`[NavService] syncActiveTabNavLinkForTab tab="${tabId}" ` +
1521
+ `navLink "${target.navLink}" → "${newNavLink}" name "${target.navName}" → "${newName}"`);
1522
+ this.activeRoutes.update((routes) => routes.map((t) => (t.tabId === tabId ? { ...t, navLink: newNavLink, navName: newName } : t)));
1523
+ }
1524
+ /**
1525
+ * Tab context-menu "Yinele" (Chrome "Duplicate"): mevcut tab'ın path'i kopyalanır, yeni bir
1526
+ * `tabId` üretilir ve yeni tab oluşturulur. Strategy `_tab` query param'ını route key'in
1527
+ * parçası yaptığı için yeni tab bağımsız `storedRoutes` slot'una yazılır → bağımsız
1528
+ * componentRef + state. Mevcut tab korunur, kullanıcı her ikisinde de bağımsız çalışabilir.
1529
+ */
1530
+ duplicateTab(item) {
1531
+ if (!item?.navLink)
1532
+ return;
1533
+ if (this.activeRoutes().length >= this.MAX_TABS) {
1534
+ this.notifyMaxTabsReached();
1535
+ return;
1536
+ }
1537
+ const newTabId = this.generateTabId();
1538
+ const newNavLink = cleanNavLink(item.navLink);
1539
+ this.activeRoutes.update((val) => [
1540
+ ...val,
1541
+ { ...item, tabId: newTabId, navLink: newNavLink }
1542
+ ]);
1543
+ safeNavigate(this.router, withTabId(newNavLink, newTabId), tabState(newTabId));
1544
+ }
1545
+ reorderActiveRoutes(newRoutes) {
1546
+ // Pinned/normal sınırı koruması — sabit tab'lar tab bar'ın solunda (insertion sırasıyla),
1547
+ // normal tab'lar sağında kalır. CDK drag drop bir tab'ı yanlış segmente düşürürse
1548
+ // burada otomatik düzeltilir. Segment içi sıra korunur.
1549
+ const pinned = newRoutes.filter((r) => r.pinned);
1550
+ const normal = newRoutes.filter((r) => !r.pinned);
1551
+ this.activeRoutes.set([...pinned, ...normal]);
1552
+ }
1553
+ /**
1554
+ * Menu config'inden navLink'e karşılık gelen item'i longest-prefix match ile bulup
1555
+ * aktif dilde label + icon döner. Adres çubuğuna URL yapıştırarak deep link açıldığında
1556
+ * tab'ın menüden açılan tab'la aynı görünüme sahip olmasını sağlar.
1557
+ *
1558
+ * Lookup `menuItemsProvider` üzerinden — host shell `setMenuItemsProvider` ile
1559
+ * `bridge.hostMenuItems()`'a bağlar. Provider yoksa veya eşleşme yoksa `null` döner ve
1560
+ * caller URL segment fallback'ine düşer.
1561
+ */
1562
+ resolveNavInfoFromMenu(navLink) {
1563
+ // Anasayfa (menüdeki `root` öğesi) çoğu zaman `routerLink` ile eşleşmez (routerLink'i `/`
1564
+ // veya boş olabilir, longest-prefix match'e takılmaz) → `/` için fallback "Page" + jenerik
1565
+ // ikon dönerdi. Home provider varsa ad/ikonu doğrudan oradan (root item'dan) çöz.
1566
+ if (this.homeNavItemProvider) {
1567
+ const home = this.homeNavItemProvider();
1568
+ // Anasayfa navLink eşleşiyorsa dön — menü boş/geç yüklendiyse ya da menüde `root` öğesi
1569
+ // yoksa navName/icon boş gelebilir; i18n ('tab.home') + ev ikonu fallback ile asla
1570
+ // menü-segment fallback'ine ("Page") düşme.
1571
+ if (navLink === cleanNavLink(home.navLink || '/')) {
1572
+ return {
1573
+ navName: home.navName || this.translocoService.translate('tab.home'),
1574
+ icon: home.icon || 'pi pi-home'
1575
+ };
1576
+ }
1577
+ }
1578
+ const items = this.menuItemsProvider?.();
1579
+ if (!items?.length)
1580
+ return null;
1581
+ const path = navLink.split('?')[0];
1582
+ const matched = this.findMenuItemForRoute(items, path);
1583
+ if (!matched)
1584
+ return null;
1585
+ const lang = this.translocoService.getActiveLang();
1586
+ const label = matched.label;
1587
+ const navName = label?.[lang] || label?.['tr'] || label?.['en'] || '';
1588
+ if (!navName)
1589
+ return null;
1590
+ return { navName, icon: matched.icon };
1591
+ }
1592
+ /**
1593
+ * Menü/i18n yüklenince tab'ları (özellikle anasayfa) menüyle revize eder — refresh/ilk açılış
1594
+ * path'inde tab fallback isim/iconla eklenmiş olabilir (menü veya çeviri o anda hazır değildi).
1595
+ * Tetikleyiciler: `setMenuItemsProvider`/`setHomeNavItemProvider`, `translationLoadSuccess`
1596
+ * event'i ve host shell menü signal'i değişimi (`AppLayoutComponent` effect). Public — host
1597
+ * layout menü async yüklenince çağırır ki anasayfa adı `tab.home` fallback'inde takılmasın.
1598
+ */
1599
+ reconcileTabsWithMenu() {
1600
+ const tabs = this.activeRoutes();
1601
+ if (!tabs.length)
1602
+ return;
1603
+ let changed = false;
1604
+ const next = tabs.map((t) => {
1605
+ const info = this.resolveNavInfoFromMenu(t.navLink);
1606
+ if (!info)
1607
+ return t;
1608
+ if (t.navName === info.navName && t.icon === info.icon)
1609
+ return t;
1610
+ changed = true;
1611
+ return { ...t, navName: info.navName, icon: info.icon };
1612
+ });
1613
+ if (changed)
1614
+ this.activeRoutes.set(next);
1615
+ }
1616
+ /**
1617
+ * Menu ağacında verilen path için en spesifik (longest routerLink prefix) eşleşmeyi bulur.
1618
+ * `mw/app-management/view-app` için hem `mw` hem `mw/app-management/view-app` eşleşse,
1619
+ * daha uzun olan kazanır → leaf menu item'ın label/icon'u kullanılır.
1620
+ *
1621
+ * Path leading slash içerebilir (Router URL formatı), menu config routerLink'leri
1622
+ * genelde leading slash'sız (`mw/meters`); ikisini normalize edip karşılaştırırız.
1623
+ */
1624
+ findMenuItemForRoute(items, path) {
1625
+ const normalize = (p) => (p.startsWith('/') ? p.slice(1) : p);
1626
+ const normalizedPath = normalize(path);
1627
+ let best;
1628
+ const visit = (list) => {
1629
+ for (const it of list) {
1630
+ if (it.routerLink) {
1631
+ const r = normalize(it.routerLink);
1632
+ if (r && (normalizedPath === r || normalizedPath.startsWith(r + '/'))) {
1633
+ const length = r.length;
1634
+ if (!best || length > best.length)
1635
+ best = { length, item: it };
1636
+ }
1637
+ }
1638
+ if (it.items?.length)
1639
+ visit(it.items);
1640
+ }
1641
+ };
1642
+ visit(items);
1643
+ return best?.item;
1644
+ }
1645
+ /**
1646
+ * Bir tab'ın stored route handle'ını strategy'den temizler. Key formatı
1647
+ * `mfe:${remoteName}:${tabId}` — strategy `processCleanupKey` matcher key'inden hem
1648
+ * MFE custom element handle'ını hem o tab'a ait child route handle'larını düşürür.
1649
+ */
1650
+ cleanupTabHandle(item) {
1651
+ const remoteName = extractRemoteName(item.navLink);
1652
+ if (!remoteName || !item.tabId)
1653
+ return;
1654
+ console.log(`[NavService] cleanupTabHandle remoteName="${remoteName}" tabId="${item.tabId}"`);
1655
+ this.routeCloseService.deleteStoredRoute(`mfe:${remoteName}:${item.tabId}`);
1656
+ }
1657
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: NavService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1658
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: NavService, providedIn: 'root' }); }
1659
+ }
1660
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: NavService, decorators: [{
1661
+ type: Injectable,
1662
+ args: [{
1663
+ providedIn: 'root'
1664
+ }]
1665
+ }] });
1666
+
1667
+ /**
1668
+ * Cross-injector singleton slot. `NavLinkContextMenuService` `providedIn: 'root'` olduğu
1669
+ * için host ve her MFE remote'unda AYRI instance yaşar — `register(cm)` sadece host
1670
+ * shell'de çağrılır, MFE'lerden gelen `open(...)` çağrıları kendi instance'ında `cm`
1671
+ * bulamaz. Bu yüzden register sırasında host instance'ı global slot'a yazar; MFE
1672
+ * instance'ları `cm` null ise bu slot üzerinden host'a delege eder.
1673
+ *
1674
+ * `bridge` modülündeki `__arilMfeBridge__` ile aynı pattern.
1675
+ */
1676
+ const SLOT = '__arilNavLinkContextMenuOpen';
1677
+ const COMMAND_SLOT = '__arilNavCommand';
1678
+ /**
1679
+ * Tab navigasyonu yapan tüm linkli yapılar için ortak sağ-tık context menu servisi.
1680
+ * Tek bir global PrimeNG `ContextMenu` instance'ı host shell (`AppLayoutComponent`)
1681
+ * template'inde yaşar; her sayfa kendi `ContextMenu` enstantiate etmek yerine bu
1682
+ * service üzerinden `open(event, navItem)` çağırır.
1683
+ *
1684
+ * Servis hangi link tıklandıysa onun `NavItem`'iyle menü modelini yeniden inşa eder
1685
+ * ve `cm.show(event)` ile açar — tüm tüketiciler için tek source-of-truth.
1686
+ *
1687
+ * **Kayıt sözleşmesi**: shell mount olunca `register(cm)` çağırır. MFE'lerden gelen
1688
+ * çağrılar global slot üzerinden host'a delege edilir.
1689
+ */
1690
+ class NavLinkContextMenuService {
1691
+ constructor() {
1692
+ this.navService = inject(NavService);
1693
+ this.translocoService = inject(TranslocoService);
1694
+ this.cm = null;
1695
+ }
1696
+ register(cm) {
1697
+ this.cm = cm;
1698
+ // MFE'lerden gelen open çağrıları için cross-injector slot kur — bu instance'ın
1699
+ // `openLocal`'ını sarmalayan bir function global slot'a yazılır.
1700
+ const slotHost = globalThis;
1701
+ slotHost[SLOT] = (event, navItem) => this.openLocal(event, navItem);
1702
+ // Programatik "yeni tab aç ve geç" — MFE'lerden (eski `window.open('#'+url,'_blank')`
1703
+ // yerine) host NavService'e delege için cross-injector slot.
1704
+ slotHost[COMMAND_SLOT] = (navItem) => this.navService.openInNewTabAndFocus(navItem);
1705
+ }
1706
+ open(event, navItem) {
1707
+ // Kendi cm'imiz varsa (host instance) doğrudan aç
1708
+ if (this.cm) {
1709
+ this.openLocal(event, navItem);
1710
+ return;
1711
+ }
1712
+ // MFE instance: global slot üzerinden host'a delege
1713
+ const slotHost = globalThis;
1714
+ const hostOpen = slotHost[SLOT];
1715
+ if (hostOpen) {
1716
+ hostOpen(event, navItem);
1717
+ }
1718
+ }
1719
+ /**
1720
+ * Programatik "yeni tab aç ve geç" — MFE component'lerinden çağrılır (eski
1721
+ * `window.open('#'+url,'_blank')` yerine). Host instance'ta (`cm` set) doğrudan
1722
+ * `NavService.openInNewTabAndFocus`'u çağırır; MFE instance'ında `__arilNavCommand`
1723
+ * global slot'u üzerinden host'a delege eder. Host shell henüz mount olmadıysa
1724
+ * (slot boş) sessizce no-op olur.
1725
+ */
1726
+ openInNewTabAndFocus(navItem) {
1727
+ if (this.cm) {
1728
+ this.navService.openInNewTabAndFocus(navItem);
1729
+ return;
1730
+ }
1731
+ const slotHost = globalThis;
1732
+ const hostCommand = slotHost[COMMAND_SLOT];
1733
+ if (hostCommand) {
1734
+ hostCommand(navItem);
1735
+ }
1736
+ }
1737
+ openLocal(event, navItem) {
1738
+ if (!this.cm)
1739
+ return;
1740
+ const t = (key) => this.translocoService.translate(key);
1741
+ this.cm.model = [
1742
+ {
1743
+ label: t('navLink.openHere'),
1744
+ icon: 'pi pi-arrow-right',
1745
+ command: () => this.navService.navigateInCurrentTab(navItem)
1746
+ },
1747
+ {
1748
+ label: t('navLink.openInNewTab'),
1749
+ icon: 'pi pi-external-link',
1750
+ command: () => this.navService.openInBackgroundTab(navItem)
1751
+ },
1752
+ {
1753
+ label: t('navLink.openInNewTabAndFocus'),
1754
+ icon: 'pi pi-clone',
1755
+ command: () => this.navService.openInNewTabAndFocus(navItem)
1756
+ }
1757
+ ];
1758
+ this.cm.show(event);
1759
+ }
1760
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: NavLinkContextMenuService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1761
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: NavLinkContextMenuService, providedIn: 'root' }); }
1762
+ }
1763
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: NavLinkContextMenuService, decorators: [{
1764
+ type: Injectable,
1765
+ args: [{ providedIn: 'root' }]
1766
+ }] });
1767
+
1768
+ /**
1769
+ * Linkli her HTML elementine sağ-tık → tab context menüsü ekleyen directive.
1770
+ *
1771
+ * **Kullanım**: navLink/navName (ve opsiyonel icon) bilgisini object literal olarak verin:
1772
+ * ```html
1773
+ * <a [attr.href]="'#/' + path"
1774
+ * [arilNavLink]="{ tabId: '', navLink: path, navName: 'Sayfa Adı', icon: 'pi pi-cog' }"
1775
+ * (click)="...">
1776
+ * ```
1777
+ *
1778
+ * Sağ tık → "Bu sekmede aç / Yeni sekmede aç / Yeni sekmede aç ve geç" menüsü açılır;
1779
+ * komutlar `NavService` üzerinden ilgili tab eylemini tetikler. Ctrl+Click / Middle Click
1780
+ * gibi mevcut shortcut'lar değişmez — bu menü onlara klavye/fare bilgisi olmayan
1781
+ * kullanıcılar için keşfedilebilir bir alternatif.
1782
+ */
1783
+ class NavLinkDirective {
1784
+ constructor() {
1785
+ this.contextMenuService = inject(NavLinkContextMenuService);
1786
+ }
1787
+ onContextMenu(event) {
1788
+ if (!this.navItem?.navLink)
1789
+ return;
1790
+ event.preventDefault();
1791
+ this.contextMenuService.open(event, this.navItem);
1792
+ }
1793
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: NavLinkDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1794
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.1.2", type: NavLinkDirective, isStandalone: true, selector: "[arilNavLink]", inputs: { navItem: ["arilNavLink", "navItem"] }, host: { listeners: { "contextmenu": "onContextMenu($event)" } }, ngImport: i0 }); }
1795
+ }
1796
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: NavLinkDirective, decorators: [{
1797
+ type: Directive,
1798
+ args: [{
1799
+ standalone: true,
1800
+ selector: '[arilNavLink]'
1801
+ }]
1802
+ }], propDecorators: { navItem: [{
1803
+ type: Input,
1804
+ args: ['arilNavLink']
1805
+ }], onContextMenu: [{
1806
+ type: HostListener,
1807
+ args: ['contextmenu', ['$event']]
1808
+ }] } });
1809
+
1810
+ /**
1811
+ * `_tab` query param'ını adres çubuğunda gizleyen `UrlSerializer`. Param Router'ın
1812
+ * internal `UrlTree.queryParams`'ında korunur (parse imzasız, default davranış);
1813
+ * sadece `serialize` çıktısı üzerinden — yani `Router.url`, `NavigationEnd.urlAfterRedirects`
1814
+ * ve browser address bar — `_tab` strip edilir.
1815
+ *
1816
+ * **Sonuç**: `CustomRouteReuseStrategy.getRouteKey` `route.queryParams['_tab']`'ı
1817
+ * normal şekilde okumaya devam eder (parse path'inde değişiklik yok), ama kullanıcının
1818
+ * gördüğü URL temizdir.
1819
+ *
1820
+ * **NavService etkisi**: `activeRoute()` artık `_tab` taşımaz. Aktif tab tespiti
1821
+ * `activeTabId` signal'i üzerinden yapılmalı (NavService bunu `window.history.state`'ten
1822
+ * okur — `navigateByUrl(url, { state: { _tab } })` ile her navigation'da set edilir).
1823
+ */
1824
+ class TabAwareUrlSerializer extends DefaultUrlSerializer {
1825
+ /** `parse` default davranışla bırakıldı — input URL'inde `_tab` varsa korunur (refresh deep link). */
1826
+ serialize(tree) {
1827
+ // Default serialize'ı tut, sonra `_tab`'ı string seviyesinde (split/filter) çıkar.
1828
+ // `URLSearchParams` round-trip'inden KAÇINIYORUZ: diğer query param'ların Angular
1829
+ // encoding'ini bozar (örn. `a%20b` → `a+b`) ve bu string `Router.url`/`NavigationEnd`
1830
+ // olarak kullanıldığı için `cleanNavLink` karşılaştırmaları kaçabilirdi. Diğer param'lara
1831
+ // HİÇ dokunmuyoruz — yalnız `_tab` segment'ini düşürüyoruz.
1832
+ const url = super.serialize(tree);
1833
+ if (!url.includes('_tab='))
1834
+ return url;
1835
+ const fragmentIdx = url.indexOf('#');
1836
+ const fragment = fragmentIdx >= 0 ? url.slice(fragmentIdx) : '';
1837
+ const beforeFragment = fragmentIdx >= 0 ? url.slice(0, fragmentIdx) : url;
1838
+ const queryIdx = beforeFragment.indexOf('?');
1839
+ if (queryIdx < 0)
1840
+ return url;
1841
+ const path = beforeFragment.slice(0, queryIdx);
1842
+ const query = beforeFragment
1843
+ .slice(queryIdx + 1)
1844
+ .split('&')
1845
+ .filter((p) => p !== '_tab' && !p.startsWith('_tab='))
1846
+ .join('&');
1847
+ return query ? `${path}?${query}${fragment}` : `${path}${fragment}`;
1848
+ }
1849
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: TabAwareUrlSerializer, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
1850
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: TabAwareUrlSerializer, providedIn: 'root' }); }
1851
+ }
1852
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.2", ngImport: i0, type: TabAwareUrlSerializer, decorators: [{
1853
+ type: Injectable,
1854
+ args: [{ providedIn: 'root' }]
1855
+ }] });
188
1856
 
189
1857
  /**
190
1858
  * Generated bundle index. Do not edit.
191
1859
  */
192
1860
 
193
- export { Apps, ArilReuseStrategy, MicroAppService };
1861
+ export { Apps, CustomRouteReuseStrategy, MicroAppService, NavLinkContextMenuService, NavLinkDirective, NavService, RouteCloseService, TabAwareUrlSerializer, safeNavigate };
194
1862
  //# sourceMappingURL=aril-boot-config-apps.mjs.map