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
|
+
}
|