aril 2.0.1-dev.5 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,138 +1,138 @@
1
- import { Injectable, Injector, effect, inject } from '@angular/core';
2
-
3
- import { KeycloakService } from 'keycloak-angular';
4
-
5
- import { NavItem, NavService } from 'aril/boot/config/apps';
6
-
7
- /**
8
- * localStorage'a serialize edilen pinned tab kaydı. `pinned` alanı SAKLANMAZ
9
- * (yüklenen her kayıt zaten pinned'dir). `tabId` SAKLANIR — F5 sonrası
10
- * `window.history.state._tab` ile eşleşip `NavService.activeRoute` handler'ının
11
- * aynı tab'ı tekrar türetmesini (duplicate) engeller.
12
- */
13
- interface PersistedTab {
14
- tabId: string;
15
- navLink: string;
16
- navName: string;
17
- icon?: string;
18
- }
19
-
20
- /**
21
- * Sabitlenmiş (pinned) tab'ları kullanıcı bazlı `localStorage` anahtarında kalıcı
22
- * kılar. F5 / yeniden login / yeni sekme açılışında pinned tab'lar geri yüklenir;
23
- * non-pinned tab'lar saklanmaz.
24
- *
25
- * **Akış**: `AppLayoutComponent` constructor'da (menü/home provider'ları set
26
- * edildikten SONRA) `restore()` SENKRON çağrılır → pinned'ler `NavService.activeRoutes`'a
27
- * eklenir → save effect armlanır. Save effect `restore()` ÖNCESİ kurulmaz; aksi
28
- * halde Angular effect'in ilk (eager) tick'i boş `activeRoutes`'u yazıp saklı
29
- * pinned'leri silerdi.
30
- *
31
- * **Duplicate önlemi**: Aktif tab pinned'di ise restore SAKLI `tabId` ile ekler;
32
- * F5 sonrası korunan `history.state._tab` bununla eşleşir → handler türetmez, sadece
33
- * focus eder. Aktif tab pinned değildiyse eşleşme olmaz → handler gelen URL'i yeni
34
- * aktif tab olarak ekler (deep-link gereksinimi: "kayıtlıları yükle + URL'i ekle").
35
- */
36
- @Injectable({ providedIn: 'root' })
37
- export class TabSessionService {
38
- private readonly navService = inject(NavService);
39
- private readonly keycloak = inject(KeycloakService);
40
- private readonly injector = inject(Injector);
41
-
42
- private readonly STORAGE_PREFIX = 'tab-session:';
43
- /** `NavService.MAX_TABS` ile aynı tavan — restore ve save iki tarafta da sınırlanır. */
44
- private readonly MAX_PERSISTED = 20;
45
-
46
- private restored = false;
47
- /** Son yazılan payload — değişmediyse gereksiz `localStorage` write'ı atlanır. */
48
- private lastWritten = '';
49
-
50
- /**
51
- * Kullanıcı bazlı storage anahtarı. Mevcut kod (`app.profilesidebar.component.ts`)
52
- * `idTokenParsed.sub` kullanıyor; her iki token şeklini de kapsamak için fallback
53
- * zinciri — kullanıcı çözülemezse `anonymous`.
54
- */
55
- private storageKey(): string {
56
- const kc = this.keycloak.getKeycloakInstance();
57
- const sub = kc?.tokenParsed?.sub ?? kc?.idTokenParsed?.sub ?? 'anonymous';
58
- return this.STORAGE_PREFIX + sub;
59
- }
60
-
61
- /**
62
- * SENKRON restore. `AppLayoutComponent` constructor'dan, ilk `NavigationEnd`
63
- * işlenmeden ÖNCE çağrılmalı — böylece `NavService.activeRoute` handler'ının
64
- * `knownTab` guard'ı restore edilen tabId'leri görür. Idempotent: ikinci çağrı no-op.
65
- */
66
- restore(): void {
67
- if (this.restored) return;
68
- const persisted = this.load();
69
- const existingIds = new Set(this.navService.activeRoutes().map((t) => t.tabId));
70
- for (const item of persisted) {
71
- // Aynı tabId zaten ekliyse atla (idempotency / olası çift çağrı koruması).
72
- if (existingIds.has(item.tabId)) continue;
73
- this.navService.addToActiveRoutes({
74
- tabId: item.tabId,
75
- navLink: item.navLink,
76
- navName: item.navName,
77
- icon: item.icon,
78
- pinned: true
79
- });
80
- }
81
- this.restored = true;
82
- this.armSave();
83
- }
84
-
85
- /**
86
- * Save effect'ini `restore()` SONRASI kurar. İlk çalıştığında `activeRoutes` zaten
87
- * restore edilmiş pinned'leri içerdiği için yazım idempotent kalır. Effect, service'in
88
- * kendi `Injector`'ına bağlanır (field initializer / constructor dışında kurulduğu için
89
- * explicit injector zorunlu).
90
- */
91
- private armSave(): void {
92
- effect(
93
- () => {
94
- const pinned = this.navService.activeRoutes().filter((t) => t.pinned);
95
- this.save(pinned);
96
- },
97
- { injector: this.injector }
98
- );
99
- }
100
-
101
- private save(pinned: NavItem[]): void {
102
- try {
103
- const payload: PersistedTab[] = pinned
104
- .slice(0, this.MAX_PERSISTED)
105
- .map((t) => ({ tabId: t.tabId, navLink: t.navLink, navName: t.navName, icon: t.icon }));
106
- const serialized = JSON.stringify(payload);
107
- if (serialized === this.lastWritten) return;
108
- this.lastWritten = serialized;
109
- localStorage.setItem(this.storageKey(), serialized);
110
- } catch {
111
- /* quota / serialize hatası: kritik değil, persist sessizce atlanır */
112
- }
113
- }
114
-
115
- private load(): PersistedTab[] {
116
- try {
117
- const raw = localStorage.getItem(this.storageKey());
118
- if (!raw) return [];
119
- const parsed: unknown = JSON.parse(raw);
120
- if (!Array.isArray(parsed)) return [];
121
- return parsed.filter((x): x is PersistedTab => this.isValidPersistedTab(x));
122
- } catch {
123
- return []; // bozuk JSON → yok say, uygulama normal açılır
124
- }
125
- }
126
-
127
- private isValidPersistedTab(x: unknown): x is PersistedTab {
128
- if (!x || typeof x !== 'object') return false;
129
- const t = x as Record<string, unknown>;
130
- return (
131
- typeof t['tabId'] === 'string' &&
132
- t['tabId'].length > 0 &&
133
- typeof t['navLink'] === 'string' &&
134
- t['navLink'].length > 0 &&
135
- typeof t['navName'] === 'string'
136
- );
137
- }
138
- }
1
+ import { Injectable, Injector, effect, inject } from '@angular/core';
2
+
3
+ import { KeycloakService } from 'keycloak-angular';
4
+
5
+ import { NavItem, NavService } from 'aril/boot/config/apps';
6
+
7
+ /**
8
+ * localStorage'a serialize edilen pinned tab kaydı. `pinned` alanı SAKLANMAZ
9
+ * (yüklenen her kayıt zaten pinned'dir). `tabId` SAKLANIR — F5 sonrası
10
+ * `window.history.state._tab` ile eşleşip `NavService.activeRoute` handler'ının
11
+ * aynı tab'ı tekrar türetmesini (duplicate) engeller.
12
+ */
13
+ interface PersistedTab {
14
+ tabId: string;
15
+ navLink: string;
16
+ navName: string;
17
+ icon?: string;
18
+ }
19
+
20
+ /**
21
+ * Sabitlenmiş (pinned) tab'ları kullanıcı bazlı `localStorage` anahtarında kalıcı
22
+ * kılar. F5 / yeniden login / yeni sekme açılışında pinned tab'lar geri yüklenir;
23
+ * non-pinned tab'lar saklanmaz.
24
+ *
25
+ * **Akış**: `AppLayoutComponent` constructor'da (menü/home provider'ları set
26
+ * edildikten SONRA) `restore()` SENKRON çağrılır → pinned'ler `NavService.activeRoutes`'a
27
+ * eklenir → save effect armlanır. Save effect `restore()` ÖNCESİ kurulmaz; aksi
28
+ * halde Angular effect'in ilk (eager) tick'i boş `activeRoutes`'u yazıp saklı
29
+ * pinned'leri silerdi.
30
+ *
31
+ * **Duplicate önlemi**: Aktif tab pinned'di ise restore SAKLI `tabId` ile ekler;
32
+ * F5 sonrası korunan `history.state._tab` bununla eşleşir → handler türetmez, sadece
33
+ * focus eder. Aktif tab pinned değildiyse eşleşme olmaz → handler gelen URL'i yeni
34
+ * aktif tab olarak ekler (deep-link gereksinimi: "kayıtlıları yükle + URL'i ekle").
35
+ */
36
+ @Injectable({ providedIn: 'root' })
37
+ export class TabSessionService {
38
+ private readonly navService = inject(NavService);
39
+ private readonly keycloak = inject(KeycloakService);
40
+ private readonly injector = inject(Injector);
41
+
42
+ private readonly STORAGE_PREFIX = 'tab-session:';
43
+ /** `NavService.MAX_TABS` ile aynı tavan — restore ve save iki tarafta da sınırlanır. */
44
+ private readonly MAX_PERSISTED = 20;
45
+
46
+ private restored = false;
47
+ /** Son yazılan payload — değişmediyse gereksiz `localStorage` write'ı atlanır. */
48
+ private lastWritten = '';
49
+
50
+ /**
51
+ * Kullanıcı bazlı storage anahtarı. Mevcut kod (`app.profilesidebar.component.ts`)
52
+ * `idTokenParsed.sub` kullanıyor; her iki token şeklini de kapsamak için fallback
53
+ * zinciri — kullanıcı çözülemezse `anonymous`.
54
+ */
55
+ private storageKey(): string {
56
+ const kc = this.keycloak.getKeycloakInstance();
57
+ const sub = kc?.tokenParsed?.sub ?? kc?.idTokenParsed?.sub ?? 'anonymous';
58
+ return this.STORAGE_PREFIX + sub;
59
+ }
60
+
61
+ /**
62
+ * SENKRON restore. `AppLayoutComponent` constructor'dan, ilk `NavigationEnd`
63
+ * işlenmeden ÖNCE çağrılmalı — böylece `NavService.activeRoute` handler'ının
64
+ * `knownTab` guard'ı restore edilen tabId'leri görür. Idempotent: ikinci çağrı no-op.
65
+ */
66
+ restore(): void {
67
+ if (this.restored) return;
68
+ const persisted = this.load();
69
+ const existingIds = new Set(this.navService.activeRoutes().map((t) => t.tabId));
70
+ for (const item of persisted) {
71
+ // Aynı tabId zaten ekliyse atla (idempotency / olası çift çağrı koruması).
72
+ if (existingIds.has(item.tabId)) continue;
73
+ this.navService.addToActiveRoutes({
74
+ tabId: item.tabId,
75
+ navLink: item.navLink,
76
+ navName: item.navName,
77
+ icon: item.icon,
78
+ pinned: true
79
+ });
80
+ }
81
+ this.restored = true;
82
+ this.armSave();
83
+ }
84
+
85
+ /**
86
+ * Save effect'ini `restore()` SONRASI kurar. İlk çalıştığında `activeRoutes` zaten
87
+ * restore edilmiş pinned'leri içerdiği için yazım idempotent kalır. Effect, service'in
88
+ * kendi `Injector`'ına bağlanır (field initializer / constructor dışında kurulduğu için
89
+ * explicit injector zorunlu).
90
+ */
91
+ private armSave(): void {
92
+ effect(
93
+ () => {
94
+ const pinned = this.navService.activeRoutes().filter((t) => t.pinned);
95
+ this.save(pinned);
96
+ },
97
+ { injector: this.injector }
98
+ );
99
+ }
100
+
101
+ private save(pinned: NavItem[]): void {
102
+ try {
103
+ const payload: PersistedTab[] = pinned
104
+ .slice(0, this.MAX_PERSISTED)
105
+ .map((t) => ({ tabId: t.tabId, navLink: t.navLink, navName: t.navName, icon: t.icon }));
106
+ const serialized = JSON.stringify(payload);
107
+ if (serialized === this.lastWritten) return;
108
+ this.lastWritten = serialized;
109
+ localStorage.setItem(this.storageKey(), serialized);
110
+ } catch {
111
+ /* quota / serialize hatası: kritik değil, persist sessizce atlanır */
112
+ }
113
+ }
114
+
115
+ private load(): PersistedTab[] {
116
+ try {
117
+ const raw = localStorage.getItem(this.storageKey());
118
+ if (!raw) return [];
119
+ const parsed: unknown = JSON.parse(raw);
120
+ if (!Array.isArray(parsed)) return [];
121
+ return parsed.filter((x): x is PersistedTab => this.isValidPersistedTab(x));
122
+ } catch {
123
+ return []; // bozuk JSON → yok say, uygulama normal açılır
124
+ }
125
+ }
126
+
127
+ private isValidPersistedTab(x: unknown): x is PersistedTab {
128
+ if (!x || typeof x !== 'object') return false;
129
+ const t = x as Record<string, unknown>;
130
+ return (
131
+ typeof t['tabId'] === 'string' &&
132
+ t['tabId'].length > 0 &&
133
+ typeof t['navLink'] === 'string' &&
134
+ t['navLink'].length > 0 &&
135
+ typeof t['navName'] === 'string'
136
+ );
137
+ }
138
+ }