aril 1.2.18 → 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.
- package/boot/bridge/src/mfe-bridge.d.ts +42 -2
- package/boot/config/apps/index.d.ts +7 -1
- package/boot/config/apps/src/custom-reuse-outlet.component.d.ts +37 -0
- package/boot/config/apps/src/custom-route-reuse-strategy.class.d.ts +156 -0
- package/boot/config/apps/src/nav-link-context-menu.service.d.ts +33 -0
- package/boot/config/apps/src/nav-link.directive.d.ts +29 -0
- package/boot/config/apps/src/nav.service.d.ts +198 -0
- package/boot/config/apps/src/route-close.service.d.ts +9 -0
- package/boot/config/apps/src/safe-navigate.d.ts +17 -0
- package/boot/config/apps/src/tab-aware-url-serializer.d.ts +22 -0
- package/boot/config/plugins/src/getNgZone.d.ts +9 -1
- package/boot/mfe/src/app.component.d.ts +15 -4
- package/boot/mfe/src/isolated-location-strategy.d.ts +57 -0
- package/esm2022/boot/bridge/src/mfe-bridge.mjs +36 -5
- package/esm2022/boot/config/api/src/api.service.mjs +12 -3
- package/esm2022/boot/config/apps/index.mjs +8 -2
- package/esm2022/boot/config/apps/src/apps.service.mjs +14 -6
- package/esm2022/boot/config/apps/src/custom-reuse-outlet.component.mjs +207 -0
- package/esm2022/boot/config/apps/src/custom-route-reuse-strategy.class.mjs +540 -0
- package/esm2022/boot/config/apps/src/nav-link-context-menu.service.mjs +105 -0
- package/esm2022/boot/config/apps/src/nav-link.directive.mjs +45 -0
- package/esm2022/boot/config/apps/src/nav.service.mjs +675 -0
- package/esm2022/boot/config/apps/src/route-close.service.mjs +19 -0
- package/esm2022/boot/config/apps/src/safe-navigate.mjs +50 -0
- package/esm2022/boot/config/apps/src/tab-aware-url-serializer.mjs +50 -0
- package/esm2022/boot/config/plugins/src/getNgZone.mjs +13 -5
- package/esm2022/boot/host/src/app.component.mjs +1 -2
- package/esm2022/boot/host/src/bootstrap.mjs +22 -7
- package/esm2022/boot/mfe/src/app.component.mjs +143 -39
- package/esm2022/boot/mfe/src/bootstrap.mjs +197 -20
- package/esm2022/boot/mfe/src/isolated-location-strategy.mjs +142 -0
- package/esm2022/keycloak/src/auth.interceptor.mjs +17 -2
- package/esm2022/provider/src/prodiveHost.mjs +3 -5
- package/esm2022/provider/src/prodiveHostRouter.mjs +88 -9
- package/esm2022/theme/layout/app/expandableMenu/expandable-menu.component.mjs +81 -19
- package/esm2022/theme/layout/app/favorite-pages/favorite-pages-sidebar.component.mjs +6 -4
- package/esm2022/theme/layout/app/general-search/general-search.component.mjs +4 -4
- package/esm2022/theme/layout/app/history/history-sidebar.component.mjs +6 -4
- package/esm2022/theme/layout/app/layout/app.layout.component.mjs +422 -20
- package/esm2022/theme/layout/app/layout/mfe.layout.component.mjs +24 -35
- package/esm2022/theme/layout/app/site-map/site-map-sidebar.component.mjs +6 -4
- package/esm2022/theme/layout/app/static-sidebar/static-sidebar.component.mjs +85 -27
- package/esm2022/theme/layout/app/topbar/app.topbar.component.mjs +3 -3
- package/esm2022/theme/layout/service/breadcrumb-publisher.service.mjs +86 -0
- package/esm2022/theme/layout/service/tab-session.service.mjs +126 -0
- package/esm2022/ui-business/ref-value/src/ref-value.component.mjs +15 -7
- package/esm2022/util/sync-active-tab-route/src/sync-active-tab-route.directive.mjs +29 -9
- package/fesm2022/aril-app.component-s14ruALV.mjs +183 -0
- package/fesm2022/aril-app.component-s14ruALV.mjs.map +1 -0
- package/fesm2022/aril-boot-bridge.mjs +35 -4
- package/fesm2022/aril-boot-bridge.mjs.map +1 -1
- package/fesm2022/aril-boot-config-api.mjs +11 -2
- package/fesm2022/aril-boot-config-api.mjs.map +1 -1
- package/fesm2022/aril-boot-config-apps.mjs +1678 -10
- package/fesm2022/aril-boot-config-apps.mjs.map +1 -1
- package/fesm2022/aril-boot-config-plugins.mjs +12 -4
- package/fesm2022/aril-boot-config-plugins.mjs.map +1 -1
- package/fesm2022/aril-boot-host.mjs +21 -7
- package/fesm2022/aril-boot-host.mjs.map +1 -1
- package/fesm2022/aril-boot-mfe-app.component-a34GeuUv.mjs +183 -0
- package/fesm2022/aril-boot-mfe-app.component-a34GeuUv.mjs.map +1 -0
- package/fesm2022/aril-boot-mfe-aril-boot-mfe-KFO_X7yR.mjs +631 -0
- package/fesm2022/aril-boot-mfe-aril-boot-mfe-KFO_X7yR.mjs.map +1 -0
- package/fesm2022/aril-boot-mfe.mjs +5 -3
- package/fesm2022/aril-boot-mfe.mjs.map +1 -1
- package/fesm2022/aril-keycloak.mjs +16 -1
- package/fesm2022/aril-keycloak.mjs.map +1 -1
- package/fesm2022/aril-provider.mjs +90 -12
- package/fesm2022/aril-provider.mjs.map +1 -1
- package/fesm2022/aril-theme-layout.mjs +2630 -2017
- package/fesm2022/aril-theme-layout.mjs.map +1 -1
- package/fesm2022/aril-ui-business-ref-value.mjs +14 -6
- package/fesm2022/aril-ui-business-ref-value.mjs.map +1 -1
- package/fesm2022/aril-util-sync-active-tab-route.mjs +28 -8
- package/fesm2022/aril-util-sync-active-tab-route.mjs.map +1 -1
- package/fesm2022/aril.mjs +354 -25
- package/fesm2022/aril.mjs.map +1 -1
- package/keycloak/src/auth.interceptor.d.ts +7 -0
- package/package.json +216 -216
- package/provider/src/prodiveHost.d.ts +1 -0
- package/theme/layout/app/expandableMenu/expandable-menu.component.d.ts +21 -4
- package/theme/layout/app/expandableMenu/expandable-menu.component.html +19 -5
- package/theme/layout/app/expandableMenu/expandable-menu.component.ts +69 -9
- package/theme/layout/app/favorite-pages/favorite-pages-sidebar.component.html +1 -0
- package/theme/layout/app/favorite-pages/favorite-pages-sidebar.component.ts +3 -1
- package/theme/layout/app/general-search/general-search.component.html +2 -1
- package/theme/layout/app/general-search/general-search.component.ts +2 -2
- package/theme/layout/app/history/history-sidebar.component.html +3 -1
- package/theme/layout/app/history/history-sidebar.component.ts +3 -1
- package/theme/layout/app/layout/app.layout.component.d.ts +105 -5
- package/theme/layout/app/layout/app.layout.component.html +102 -1
- package/theme/layout/app/layout/app.layout.component.scss +372 -0
- package/theme/layout/app/layout/app.layout.component.ts +452 -13
- package/theme/layout/app/layout/mfe.layout.component.d.ts +7 -5
- package/theme/layout/app/layout/mfe.layout.component.ts +13 -39
- package/theme/layout/app/site-map/site-map-sidebar.component.html +1 -0
- package/theme/layout/app/site-map/site-map-sidebar.component.ts +3 -1
- package/theme/layout/app/static-sidebar/static-sidebar.component.d.ts +26 -5
- package/theme/layout/app/static-sidebar/static-sidebar.component.html +11 -5
- package/theme/layout/app/static-sidebar/static-sidebar.component.ts +68 -13
- package/theme/layout/app/topbar/app.topbar.component.html +0 -1
- package/theme/layout/app/topbar/app.topbar.component.scss +1 -1
- package/theme/layout/service/breadcrumb-publisher.service.d.ts +24 -0
- package/theme/layout/service/breadcrumb-publisher.service.ts +95 -0
- package/theme/layout/service/tab-session.service.d.ts +52 -0
- package/theme/layout/service/tab-session.service.ts +138 -0
- package/theme/styles/layout/_breadcrumb.scss +95 -0
- package/theme/styles/layout/_content.scss +2 -2
- package/ui-business/ref-value/src/ref-value.component.d.ts +4 -2
- package/util/sync-active-tab-route/src/sync-active-tab-route.directive.d.ts +15 -2
- package/boot/config/apps/src/reuse-strategy.d.ts +0 -4
- package/esm2022/boot/config/apps/src/reuse-strategy.mjs +0 -9
- package/esm2022/theme/layout/app/breadcrumb/app.breadcrumb.component.mjs +0 -107
- package/fesm2022/aril-app.component-wxP3y8dg.mjs +0 -81
- package/fesm2022/aril-app.component-wxP3y8dg.mjs.map +0 -1
- package/fesm2022/aril-boot-mfe-app.component-7IjAmjz0.mjs +0 -80
- package/fesm2022/aril-boot-mfe-app.component-7IjAmjz0.mjs.map +0 -1
- package/fesm2022/aril-boot-mfe-aril-boot-mfe-KXDpUyv7.mjs +0 -315
- package/fesm2022/aril-boot-mfe-aril-boot-mfe-KXDpUyv7.mjs.map +0 -1
- package/theme/layout/app/breadcrumb/app.breadcrumb.component.d.ts +0 -25
- package/theme/layout/app/breadcrumb/app.breadcrumb.component.html +0 -8
- 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,
|
|
3
|
-
import
|
|
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((
|
|
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(
|
|
123
|
-
component:
|
|
124
|
-
data:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1861
|
+
export { Apps, CustomRouteReuseStrategy, MicroAppService, NavLinkContextMenuService, NavLinkDirective, NavService, RouteCloseService, TabAwareUrlSerializer, safeNavigate };
|
|
194
1862
|
//# sourceMappingURL=aril-boot-config-apps.mjs.map
|