web-mojo 2.2.57 → 2.2.59
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/dist/admin.cjs.js +1 -1
- package/dist/admin.cjs.js.map +1 -1
- package/dist/admin.es.js +1 -10105
- package/dist/admin.es.js.map +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.es.js +1 -588
- package/dist/auth.es.js.map +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -571
- package/dist/charts.es.js.map +1 -1
- package/dist/chunks/ChatView-D4A9rIX3.js +2 -0
- package/dist/chunks/ChatView-D4A9rIX3.js.map +1 -0
- package/dist/chunks/ChatView-nxaq8aIo.js +2 -0
- package/dist/chunks/ChatView-nxaq8aIo.js.map +1 -0
- package/dist/chunks/Collection-1sPoIFvQ.js +2 -0
- package/dist/chunks/{Collection-DaiL0uGl.js.map → Collection-1sPoIFvQ.js.map} +1 -1
- package/dist/chunks/{Collection-CxbNKOas.js → Collection-DSBRXpwK.js} +2 -2
- package/dist/chunks/{Collection-CxbNKOas.js.map → Collection-DSBRXpwK.js.map} +1 -1
- package/dist/chunks/{ContextMenu-ClwHEbbD.js → ContextMenu-BWy7WqF4.js} +2 -2
- package/dist/chunks/{ContextMenu-ClwHEbbD.js.map → ContextMenu-BWy7WqF4.js.map} +1 -1
- package/dist/chunks/ContextMenu-BvniQz-N.js +3 -0
- package/dist/chunks/{ContextMenu-sgvgSACY.js.map → ContextMenu-BvniQz-N.js.map} +1 -1
- package/dist/chunks/DataView--nUWtq6r.js +2 -0
- package/dist/chunks/{DataView-Dzo0jbs2.js.map → DataView--nUWtq6r.js.map} +1 -1
- package/dist/chunks/{DataView-1xh3GFeC.js → DataView-CK3Z0TJH.js} +2 -2
- package/dist/chunks/{DataView-1xh3GFeC.js.map → DataView-CK3Z0TJH.js.map} +1 -1
- package/dist/chunks/Dialog-BcgSR01Z.js +2 -0
- package/dist/chunks/{Dialog-DOGDalUq.js.map → Dialog-BcgSR01Z.js.map} +1 -1
- package/dist/chunks/{Dialog-CQlTDhZS.js → Dialog-DwCTFV6O.js} +2 -2
- package/dist/chunks/{Dialog-CQlTDhZS.js.map → Dialog-DwCTFV6O.js.map} +1 -1
- package/dist/chunks/FormPlugins-DvQ-G5J5.js +2 -0
- package/dist/chunks/{FormPlugins-DY6e88YT.js.map → FormPlugins-DvQ-G5J5.js.map} +1 -1
- package/dist/chunks/{FormView-DaKA4Sys.js → FormView-CRmEReTC.js} +3 -3
- package/dist/chunks/{FormView-DaKA4Sys.js.map → FormView-CRmEReTC.js.map} +1 -1
- package/dist/chunks/FormView-OLA7t-yv.js +3 -0
- package/dist/chunks/{FormView-Dz3mYasQ.js.map → FormView-OLA7t-yv.js.map} +1 -1
- package/dist/chunks/ListView-6JQ6tRXs.js +2 -0
- package/dist/chunks/{ListView-X5w5jf51.js.map → ListView-6JQ6tRXs.js.map} +1 -1
- package/dist/chunks/{ListView-CDzKIpd8.js → ListView-DVStKiMi.js} +2 -2
- package/dist/chunks/{ListView-CDzKIpd8.js.map → ListView-DVStKiMi.js.map} +1 -1
- package/dist/chunks/{MetricsCountryMapView-Dx2cw7ya.js → MetricsCountryMapView-CnAEbUw_.js} +2 -2
- package/dist/chunks/{MetricsCountryMapView-Dx2cw7ya.js.map → MetricsCountryMapView-CnAEbUw_.js.map} +1 -1
- package/dist/chunks/MetricsCountryMapView-J067qrrt.js +2 -0
- package/dist/chunks/{MetricsCountryMapView-B2xz6zUw.js.map → MetricsCountryMapView-J067qrrt.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-CBuso0OE.js → MetricsMiniChartWidget-BeD1slGs.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-CBuso0OE.js.map → MetricsMiniChartWidget-BeD1slGs.js.map} +1 -1
- package/dist/chunks/MetricsMiniChartWidget-x2gFjHOU.js +2 -0
- package/dist/chunks/{MetricsMiniChartWidget-DvKd7Qrk.js.map → MetricsMiniChartWidget-x2gFjHOU.js.map} +1 -1
- package/dist/chunks/PDFViewer-CsyKn-gh.js +2 -0
- package/dist/chunks/{PDFViewer-EJ9cOfPF.js.map → PDFViewer-CsyKn-gh.js.map} +1 -1
- package/dist/chunks/{PDFViewer-ofMGdSaj.js → PDFViewer-DSa4BZCm.js} +2 -2
- package/dist/chunks/{PDFViewer-ofMGdSaj.js.map → PDFViewer-DSa4BZCm.js.map} +1 -1
- package/dist/chunks/Rest-DHbszkuP.js +2 -0
- package/dist/chunks/Rest-DHbszkuP.js.map +1 -0
- package/dist/chunks/Rest-Ds9e8tN8.js +2 -0
- package/dist/chunks/Rest-Ds9e8tN8.js.map +1 -0
- package/dist/chunks/TokenManager-D6SjKgPZ.js +2 -0
- package/dist/chunks/{TokenManager-DoN9e6q6.js.map → TokenManager-D6SjKgPZ.js.map} +1 -1
- package/dist/chunks/{TokenManager-Gqvj7SDX.js → TokenManager-REbha1Le.js} +2 -2
- package/dist/chunks/{TokenManager-Gqvj7SDX.js.map → TokenManager-REbha1Le.js.map} +1 -1
- package/dist/chunks/WebApp-CULZpO_0.js +2 -0
- package/dist/chunks/{WebApp-6qvqmOts.js.map → WebApp-CULZpO_0.js.map} +1 -1
- package/dist/chunks/{WebApp-_dgpwtFw.js → WebApp-DovLtA60.js} +2 -2
- package/dist/chunks/{WebApp-_dgpwtFw.js.map → WebApp-DovLtA60.js.map} +1 -1
- package/dist/chunks/WebSocketClient-B-wc3mez.js +2 -0
- package/dist/chunks/{WebSocketClient-DG2olXpH.js.map → WebSocketClient-B-wc3mez.js.map} +1 -1
- package/dist/chunks/{WebSocketClient-MFkFlSue.js → WebSocketClient-BdZ9QYll.js} +2 -2
- package/dist/chunks/{WebSocketClient-MFkFlSue.js.map → WebSocketClient-BdZ9QYll.js.map} +1 -1
- package/dist/chunks/version-C3dnl1bg.js +2 -0
- package/dist/chunks/version-C3dnl1bg.js.map +1 -0
- package/dist/chunks/{version-BVADfTA5.js → version-ioN546cp.js} +2 -2
- package/dist/chunks/{version-BVADfTA5.js.map → version-ioN546cp.js.map} +1 -1
- package/dist/css/web-mojo.css +1 -1
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.es.js +1 -957
- package/dist/docit.es.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -3252
- package/dist/index.es.js.map +1 -1
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.es.js +1 -3737
- package/dist/lightbox.es.js.map +1 -1
- package/dist/loader.umd.js +2 -2
- package/dist/map.cjs.js +1 -1
- package/dist/map.es.js +1 -1032
- package/dist/map.es.js.map +1 -1
- package/dist/mojo-auth.es.js +338 -0
- package/dist/mojo-auth.umd.js +1 -0
- package/dist/timeline.cjs.js +1 -1
- package/dist/timeline.es.js +1 -224
- package/dist/timeline.es.js.map +1 -1
- package/dist/web-mojo.lite.iife.js +14 -3
- package/dist/web-mojo.lite.iife.js.map +1 -1
- package/dist/web-mojo.lite.iife.min.js +6 -6
- package/dist/web-mojo.lite.iife.min.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunks/ChatView-9k6xBWXk.js +0 -7632
- package/dist/chunks/ChatView-9k6xBWXk.js.map +0 -1
- package/dist/chunks/ChatView-CdtuCDYm.js +0 -2
- package/dist/chunks/ChatView-CdtuCDYm.js.map +0 -1
- package/dist/chunks/Collection-DaiL0uGl.js +0 -1014
- package/dist/chunks/ContextMenu-sgvgSACY.js +0 -1535
- package/dist/chunks/DataView-Dzo0jbs2.js +0 -862
- package/dist/chunks/Dialog-DOGDalUq.js +0 -1579
- package/dist/chunks/FormPlugins-DY6e88YT.js +0 -124
- package/dist/chunks/FormView-Dz3mYasQ.js +0 -8636
- package/dist/chunks/ListView-X5w5jf51.js +0 -495
- package/dist/chunks/MetricsCountryMapView-B2xz6zUw.js +0 -1054
- package/dist/chunks/MetricsMiniChartWidget-DvKd7Qrk.js +0 -3283
- package/dist/chunks/PDFViewer-EJ9cOfPF.js +0 -946
- package/dist/chunks/Rest-CgSjfMaU.js +0 -2
- package/dist/chunks/Rest-CgSjfMaU.js.map +0 -1
- package/dist/chunks/Rest-W-sPfGh9.js +0 -4375
- package/dist/chunks/Rest-W-sPfGh9.js.map +0 -1
- package/dist/chunks/TokenManager-DoN9e6q6.js +0 -1423
- package/dist/chunks/WebApp-6qvqmOts.js +0 -1386
- package/dist/chunks/WebSocketClient-DG2olXpH.js +0 -209
- package/dist/chunks/version-OyPGnx30.js +0 -38
- package/dist/chunks/version-OyPGnx30.js.map +0 -1
|
@@ -1,1423 +0,0 @@
|
|
|
1
|
-
import { V as View } from "./Rest-W-sPfGh9.js";
|
|
2
|
-
import Dialog from "./Dialog-DOGDalUq.js";
|
|
3
|
-
import { G as GroupList } from "./ContextMenu-sgvgSACY.js";
|
|
4
|
-
class ResultsView extends View {
|
|
5
|
-
constructor(options = {}) {
|
|
6
|
-
super({
|
|
7
|
-
className: "search-results-view flex-grow-1 overflow-auto d-flex flex-column",
|
|
8
|
-
template: `
|
|
9
|
-
<div id="results-container" class="flex-grow-1 overflow-auto">
|
|
10
|
-
{{#data.loading}}
|
|
11
|
-
<div class="text-center p-4">
|
|
12
|
-
<div class="spinner-border spinner-border-sm text-muted" role="status">
|
|
13
|
-
<span class="visually-hidden">Loading...</span>
|
|
14
|
-
</div>
|
|
15
|
-
<div class="mt-2 small text-muted">{{data.loadingText}}</div>
|
|
16
|
-
</div>
|
|
17
|
-
{{/data.loading}}
|
|
18
|
-
|
|
19
|
-
{{^data.loading}}
|
|
20
|
-
{{#data.items}}
|
|
21
|
-
<div class="simple-search-item position-relative"
|
|
22
|
-
data-action="select-item"
|
|
23
|
-
data-item-index="{{index}}">
|
|
24
|
-
{{{itemContent}}}
|
|
25
|
-
|
|
26
|
-
</div>
|
|
27
|
-
{{/data.items}}
|
|
28
|
-
|
|
29
|
-
{{#data.showNoResults}}
|
|
30
|
-
<div class="text-center p-4">
|
|
31
|
-
<i class="bi bi-search text-muted mb-2" style="font-size: 1.5rem;"></i>
|
|
32
|
-
<div class="text-muted small">{{data.noResultsText}}</div>
|
|
33
|
-
<button type="button"
|
|
34
|
-
class="btn btn-link btn-sm mt-2 p-0"
|
|
35
|
-
data-action="clear-search">
|
|
36
|
-
Clear search
|
|
37
|
-
</button>
|
|
38
|
-
</div>
|
|
39
|
-
{{/data.showNoResults}}
|
|
40
|
-
|
|
41
|
-
{{#data.showEmpty}}
|
|
42
|
-
<div class="text-center p-4">
|
|
43
|
-
<i class="{{data.emptyIcon}} text-muted mb-2" style="font-size: 2rem;"></i>
|
|
44
|
-
<div class="text-muted small mb-2">{{data.emptyText}}</div>
|
|
45
|
-
{{#data.emptySubtext}}
|
|
46
|
-
<div class="text-muted" style="font-size: 0.75rem;">
|
|
47
|
-
{{data.emptySubtext}}
|
|
48
|
-
</div>
|
|
49
|
-
{{/data.emptySubtext}}
|
|
50
|
-
</div>
|
|
51
|
-
{{/data.showEmpty}}
|
|
52
|
-
{{/data.loading}}
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
{{#data.showResultsCount}}
|
|
56
|
-
<div class="border-top bg-light p-2 text-center">
|
|
57
|
-
<small class="text-muted">
|
|
58
|
-
{{data.filteredCount}} of {{data.totalCount}}
|
|
59
|
-
</small>
|
|
60
|
-
</div>
|
|
61
|
-
{{/data.showResultsCount}}
|
|
62
|
-
`,
|
|
63
|
-
...options
|
|
64
|
-
});
|
|
65
|
-
this.parentView = options.parentView;
|
|
66
|
-
}
|
|
67
|
-
async handleActionSelectItem(event, element) {
|
|
68
|
-
event.preventDefault();
|
|
69
|
-
const itemIndex = parseInt(element.getAttribute("data-item-index"));
|
|
70
|
-
if (this.parentView) {
|
|
71
|
-
this.parentView.handleItemSelection(itemIndex);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
async handleActionClearSearch(event, _element) {
|
|
75
|
-
event.preventDefault();
|
|
76
|
-
if (this.parentView) {
|
|
77
|
-
this.parentView.clearSearch();
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async onAfterRender() {
|
|
81
|
-
if (this.parentView && this.parentView.maxHeight) {
|
|
82
|
-
const container = this.element.querySelector("#results-container");
|
|
83
|
-
if (container) {
|
|
84
|
-
container.style.maxHeight = `${this.parentView.maxHeight}px`;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
class SimpleSearchView extends View {
|
|
90
|
-
constructor(options = {}) {
|
|
91
|
-
super({
|
|
92
|
-
className: "simple-search-view d-flex flex-column",
|
|
93
|
-
template: `
|
|
94
|
-
<div class="p-3 border-bottom bg-light">
|
|
95
|
-
{{#data.headerText}}
|
|
96
|
-
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
97
|
-
<h6 class="text-muted fw-semibold mb-0">
|
|
98
|
-
{{#data.headerIcon}}<i class="{{data.headerIcon}} me-2"></i>{{/data.headerIcon}}
|
|
99
|
-
{{{data.headerText}}}
|
|
100
|
-
</h6>
|
|
101
|
-
{{#data.showExitButton}}
|
|
102
|
-
<button class="btn btn-link p-0 text-muted simple-search-exit-btn"
|
|
103
|
-
type="button"
|
|
104
|
-
data-action="exit-view"
|
|
105
|
-
title="Exit"
|
|
106
|
-
aria-label="Exit view">
|
|
107
|
-
<i class="bi bi-x-lg" aria-hidden="true"></i>
|
|
108
|
-
</button>
|
|
109
|
-
{{/data.showExitButton}}
|
|
110
|
-
</div>
|
|
111
|
-
{{/data.headerText}}
|
|
112
|
-
<div class="position-relative">
|
|
113
|
-
<input type="text"
|
|
114
|
-
class="form-control form-control-sm pe-5"
|
|
115
|
-
placeholder="{{data.searchPlaceholder}}"
|
|
116
|
-
value="{{data.searchValue}}"
|
|
117
|
-
data-filter="live-search"
|
|
118
|
-
data-filter-debounce="{{data.debounceMs}}"
|
|
119
|
-
data-change-action="search-items">
|
|
120
|
-
<button class="btn btn-link p-0 position-absolute top-50 end-0 translate-middle-y me-2 text-muted simple-search-clear-btn"
|
|
121
|
-
type="button"
|
|
122
|
-
data-action="clear-search"
|
|
123
|
-
title="Clear search"
|
|
124
|
-
aria-label="Clear search">
|
|
125
|
-
<i class="bi bi-x-circle-fill" aria-hidden="true"></i>
|
|
126
|
-
</button>
|
|
127
|
-
</div>
|
|
128
|
-
</div>
|
|
129
|
-
|
|
130
|
-
<div data-container="results"></div>
|
|
131
|
-
|
|
132
|
-
{{#data.showFooter}}
|
|
133
|
-
<div class="p-3 border-top bg-light">
|
|
134
|
-
<small class="text-muted">
|
|
135
|
-
<i class="{{data.footerIcon}} me-1"></i>
|
|
136
|
-
{{{data.footerContent}}}
|
|
137
|
-
</small>
|
|
138
|
-
</div>
|
|
139
|
-
{{/data.showFooter}}
|
|
140
|
-
`,
|
|
141
|
-
...options
|
|
142
|
-
});
|
|
143
|
-
this.Collection = options.Collection;
|
|
144
|
-
this.collection = options.collection;
|
|
145
|
-
this.itemTemplate = options.itemTemplate || this.getDefaultItemTemplate();
|
|
146
|
-
this.searchFields = options.searchFields || ["name"];
|
|
147
|
-
this.collectionParams = { size: 25, ...options.collectionParams };
|
|
148
|
-
if (options.headerText === void 0) this.headerText = "Select Item";
|
|
149
|
-
this.headerText = options.headerText;
|
|
150
|
-
this.headerIcon = options.headerIcon || "bi bi-list";
|
|
151
|
-
this.searchPlaceholder = options.searchPlaceholder || "Search...";
|
|
152
|
-
this.loadingText = options.loadingText || "Loading items...";
|
|
153
|
-
this.noResultsText = options.noResultsText || "No items match your search";
|
|
154
|
-
this.emptyText = options.emptyText || "No items available";
|
|
155
|
-
this.emptySubtext = options.emptySubtext || null;
|
|
156
|
-
this.emptyIcon = options.emptyIcon || "bi bi-inbox";
|
|
157
|
-
this.footerContent = options.footerContent || null;
|
|
158
|
-
this.footerIcon = options.footerIcon || "bi bi-info-circle";
|
|
159
|
-
this.showExitButton = options.showExitButton || false;
|
|
160
|
-
this.searchValue = "";
|
|
161
|
-
this.filteredItems = [];
|
|
162
|
-
this.loading = false;
|
|
163
|
-
this.hasSearched = false;
|
|
164
|
-
this.searchTimer = null;
|
|
165
|
-
this.debounceMs = options.debounceMs || 300;
|
|
166
|
-
if (options.maxHeight) {
|
|
167
|
-
this.maxHeight = options.maxHeight;
|
|
168
|
-
} else {
|
|
169
|
-
this.addClass("h-100");
|
|
170
|
-
}
|
|
171
|
-
this.resultsView = new ResultsView({
|
|
172
|
-
parentView: this
|
|
173
|
-
});
|
|
174
|
-
if (!this.collection && this.Collection) {
|
|
175
|
-
this.collection = new this.Collection();
|
|
176
|
-
}
|
|
177
|
-
this.addChild(this.resultsView);
|
|
178
|
-
}
|
|
179
|
-
onInit() {
|
|
180
|
-
if (this.collection) {
|
|
181
|
-
this.setupCollection();
|
|
182
|
-
}
|
|
183
|
-
if (this.collection && this.options.autoLoad !== false) {
|
|
184
|
-
this.loadItems();
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
setupCollection() {
|
|
188
|
-
Object.assign(this.collection.params, this.collectionParams);
|
|
189
|
-
this.collection.on("fetch:success", () => {
|
|
190
|
-
this.loading = false;
|
|
191
|
-
this.updateFilteredItems();
|
|
192
|
-
});
|
|
193
|
-
this.collection.on("fetch:error", () => {
|
|
194
|
-
this.loading = false;
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
async loadItems() {
|
|
198
|
-
if (!this.collection) {
|
|
199
|
-
console.warn("SimpleSearchView: No collection provided");
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
this.loading = true;
|
|
203
|
-
this.updateResultsView();
|
|
204
|
-
try {
|
|
205
|
-
await this.collection.fetch();
|
|
206
|
-
this.updateFilteredItems();
|
|
207
|
-
} catch (error) {
|
|
208
|
-
console.error("Error loading items:", error);
|
|
209
|
-
const app = this.getApp();
|
|
210
|
-
app?.showError?.("Failed to load items. Please try again.");
|
|
211
|
-
} finally {
|
|
212
|
-
this.loading = false;
|
|
213
|
-
this.updateFilteredItems();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
updateFilteredItems() {
|
|
217
|
-
if (!this.collection) {
|
|
218
|
-
this.filteredItems = [];
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
this.filteredItems = this.collection.toJSON();
|
|
222
|
-
this.updateResultsView();
|
|
223
|
-
}
|
|
224
|
-
getNestedValue(obj, path) {
|
|
225
|
-
return path.split(".").reduce((current, key) => current?.[key], obj);
|
|
226
|
-
}
|
|
227
|
-
async getViewData() {
|
|
228
|
-
return {
|
|
229
|
-
searchValue: this.searchValue,
|
|
230
|
-
showFooter: !!this.footerContent,
|
|
231
|
-
showExitButton: this.showExitButton,
|
|
232
|
-
debounceMs: this.debounceMs,
|
|
233
|
-
// UI text
|
|
234
|
-
headerText: this.headerText,
|
|
235
|
-
headerIcon: this.headerIcon,
|
|
236
|
-
searchPlaceholder: this.searchPlaceholder,
|
|
237
|
-
footerContent: this.footerContent,
|
|
238
|
-
footerIcon: this.footerIcon
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
updateResultsView() {
|
|
242
|
-
if (!this.resultsView) return;
|
|
243
|
-
const hasItems = this.collection && this.collection.length() > 0;
|
|
244
|
-
const hasFilteredItems = this.filteredItems.length > 0;
|
|
245
|
-
const hasSearchValue = this.searchValue.length > 0;
|
|
246
|
-
const processedItems = this.filteredItems.map((item, index) => {
|
|
247
|
-
return {
|
|
248
|
-
...item,
|
|
249
|
-
index,
|
|
250
|
-
itemContent: this.processItemTemplate(item)
|
|
251
|
-
};
|
|
252
|
-
});
|
|
253
|
-
this.resultsView.data = {
|
|
254
|
-
loading: this.loading,
|
|
255
|
-
items: processedItems,
|
|
256
|
-
showEmpty: !this.loading && !hasItems,
|
|
257
|
-
showNoResults: !this.loading && hasItems && !hasFilteredItems && hasSearchValue,
|
|
258
|
-
showResultsCount: !this.loading && hasItems,
|
|
259
|
-
filteredCount: this.filteredItems.length,
|
|
260
|
-
totalCount: this.collection?.restEnabled ? this.collection?.meta?.count || 0 : this.collection?.length() || 0,
|
|
261
|
-
// UI text
|
|
262
|
-
loadingText: this.loadingText,
|
|
263
|
-
noResultsText: this.noResultsText,
|
|
264
|
-
emptyText: this.emptyText,
|
|
265
|
-
emptySubtext: this.emptySubtext,
|
|
266
|
-
emptyIcon: this.emptyIcon
|
|
267
|
-
};
|
|
268
|
-
this.resultsView.render();
|
|
269
|
-
}
|
|
270
|
-
processItemTemplate(item) {
|
|
271
|
-
let template = this.itemTemplate;
|
|
272
|
-
template = template.replace(/\{\{(\w+)\}\}/g, (match, prop) => {
|
|
273
|
-
return this.getNestedValue(item, prop) || "";
|
|
274
|
-
});
|
|
275
|
-
return template;
|
|
276
|
-
}
|
|
277
|
-
getDefaultItemTemplate() {
|
|
278
|
-
return `
|
|
279
|
-
<div class="p-3 border-bottom">
|
|
280
|
-
<div class="fw-semibold text-dark">{{name}}</div>
|
|
281
|
-
<small class="text-muted">{{id}}</small>
|
|
282
|
-
</div>
|
|
283
|
-
`;
|
|
284
|
-
}
|
|
285
|
-
async onPassThruActionSearchItems(event, element) {
|
|
286
|
-
const searchValue = element.value || "";
|
|
287
|
-
console.log("search change...");
|
|
288
|
-
this.searchValue = searchValue;
|
|
289
|
-
this.hasSearched = true;
|
|
290
|
-
if (this.searchTimer) {
|
|
291
|
-
clearTimeout(this.searchTimer);
|
|
292
|
-
}
|
|
293
|
-
this.performSearch();
|
|
294
|
-
}
|
|
295
|
-
async performSearch() {
|
|
296
|
-
const searchParams = { ...this.collectionParams };
|
|
297
|
-
if (this.searchValue && this.searchValue.length > 1) {
|
|
298
|
-
searchParams.search = this.searchValue.trim();
|
|
299
|
-
}
|
|
300
|
-
this.collection.setParams(searchParams, true);
|
|
301
|
-
}
|
|
302
|
-
handleItemSelection(itemIndex) {
|
|
303
|
-
if (isNaN(itemIndex) || itemIndex < 0 || itemIndex >= this.filteredItems.length) {
|
|
304
|
-
console.error("Invalid item index:", itemIndex);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
const item = this.filteredItems[itemIndex];
|
|
308
|
-
let model = this.collection ? this.collection.get(item.id) : null;
|
|
309
|
-
if (!model) {
|
|
310
|
-
model = new this.collection.ModelClass({ id: item.id });
|
|
311
|
-
const app = this.getApp();
|
|
312
|
-
app.showLoading();
|
|
313
|
-
model.fetch().then(() => {
|
|
314
|
-
app.hideLoading();
|
|
315
|
-
this.emit("item:selected", {
|
|
316
|
-
item,
|
|
317
|
-
model,
|
|
318
|
-
index: itemIndex
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
return;
|
|
322
|
-
} else {
|
|
323
|
-
this.emit("item:selected", {
|
|
324
|
-
item,
|
|
325
|
-
model,
|
|
326
|
-
index: itemIndex
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Set the collection for this search view
|
|
332
|
-
*/
|
|
333
|
-
setCollection(collection) {
|
|
334
|
-
this.collection = collection;
|
|
335
|
-
this.setupCollection();
|
|
336
|
-
return this;
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Set the item template
|
|
340
|
-
*/
|
|
341
|
-
setItemTemplate(template) {
|
|
342
|
-
this.itemTemplate = template;
|
|
343
|
-
this.updateResultsView();
|
|
344
|
-
return this;
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Set search fields
|
|
348
|
-
*/
|
|
349
|
-
setSearchFields(fields) {
|
|
350
|
-
this.searchFields = Array.isArray(fields) ? fields : [fields];
|
|
351
|
-
return this;
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Refresh items list
|
|
355
|
-
*/
|
|
356
|
-
async refresh() {
|
|
357
|
-
await this.loadItems();
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Focus the search input
|
|
361
|
-
*/
|
|
362
|
-
focusSearch() {
|
|
363
|
-
const searchInput = this.element?.querySelector('input[data-action="search-items"]');
|
|
364
|
-
if (searchInput) {
|
|
365
|
-
searchInput.focus();
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
|
-
* Handle exit button click - emits event instead of closing
|
|
370
|
-
*/
|
|
371
|
-
async handleActionExitView(event, element) {
|
|
372
|
-
this.emit("exit", { view: this });
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Clear search and reset
|
|
376
|
-
*/
|
|
377
|
-
async handleActionClearSearch(event, element) {
|
|
378
|
-
this.clearSearch();
|
|
379
|
-
}
|
|
380
|
-
clearSearch() {
|
|
381
|
-
this.searchValue = "";
|
|
382
|
-
this.hasSearched = false;
|
|
383
|
-
const searchInput = this.element?.querySelector('input[data-change-action="search-items"]');
|
|
384
|
-
if (searchInput) {
|
|
385
|
-
searchInput.value = "";
|
|
386
|
-
searchInput.focus();
|
|
387
|
-
}
|
|
388
|
-
this.performSearch();
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Get the number of available items
|
|
392
|
-
*/
|
|
393
|
-
getItemCount() {
|
|
394
|
-
return this.collection ? this.collection.length() : 0;
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Get the number of filtered items
|
|
398
|
-
*/
|
|
399
|
-
getFilteredItemCount() {
|
|
400
|
-
return this.filteredItems.length;
|
|
401
|
-
}
|
|
402
|
-
/**
|
|
403
|
-
* Check if items are loaded
|
|
404
|
-
*/
|
|
405
|
-
hasItems() {
|
|
406
|
-
return this.getItemCount() > 0;
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Get current search value
|
|
410
|
-
*/
|
|
411
|
-
getSearchValue() {
|
|
412
|
-
return this.searchValue;
|
|
413
|
-
}
|
|
414
|
-
/**
|
|
415
|
-
* Set search value programmatically
|
|
416
|
-
*/
|
|
417
|
-
setSearchValue(value) {
|
|
418
|
-
this.searchValue = value || "";
|
|
419
|
-
this.hasSearched = !!this.searchValue;
|
|
420
|
-
const searchInput = this.element?.querySelector('input[data-action="search-items"]');
|
|
421
|
-
if (searchInput) {
|
|
422
|
-
searchInput.value = this.searchValue;
|
|
423
|
-
}
|
|
424
|
-
this.performSearch();
|
|
425
|
-
return this;
|
|
426
|
-
}
|
|
427
|
-
async onAfterRender() {
|
|
428
|
-
await super.onAfterRender();
|
|
429
|
-
if (this.resultsView && !this.resultsView.isMounted()) {
|
|
430
|
-
const container = this.element?.querySelector('[data-container="results"]');
|
|
431
|
-
if (container) {
|
|
432
|
-
await this.resultsView.render(true, container);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
this.updateResultsView();
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* Cleanup on destroy
|
|
439
|
-
*/
|
|
440
|
-
async onBeforeDestroy() {
|
|
441
|
-
if (this.searchTimer) {
|
|
442
|
-
clearTimeout(this.searchTimer);
|
|
443
|
-
}
|
|
444
|
-
if (this.collection) {
|
|
445
|
-
this.collection.off("update");
|
|
446
|
-
}
|
|
447
|
-
await super.onBeforeDestroy();
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
class GroupSelectorButton extends View {
|
|
451
|
-
constructor(options = {}) {
|
|
452
|
-
super({
|
|
453
|
-
tagName: "div",
|
|
454
|
-
className: "nav-item",
|
|
455
|
-
...options
|
|
456
|
-
});
|
|
457
|
-
const app = this.getApp();
|
|
458
|
-
this.Collection = options.Collection || app?.GroupCollection || GroupList;
|
|
459
|
-
this.collection = options.collection || new this.Collection();
|
|
460
|
-
this.currentGroup = options.currentGroup !== void 0 ? options.currentGroup : app?.activeGroup;
|
|
461
|
-
this.buttonClass = options.buttonClass || "btn btn-link nav-link";
|
|
462
|
-
this.buttonIcon = options.buttonIcon || "bi-building";
|
|
463
|
-
this.defaultText = options.defaultText || "Select Group";
|
|
464
|
-
this.itemTemplate = options.itemTemplate;
|
|
465
|
-
this.searchFields = options.searchFields || ["name"];
|
|
466
|
-
this.headerText = options.headerText || "Select Group";
|
|
467
|
-
this.searchPlaceholder = options.searchPlaceholder || "Search groups...";
|
|
468
|
-
this.autoSetActiveGroup = options.autoSetActiveGroup !== false;
|
|
469
|
-
this.onGroupSelected = options.onGroupSelected;
|
|
470
|
-
this.dialog = null;
|
|
471
|
-
if (app?.events) {
|
|
472
|
-
app.events.on("group:changed", (data) => {
|
|
473
|
-
if (data.group !== this.currentGroup) {
|
|
474
|
-
this.setCurrentGroup(data.group);
|
|
475
|
-
}
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
async getTemplate() {
|
|
480
|
-
return `
|
|
481
|
-
<button class="{{buttonClass}}"
|
|
482
|
-
data-action="show-selector"
|
|
483
|
-
type="button"
|
|
484
|
-
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
485
|
-
<i class="{{buttonIcon}} me-1"></i>
|
|
486
|
-
<span class="group-name">{{displayName}}</span>
|
|
487
|
-
</button>
|
|
488
|
-
`;
|
|
489
|
-
}
|
|
490
|
-
async onBeforeRender() {
|
|
491
|
-
await super.onBeforeRender();
|
|
492
|
-
console.log("GroupSelectorButton onBeforeRender - currentGroup:", this.currentGroup?.get?.("name") || this.currentGroup?.name || "none");
|
|
493
|
-
this.buttonClass = this.buttonClass;
|
|
494
|
-
this.buttonIcon = this.buttonIcon;
|
|
495
|
-
this.displayName = this.currentGroup?.get?.("name") || this.currentGroup?.name || this.defaultText;
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Show the group selector dialog
|
|
499
|
-
*/
|
|
500
|
-
async onActionShowSelector(event) {
|
|
501
|
-
const searchView = new SimpleSearchView({
|
|
502
|
-
Collection: this.Collection,
|
|
503
|
-
collection: this.collection,
|
|
504
|
-
itemTemplate: this.itemTemplate || this.getDefaultItemTemplate(),
|
|
505
|
-
searchFields: this.searchFields,
|
|
506
|
-
headerText: this.headerText,
|
|
507
|
-
searchPlaceholder: this.searchPlaceholder,
|
|
508
|
-
headerIcon: this.buttonIcon,
|
|
509
|
-
showExitButton: false
|
|
510
|
-
});
|
|
511
|
-
this.dialog = new Dialog({
|
|
512
|
-
title: this.headerText,
|
|
513
|
-
body: searchView,
|
|
514
|
-
size: "md",
|
|
515
|
-
scrollable: true,
|
|
516
|
-
noBodyPadding: true,
|
|
517
|
-
buttons: [],
|
|
518
|
-
closeButton: true
|
|
519
|
-
});
|
|
520
|
-
searchView.on("item:selected", (data) => {
|
|
521
|
-
this.handleGroupSelection(data.model || data.item);
|
|
522
|
-
if (this.dialog) {
|
|
523
|
-
this.dialog.hide();
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
this.dialog.on("hidden", () => {
|
|
527
|
-
this.dialog.destroy();
|
|
528
|
-
this.dialog = null;
|
|
529
|
-
});
|
|
530
|
-
await this.dialog.render(true, document.body);
|
|
531
|
-
this.dialog.show();
|
|
532
|
-
return true;
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Handle group selection
|
|
536
|
-
*/
|
|
537
|
-
handleGroupSelection(group) {
|
|
538
|
-
this.currentGroup = group;
|
|
539
|
-
this.displayName = group?.get?.("name") || group?.name || this.defaultText;
|
|
540
|
-
this.render();
|
|
541
|
-
const app = this.getApp();
|
|
542
|
-
if (this.autoSetActiveGroup && app?.setActiveGroup) {
|
|
543
|
-
app.setActiveGroup(group);
|
|
544
|
-
}
|
|
545
|
-
if (this.onGroupSelected) {
|
|
546
|
-
this.onGroupSelected({ group });
|
|
547
|
-
}
|
|
548
|
-
this.emit("group-selected", { group });
|
|
549
|
-
if (app?.events) {
|
|
550
|
-
app.events.emit("group:selected", { group });
|
|
551
|
-
app.events.emit("group:changed", { group });
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* Default item template for groups (matches Sidebar pattern)
|
|
556
|
-
* Note: data-action and data-item-index are added by ResultsView wrapper
|
|
557
|
-
*/
|
|
558
|
-
getDefaultItemTemplate() {
|
|
559
|
-
return `
|
|
560
|
-
<div class="d-flex align-items-center p-3 border-bottom">
|
|
561
|
-
<div class="flex-grow-1">
|
|
562
|
-
<div class="fw-semibold text-dark">{{name}}</div>
|
|
563
|
-
<small class="text-muted">#{{id}} {{kind}}</small>
|
|
564
|
-
</div>
|
|
565
|
-
</div>
|
|
566
|
-
`;
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* Set the current group programmatically
|
|
570
|
-
*/
|
|
571
|
-
setCurrentGroup(group) {
|
|
572
|
-
this.currentGroup = group;
|
|
573
|
-
this.displayName = group?.get?.("name") || group?.name || this.defaultText;
|
|
574
|
-
if (this.mounted) {
|
|
575
|
-
this.render();
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Get the current group
|
|
580
|
-
*/
|
|
581
|
-
getCurrentGroup() {
|
|
582
|
-
return this.currentGroup;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
class TopNav extends View {
|
|
586
|
-
constructor(options = {}) {
|
|
587
|
-
const themes = {
|
|
588
|
-
light: "navbar navbar-expand-lg navbar-light topnav-light",
|
|
589
|
-
dark: "navbar navbar-expand-lg navbar-dark topnav-dark",
|
|
590
|
-
clean: "navbar navbar-expand-lg navbar-light topnav-clean",
|
|
591
|
-
gradient: "navbar navbar-expand-lg navbar-dark topnav-gradient"
|
|
592
|
-
};
|
|
593
|
-
const themeName = options.theme || "light";
|
|
594
|
-
let navbarClass = themes[themeName] || themes.light;
|
|
595
|
-
if (options.shadow) {
|
|
596
|
-
navbarClass += ` topnav-shadow-${options.shadow}`;
|
|
597
|
-
}
|
|
598
|
-
super({
|
|
599
|
-
tagName: "nav",
|
|
600
|
-
className: navbarClass,
|
|
601
|
-
enableTooltips: true,
|
|
602
|
-
style: "position: relative; z-index: 1030;",
|
|
603
|
-
...options
|
|
604
|
-
});
|
|
605
|
-
this.displayMode = options.displayMode || "both";
|
|
606
|
-
this.showPageIcon = options.showPageIcon !== false;
|
|
607
|
-
this.showPageDescription = options.showPageDescription || false;
|
|
608
|
-
this.showBreadcrumbs = options.showBreadcrumbs || false;
|
|
609
|
-
this.groupIcon = options.groupIcon || "bi-building";
|
|
610
|
-
this.currentPage = null;
|
|
611
|
-
this.previousPage = null;
|
|
612
|
-
this.config = {
|
|
613
|
-
brand: options.brand || "MOJO App",
|
|
614
|
-
brandIcon: options.brandIcon || "bi bi-play-circle",
|
|
615
|
-
brandRoute: options.brandRoute || "/",
|
|
616
|
-
navItems: options.navItems || [],
|
|
617
|
-
rightItems: options.rightItems || [],
|
|
618
|
-
showSidebarToggle: options.showSidebarToggle || false,
|
|
619
|
-
sidebarToggleAction: options.sidebarToggleAction || "toggle-sidebar",
|
|
620
|
-
...options
|
|
621
|
-
};
|
|
622
|
-
this.userMenu = options.userMenu || this.findMenuItem("user");
|
|
623
|
-
if (this.userMenu) this.userMenu.id = "user";
|
|
624
|
-
this.loginMenu = options.loginMenu || this.findMenuItem("login");
|
|
625
|
-
this.setupPageListeners();
|
|
626
|
-
this.setupGroupListeners();
|
|
627
|
-
this.groupSelectorButton = null;
|
|
628
|
-
this.currentGroup = null;
|
|
629
|
-
}
|
|
630
|
-
findMenuItem(id) {
|
|
631
|
-
let item = this.config.navItems.find((item2) => item2.id === id);
|
|
632
|
-
if (!item) {
|
|
633
|
-
item = this.config.rightItems.find((item2) => item2.id === id);
|
|
634
|
-
}
|
|
635
|
-
return item || null;
|
|
636
|
-
}
|
|
637
|
-
replaceMenuItem(id, new_menu) {
|
|
638
|
-
const navIndex = this.config.navItems.findIndex((item) => item.id === id);
|
|
639
|
-
if (navIndex !== -1) {
|
|
640
|
-
this.config.navItems[navIndex] = new_menu;
|
|
641
|
-
return true;
|
|
642
|
-
}
|
|
643
|
-
const rightIndex = this.config.rightItems.findIndex((item) => item.id === id);
|
|
644
|
-
if (rightIndex !== -1) {
|
|
645
|
-
this.config.rightItems[rightIndex] = new_menu;
|
|
646
|
-
return true;
|
|
647
|
-
}
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
|
-
setBrand(brand, icon = null) {
|
|
651
|
-
this.config.brand = brand;
|
|
652
|
-
this.config.brandIcon = icon || this.config.brandIcon;
|
|
653
|
-
this.render();
|
|
654
|
-
}
|
|
655
|
-
setUser(user) {
|
|
656
|
-
if (!user) {
|
|
657
|
-
this.replaceMenuItem("user", this.loginMenu);
|
|
658
|
-
} else {
|
|
659
|
-
this.userMenu.label = user.get("display_name");
|
|
660
|
-
this.replaceMenuItem("login", this.userMenu);
|
|
661
|
-
}
|
|
662
|
-
this.setModel(user);
|
|
663
|
-
}
|
|
664
|
-
_onModelChange() {
|
|
665
|
-
if (this.model) {
|
|
666
|
-
this.userMenu.label = this.model.get("display_name");
|
|
667
|
-
}
|
|
668
|
-
if (this.isMounted()) {
|
|
669
|
-
this.render();
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Get template based on display mode
|
|
674
|
-
*/
|
|
675
|
-
async getTemplate() {
|
|
676
|
-
return `
|
|
677
|
-
<div class="container-fluid">
|
|
678
|
-
{{#data.showSidebarToggle}}
|
|
679
|
-
<button class="topnav-sidebar-toggle me-2" data-action="{{data.sidebarToggleAction}}" aria-label="Toggle Sidebar">
|
|
680
|
-
<i class="bi bi-chevron-right toggle-chevron"></i>
|
|
681
|
-
</button>
|
|
682
|
-
{{/data.showSidebarToggle}}
|
|
683
|
-
|
|
684
|
-
{{#data.showGroupInfo}}
|
|
685
|
-
<div class="navbar-brand d-flex align-items-center">
|
|
686
|
-
{{#data.groupIcon}}<i class="{{data.groupIcon}} me-2"></i>{{/data.groupIcon}}
|
|
687
|
-
<div>
|
|
688
|
-
<span class="topnav-group-name"
|
|
689
|
-
role="button"
|
|
690
|
-
tabindex="0"
|
|
691
|
-
data-action="open-group-selector"
|
|
692
|
-
style="cursor: pointer;">
|
|
693
|
-
{{data.currentGroupName}}
|
|
694
|
-
</span>
|
|
695
|
-
{{#data.showPageTitle}}
|
|
696
|
-
<span class="text-muted mx-2">|</span>
|
|
697
|
-
<span>{{data.currentPageName}}</span>
|
|
698
|
-
{{/data.showPageTitle}}
|
|
699
|
-
</div>
|
|
700
|
-
</div>
|
|
701
|
-
{{/data.showGroupInfo}}
|
|
702
|
-
|
|
703
|
-
{{#data.showPageInfo}}
|
|
704
|
-
<div class="navbar-brand d-flex align-items-center">
|
|
705
|
-
{{#data.currentPageIcon}}<i class="{{data.currentPageIcon}} me-2"></i>{{/data.currentPageIcon}}
|
|
706
|
-
<div>
|
|
707
|
-
<span>{{data.currentPageName}}</span>
|
|
708
|
-
{{#data.currentPageDescription}}
|
|
709
|
-
<small class="d-block" style="font-size: 0.75rem; line-height: 1;">{{data.currentPageDescription}}</small>
|
|
710
|
-
{{/data.currentPageDescription}}
|
|
711
|
-
</div>
|
|
712
|
-
</div>
|
|
713
|
-
{{/data.showPageInfo}}
|
|
714
|
-
|
|
715
|
-
{{#data.showBrand}}
|
|
716
|
-
<a class="navbar-brand" href="{{data.brandRoute}}">
|
|
717
|
-
{{#data.brandIcon}}<i class="{{data.brandIcon}} me-2"></i>{{/data.brandIcon}}
|
|
718
|
-
{{data.brand}}
|
|
719
|
-
</a>
|
|
720
|
-
{{/data.showBrand}}
|
|
721
|
-
|
|
722
|
-
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#{{data.navbarId}}">
|
|
723
|
-
<span class="navbar-toggler-icon"></span>
|
|
724
|
-
</button>
|
|
725
|
-
|
|
726
|
-
<div class="collapse navbar-collapse" id="{{data.navbarId}}">
|
|
727
|
-
{{#data.showNavItems}}
|
|
728
|
-
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
|
729
|
-
{{#data.navItems}}
|
|
730
|
-
<li class="nav-item">
|
|
731
|
-
<a class="nav-link {{#active}}active{{/active}}" href="{{route}}" {{#tooltip}}data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="{{tooltip}}"{{/tooltip}}>
|
|
732
|
-
{{#icon}}<i class="{{icon}} me-1"></i>{{/icon}}
|
|
733
|
-
{{text}}
|
|
734
|
-
</a>
|
|
735
|
-
</li>
|
|
736
|
-
{{/data.navItems}}
|
|
737
|
-
</ul>
|
|
738
|
-
{{/data.showNavItems}}
|
|
739
|
-
|
|
740
|
-
{{#data.hasRightItems}}
|
|
741
|
-
<div class="navbar-nav ms-auto">
|
|
742
|
-
{{#data.rightItems}}
|
|
743
|
-
{{#isGroupSelector}}
|
|
744
|
-
<div data-container="group-selector-{{id}}"></div>
|
|
745
|
-
{{/isGroupSelector}}
|
|
746
|
-
{{^isGroupSelector}}
|
|
747
|
-
{{#isDropdown}}
|
|
748
|
-
<div class="nav-item dropdown">
|
|
749
|
-
<a class="nav-link dropdown-toggle" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
750
|
-
{{#icon}}<i class="{{icon}} me-1"></i>{{/icon}}
|
|
751
|
-
{{label}}
|
|
752
|
-
</a>
|
|
753
|
-
<ul class="dropdown-menu dropdown-menu-end">
|
|
754
|
-
{{#items}}
|
|
755
|
-
{{#divider}}
|
|
756
|
-
<li><hr class="dropdown-divider"></li>
|
|
757
|
-
{{/divider}}
|
|
758
|
-
{{^divider}}
|
|
759
|
-
<li>
|
|
760
|
-
<a class="dropdown-item" role="button" {{#action}}data-action="{{action}}"{{/action}}>
|
|
761
|
-
{{#icon}}<i class="{{icon}} me-1"></i>{{/icon}}
|
|
762
|
-
{{label}}
|
|
763
|
-
</a>
|
|
764
|
-
</li>
|
|
765
|
-
{{/divider}}
|
|
766
|
-
{{/items}}
|
|
767
|
-
</ul>
|
|
768
|
-
</div>
|
|
769
|
-
{{/isDropdown}}
|
|
770
|
-
{{^isDropdown}}
|
|
771
|
-
{{#isButton}}
|
|
772
|
-
<button class="{{buttonClass}}" data-action="{{action}}" data-id="{{id}}" {{#tooltip}}data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="{{tooltip}}"{{/tooltip}}>
|
|
773
|
-
{{#icon}}<i class="{{icon}} me-1"></i>{{/icon}}
|
|
774
|
-
{{label}}
|
|
775
|
-
</button>
|
|
776
|
-
{{/isButton}}
|
|
777
|
-
{{^isButton}}
|
|
778
|
-
<a class="nav-link" href="{{href}}" {{#action}}data-action="{{action}}"{{/action}} {{#tooltip}}data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="{{tooltip}}"{{/tooltip}}>
|
|
779
|
-
{{#icon}}<i class="{{icon}} me-1"></i>{{/icon}}
|
|
780
|
-
{{label}}
|
|
781
|
-
</a>
|
|
782
|
-
{{/isButton}}
|
|
783
|
-
{{/isDropdown}}
|
|
784
|
-
{{/isGroupSelector}}
|
|
785
|
-
{{/data.rightItems}}
|
|
786
|
-
</div>
|
|
787
|
-
{{/data.hasRightItems}}
|
|
788
|
-
</div>
|
|
789
|
-
</div>
|
|
790
|
-
`;
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Process and normalize data before rendering (like Sidebar)
|
|
794
|
-
*/
|
|
795
|
-
async onBeforeRender() {
|
|
796
|
-
await super.onBeforeRender();
|
|
797
|
-
const app = this.getApp();
|
|
798
|
-
const activeGroup = this.currentGroup || app?.activeGroup;
|
|
799
|
-
const showGroupInfo = this.displayMode === "group" || this.displayMode === "group_page_titles";
|
|
800
|
-
const showPageTitle = this.displayMode === "group_page_titles";
|
|
801
|
-
const showPageInfo = this.displayMode === "page" || this.displayMode === "both";
|
|
802
|
-
const showBrand = !showGroupInfo && !showPageInfo;
|
|
803
|
-
const showNavItems = this.displayMode === "menu" || this.displayMode === "both";
|
|
804
|
-
const navItems = this.filterItemsByPermissions(this.config.navItems || []);
|
|
805
|
-
const rightItems = this.processRightItems(this.config.rightItems || []);
|
|
806
|
-
this.data = {
|
|
807
|
-
// Brand information
|
|
808
|
-
brand: this.config.brand,
|
|
809
|
-
brandIcon: this.config.brandIcon,
|
|
810
|
-
brandRoute: this.config.brandRoute,
|
|
811
|
-
showBrand,
|
|
812
|
-
// Navbar configuration
|
|
813
|
-
navbarId: `navbar-${this.id}`,
|
|
814
|
-
// Navigation items
|
|
815
|
-
navItems,
|
|
816
|
-
showNavItems,
|
|
817
|
-
// Right items
|
|
818
|
-
rightItems,
|
|
819
|
-
hasRightItems: rightItems.length > 0,
|
|
820
|
-
// Group display
|
|
821
|
-
showGroupInfo,
|
|
822
|
-
showPageTitle,
|
|
823
|
-
currentGroupName: activeGroup?.get?.("name") || activeGroup?.name || "Select Group",
|
|
824
|
-
groupIcon: this.groupIcon,
|
|
825
|
-
// Page display
|
|
826
|
-
showPageInfo,
|
|
827
|
-
currentPageName: this.currentPage?.title || this.currentPage?.name || "",
|
|
828
|
-
currentPageIcon: this.currentPage?.icon || this.currentPage?.pageIcon || "",
|
|
829
|
-
currentPageDescription: this.showPageDescription ? this.currentPage?.description : "",
|
|
830
|
-
// Sidebar toggle
|
|
831
|
-
showSidebarToggle: this.config.showSidebarToggle,
|
|
832
|
-
sidebarToggleAction: this.config.sidebarToggleAction,
|
|
833
|
-
// Display mode
|
|
834
|
-
displayMode: this.displayMode
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
/**
|
|
838
|
-
* Process right items configuration
|
|
839
|
-
*/
|
|
840
|
-
processRightItems(rightItems) {
|
|
841
|
-
return this.filterItemsByPermissions(rightItems).map((item) => {
|
|
842
|
-
const processedItem = { ...item };
|
|
843
|
-
if (item.items) {
|
|
844
|
-
processedItem.items = this.filterItemsByPermissions(item.items);
|
|
845
|
-
}
|
|
846
|
-
if (item.type === "group-selector") {
|
|
847
|
-
processedItem.isGroupSelector = true;
|
|
848
|
-
processedItem.isDropdown = false;
|
|
849
|
-
processedItem.isButton = false;
|
|
850
|
-
const groupSelectorOptions = {
|
|
851
|
-
containerId: `group-selector-${item.id || "default"}`
|
|
852
|
-
};
|
|
853
|
-
if (item.Collection !== void 0) groupSelectorOptions.Collection = item.Collection;
|
|
854
|
-
if (item.collection !== void 0) groupSelectorOptions.collection = item.collection;
|
|
855
|
-
if (item.currentGroup !== void 0) groupSelectorOptions.currentGroup = item.currentGroup;
|
|
856
|
-
if (item.buttonClass !== void 0) groupSelectorOptions.buttonClass = item.buttonClass;
|
|
857
|
-
if (item.buttonIcon !== void 0) groupSelectorOptions.buttonIcon = item.buttonIcon;
|
|
858
|
-
if (item.defaultText !== void 0) groupSelectorOptions.defaultText = item.defaultText;
|
|
859
|
-
if (item.itemTemplate !== void 0) groupSelectorOptions.itemTemplate = item.itemTemplate;
|
|
860
|
-
if (item.searchFields !== void 0) groupSelectorOptions.searchFields = item.searchFields;
|
|
861
|
-
if (item.headerText !== void 0) groupSelectorOptions.headerText = item.headerText;
|
|
862
|
-
if (item.searchPlaceholder !== void 0) groupSelectorOptions.searchPlaceholder = item.searchPlaceholder;
|
|
863
|
-
if (item.autoSetActiveGroup !== void 0) groupSelectorOptions.autoSetActiveGroup = item.autoSetActiveGroup;
|
|
864
|
-
if (item.onGroupSelected !== void 0) groupSelectorOptions.onGroupSelected = item.onGroupSelected;
|
|
865
|
-
const groupSelector = new GroupSelectorButton(groupSelectorOptions);
|
|
866
|
-
this.groupSelectorButton = groupSelector;
|
|
867
|
-
this.addChild(groupSelector);
|
|
868
|
-
} else if (processedItem.items && processedItem.items.length > 0) {
|
|
869
|
-
processedItem.isDropdown = true;
|
|
870
|
-
processedItem.isButton = false;
|
|
871
|
-
} else if (item.buttonClass) {
|
|
872
|
-
processedItem.isButton = true;
|
|
873
|
-
processedItem.isDropdown = false;
|
|
874
|
-
} else {
|
|
875
|
-
processedItem.isButton = false;
|
|
876
|
-
processedItem.isDropdown = false;
|
|
877
|
-
}
|
|
878
|
-
if (item.handler) {
|
|
879
|
-
this.rightItemHandlers = this.rightItemHandlers || /* @__PURE__ */ new Map();
|
|
880
|
-
this.rightItemHandlers.set(item.id, item.handler);
|
|
881
|
-
}
|
|
882
|
-
return processedItem;
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
/**
|
|
886
|
-
* Setup listeners for page change events
|
|
887
|
-
*/
|
|
888
|
-
setupPageListeners() {
|
|
889
|
-
this.getApp().events.on("page:show", (data) => {
|
|
890
|
-
this.onPageChanged(data);
|
|
891
|
-
});
|
|
892
|
-
}
|
|
893
|
-
/**
|
|
894
|
-
* Setup listeners for group change events
|
|
895
|
-
*/
|
|
896
|
-
setupGroupListeners() {
|
|
897
|
-
const app = this.getApp();
|
|
898
|
-
if (!app?.events) return;
|
|
899
|
-
app.events.on(["group:changed", "group:loaded"], (data) => {
|
|
900
|
-
if (data?.group) {
|
|
901
|
-
this.currentGroup = data.group;
|
|
902
|
-
}
|
|
903
|
-
if (this.displayMode === "group" || this.displayMode === "group_page_titles") {
|
|
904
|
-
if (this.mounted) {
|
|
905
|
-
this.render();
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
/**
|
|
911
|
-
* Handle page before change event
|
|
912
|
-
* @param {object} data - Event data
|
|
913
|
-
*/
|
|
914
|
-
onPageBeforeChange(data) {
|
|
915
|
-
if (this.displayMode === "page" || this.displayMode === "both") ;
|
|
916
|
-
}
|
|
917
|
-
/**
|
|
918
|
-
* Handle page changed event
|
|
919
|
-
* @param {object} data - Event data with previousPage and currentPage
|
|
920
|
-
*/
|
|
921
|
-
onPageChanged(data) {
|
|
922
|
-
this.previousPage = this.currentPage;
|
|
923
|
-
this.currentPage = data.page;
|
|
924
|
-
if (this.displayMode === "page" || this.displayMode === "both") {
|
|
925
|
-
this.updatePageDisplay();
|
|
926
|
-
}
|
|
927
|
-
if (this.displayMode === "menu" || this.displayMode === "both") {
|
|
928
|
-
if (this.currentPage && this.currentPage.route) {
|
|
929
|
-
this.updateActiveItem(this.currentPage.route);
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
/**
|
|
934
|
-
* Update the display to show current page info
|
|
935
|
-
*/
|
|
936
|
-
updatePageDisplay() {
|
|
937
|
-
if (!this.currentPage) return;
|
|
938
|
-
if (this.mounted) {
|
|
939
|
-
this.render();
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
updateActiveItem(currentRoute) {
|
|
943
|
-
const normalizeRoute = (route) => {
|
|
944
|
-
if (!route) return "/";
|
|
945
|
-
return route.startsWith("/") ? route : `/${route}`;
|
|
946
|
-
};
|
|
947
|
-
const normalizedCurrentRoute = normalizeRoute(currentRoute);
|
|
948
|
-
const navItems = this.data.navItems.map((item) => {
|
|
949
|
-
const normalizedItemRoute = normalizeRoute(item.route);
|
|
950
|
-
let isActive = false;
|
|
951
|
-
if (normalizedItemRoute === "/" && normalizedCurrentRoute === "/") {
|
|
952
|
-
isActive = true;
|
|
953
|
-
} else if (normalizedItemRoute !== "/" && normalizedCurrentRoute !== "/") {
|
|
954
|
-
isActive = normalizedCurrentRoute.startsWith(normalizedItemRoute) || normalizedCurrentRoute === normalizedItemRoute;
|
|
955
|
-
}
|
|
956
|
-
return {
|
|
957
|
-
...item,
|
|
958
|
-
active: isActive
|
|
959
|
-
};
|
|
960
|
-
});
|
|
961
|
-
this.updateData({ navItems }, true);
|
|
962
|
-
}
|
|
963
|
-
onPassThruActionProfile() {
|
|
964
|
-
this.getApp().events.emit("portal:action", { action: "profile" });
|
|
965
|
-
}
|
|
966
|
-
onActionSettings() {
|
|
967
|
-
this.getApp().events.emit("portal:action", { action: "settings" });
|
|
968
|
-
}
|
|
969
|
-
onActionLogout() {
|
|
970
|
-
this.getApp().events.emit("auth:logout", { action: "logout" });
|
|
971
|
-
}
|
|
972
|
-
/**
|
|
973
|
-
* Handle open group selector action (from clicking group name in brand)
|
|
974
|
-
*/
|
|
975
|
-
async onActionOpenGroupSelector(event) {
|
|
976
|
-
if (this.groupSelectorButton) {
|
|
977
|
-
await this.groupSelectorButton.onActionShowSelector(event);
|
|
978
|
-
return true;
|
|
979
|
-
}
|
|
980
|
-
const { GroupList: GroupList2 } = await import("./ContextMenu-sgvgSACY.js").then((n) => n.j);
|
|
981
|
-
const tempSelector = new GroupSelectorButton({
|
|
982
|
-
Collection: GroupList2,
|
|
983
|
-
currentGroup: this.getApp()?.activeGroup
|
|
984
|
-
});
|
|
985
|
-
await tempSelector.onActionShowSelector(event);
|
|
986
|
-
return true;
|
|
987
|
-
}
|
|
988
|
-
/**
|
|
989
|
-
* Handle dynamic action dispatch for right items
|
|
990
|
-
*/
|
|
991
|
-
async handleAction(actionName, event, element) {
|
|
992
|
-
const itemId = element.getAttribute("data-id");
|
|
993
|
-
if (itemId && this.rightItemHandlers && this.rightItemHandlers.has(itemId)) {
|
|
994
|
-
const handler = this.rightItemHandlers.get(itemId);
|
|
995
|
-
if (typeof handler === "function") {
|
|
996
|
-
return await handler.call(this, actionName, event, element);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
const methodName = `onAction${actionName.charAt(0).toUpperCase() + actionName.slice(1).replace(/-([a-z])/g, (g) => g[1].toUpperCase())}`;
|
|
1000
|
-
if (typeof this[methodName] === "function") {
|
|
1001
|
-
return await this[methodName](event, element);
|
|
1002
|
-
}
|
|
1003
|
-
this.emit("action", {
|
|
1004
|
-
action: actionName,
|
|
1005
|
-
event,
|
|
1006
|
-
element,
|
|
1007
|
-
topnav: this
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
/**
|
|
1011
|
-
* Handle default actions by searching through rightItems and navItems
|
|
1012
|
-
*/
|
|
1013
|
-
async onActionDefault(action, event, el) {
|
|
1014
|
-
if (this.config.navItems) {
|
|
1015
|
-
for (const item of this.config.navItems) {
|
|
1016
|
-
if (item.action === action && item.handler) {
|
|
1017
|
-
await item.handler.call(this, action, event, el);
|
|
1018
|
-
return true;
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
if (this.config.rightItems) {
|
|
1023
|
-
for (const item of this.config.rightItems) {
|
|
1024
|
-
if (item.action === action && item.handler) {
|
|
1025
|
-
await item.handler.call(this, action, event, el);
|
|
1026
|
-
return true;
|
|
1027
|
-
}
|
|
1028
|
-
if (item.items) {
|
|
1029
|
-
for (const subItem of item.items) {
|
|
1030
|
-
if (subItem.action === action && subItem.handler) {
|
|
1031
|
-
await subItem.handler.call(this, action, event, el);
|
|
1032
|
-
return true;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
this.getApp().events.emit("portal:action", { action, event, el });
|
|
1039
|
-
return false;
|
|
1040
|
-
}
|
|
1041
|
-
/**
|
|
1042
|
-
* Filter items by user permissions
|
|
1043
|
-
*/
|
|
1044
|
-
filterItemsByPermissions(items) {
|
|
1045
|
-
if (!items) return [];
|
|
1046
|
-
const app = this.getApp();
|
|
1047
|
-
const activeUser = app?.activeUser;
|
|
1048
|
-
return items.filter((item) => {
|
|
1049
|
-
if (item.permissions && activeUser) {
|
|
1050
|
-
return activeUser.hasPermission(item.permissions);
|
|
1051
|
-
}
|
|
1052
|
-
return true;
|
|
1053
|
-
});
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
class Token {
|
|
1057
|
-
constructor(token) {
|
|
1058
|
-
this.token = token;
|
|
1059
|
-
this.payload = null;
|
|
1060
|
-
this.uid = null;
|
|
1061
|
-
this.email = null;
|
|
1062
|
-
this.name = null;
|
|
1063
|
-
this.exp = null;
|
|
1064
|
-
this.iat = null;
|
|
1065
|
-
this.isValidToken = false;
|
|
1066
|
-
this._decode();
|
|
1067
|
-
}
|
|
1068
|
-
/**
|
|
1069
|
-
* Decode JWT token payload (client-side only, no verification)
|
|
1070
|
-
* @private
|
|
1071
|
-
*/
|
|
1072
|
-
_decode() {
|
|
1073
|
-
if (!this.token || typeof this.token !== "string") {
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
|
-
try {
|
|
1077
|
-
const parts = this.token.split(".");
|
|
1078
|
-
if (parts.length !== 3) {
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
const payload = parts[1];
|
|
1082
|
-
let base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
1083
|
-
const padding = 4 - base64.length % 4;
|
|
1084
|
-
if (padding !== 4) {
|
|
1085
|
-
base64 += "=".repeat(padding);
|
|
1086
|
-
}
|
|
1087
|
-
const decoded = atob(base64);
|
|
1088
|
-
this.payload = JSON.parse(decoded);
|
|
1089
|
-
this.uid = this.payload.uid || this.payload.sub || this.payload.user_id || null;
|
|
1090
|
-
this.email = this.payload.email || null;
|
|
1091
|
-
this.name = this.payload.name || this.payload.username || null;
|
|
1092
|
-
this.exp = this.payload.exp ? new Date(this.payload.exp * 1e3) : null;
|
|
1093
|
-
this.iat = this.payload.iat ? new Date(this.payload.iat * 1e3) : null;
|
|
1094
|
-
this.isValidToken = this._checkValidity();
|
|
1095
|
-
} catch (error) {
|
|
1096
|
-
this.payload = null;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
/**
|
|
1100
|
-
* Check token validity
|
|
1101
|
-
* @private
|
|
1102
|
-
* @returns {boolean} True if token is valid
|
|
1103
|
-
*/
|
|
1104
|
-
_checkValidity() {
|
|
1105
|
-
if (!this.token || !this.payload) {
|
|
1106
|
-
return false;
|
|
1107
|
-
}
|
|
1108
|
-
if (this.payload.exp) {
|
|
1109
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
1110
|
-
return now < this.payload.exp;
|
|
1111
|
-
}
|
|
1112
|
-
return true;
|
|
1113
|
-
}
|
|
1114
|
-
/**
|
|
1115
|
-
* Decode JWT token payload (client-side only, no verification)
|
|
1116
|
-
* @returns {object|null} Decoded payload or null if invalid
|
|
1117
|
-
*/
|
|
1118
|
-
decode() {
|
|
1119
|
-
return this.payload;
|
|
1120
|
-
}
|
|
1121
|
-
/**
|
|
1122
|
-
* Get user ID from token
|
|
1123
|
-
* @returns {string|null} User ID or null if not found
|
|
1124
|
-
*/
|
|
1125
|
-
getUserId() {
|
|
1126
|
-
return this.uid;
|
|
1127
|
-
}
|
|
1128
|
-
/**
|
|
1129
|
-
* Check if token is valid (exists and not expired)
|
|
1130
|
-
* @returns {boolean} True if token is valid
|
|
1131
|
-
*/
|
|
1132
|
-
isValid() {
|
|
1133
|
-
return this.isValidToken;
|
|
1134
|
-
}
|
|
1135
|
-
/**
|
|
1136
|
-
* Check if token will expire soon
|
|
1137
|
-
* @param {number} thresholdMinutes - Minutes before expiry to consider "soon"
|
|
1138
|
-
* @returns {boolean} True if expiring soon
|
|
1139
|
-
*/
|
|
1140
|
-
isExpiringSoon(thresholdMinutes = 5) {
|
|
1141
|
-
if (!this.payload?.exp) {
|
|
1142
|
-
return false;
|
|
1143
|
-
}
|
|
1144
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
1145
|
-
const threshold = thresholdMinutes * 60;
|
|
1146
|
-
return this.payload.exp - now <= threshold;
|
|
1147
|
-
}
|
|
1148
|
-
/**
|
|
1149
|
-
* Check if token is expired
|
|
1150
|
-
* @returns {boolean} True if expired
|
|
1151
|
-
*/
|
|
1152
|
-
isExpired() {
|
|
1153
|
-
if (!this.payload?.exp) {
|
|
1154
|
-
return false;
|
|
1155
|
-
}
|
|
1156
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
1157
|
-
return now >= this.payload.exp;
|
|
1158
|
-
}
|
|
1159
|
-
/**
|
|
1160
|
-
* Get token age in minutes
|
|
1161
|
-
* @returns {number|null} Age in minutes since token was issued, or null if no iat
|
|
1162
|
-
*/
|
|
1163
|
-
getAgeMinutes() {
|
|
1164
|
-
if (!this.payload?.iat) {
|
|
1165
|
-
return null;
|
|
1166
|
-
}
|
|
1167
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
1168
|
-
const ageSeconds = now - this.payload.iat;
|
|
1169
|
-
return Math.floor(ageSeconds / 60);
|
|
1170
|
-
}
|
|
1171
|
-
/**
|
|
1172
|
-
* Get authorization header value
|
|
1173
|
-
* @returns {string|null} Bearer token string or null if no token
|
|
1174
|
-
*/
|
|
1175
|
-
getAuthHeader() {
|
|
1176
|
-
return this.token ? `Bearer ${this.token}` : null;
|
|
1177
|
-
}
|
|
1178
|
-
/**
|
|
1179
|
-
* Get basic user info from token
|
|
1180
|
-
* @returns {object|null} User info or null
|
|
1181
|
-
*/
|
|
1182
|
-
getUserInfo() {
|
|
1183
|
-
if (!this.payload) {
|
|
1184
|
-
return null;
|
|
1185
|
-
}
|
|
1186
|
-
return {
|
|
1187
|
-
uid: this.uid,
|
|
1188
|
-
email: this.email,
|
|
1189
|
-
name: this.name,
|
|
1190
|
-
exp: this.exp,
|
|
1191
|
-
iat: this.iat
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
class TokenManager {
|
|
1196
|
-
constructor() {
|
|
1197
|
-
this.tokenKey = "access_token";
|
|
1198
|
-
this.refreshTokenKey = "refresh_token";
|
|
1199
|
-
this.tokenInstance = null;
|
|
1200
|
-
}
|
|
1201
|
-
/**
|
|
1202
|
-
* Store authentication tokens
|
|
1203
|
-
* @param {string} token - Access token
|
|
1204
|
-
* @param {string} refreshToken - Refresh token (optional)
|
|
1205
|
-
* @param {boolean} persistent - Use localStorage if true, sessionStorage if false
|
|
1206
|
-
*/
|
|
1207
|
-
setTokens(token, refreshToken = null, persistent = true) {
|
|
1208
|
-
const storage = persistent ? localStorage : sessionStorage;
|
|
1209
|
-
this.tokenInstance = new Token(token);
|
|
1210
|
-
if (token) {
|
|
1211
|
-
storage.setItem(this.tokenKey, token);
|
|
1212
|
-
}
|
|
1213
|
-
if (refreshToken) {
|
|
1214
|
-
storage.setItem(this.refreshTokenKey, refreshToken);
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
/**
|
|
1218
|
-
* Get stored access token
|
|
1219
|
-
* @returns {string|null} Access token or null if not found
|
|
1220
|
-
*/
|
|
1221
|
-
getToken() {
|
|
1222
|
-
return localStorage.getItem(this.tokenKey) || sessionStorage.getItem(this.tokenKey);
|
|
1223
|
-
}
|
|
1224
|
-
/**
|
|
1225
|
-
* Get stored refresh token
|
|
1226
|
-
* @returns {string|null} Refresh token or null if not found
|
|
1227
|
-
*/
|
|
1228
|
-
getRefreshToken() {
|
|
1229
|
-
return localStorage.getItem(this.refreshTokenKey) || sessionStorage.getItem(this.refreshTokenKey);
|
|
1230
|
-
}
|
|
1231
|
-
/**
|
|
1232
|
-
* Clear all stored tokens
|
|
1233
|
-
*/
|
|
1234
|
-
clearTokens() {
|
|
1235
|
-
localStorage.removeItem(this.tokenKey);
|
|
1236
|
-
localStorage.removeItem(this.refreshTokenKey);
|
|
1237
|
-
sessionStorage.removeItem(this.tokenKey);
|
|
1238
|
-
sessionStorage.removeItem(this.refreshTokenKey);
|
|
1239
|
-
}
|
|
1240
|
-
/**
|
|
1241
|
-
* Get Token instance for current stored token
|
|
1242
|
-
* @returns {Token|null} Token instance or null if no token
|
|
1243
|
-
*/
|
|
1244
|
-
getTokenInstance() {
|
|
1245
|
-
const currentToken = this.getToken();
|
|
1246
|
-
if (!currentToken) {
|
|
1247
|
-
this.tokenInstance = null;
|
|
1248
|
-
return null;
|
|
1249
|
-
}
|
|
1250
|
-
if (!this.tokenInstance || this.tokenInstance.token !== currentToken) {
|
|
1251
|
-
this.tokenInstance = new Token(currentToken);
|
|
1252
|
-
}
|
|
1253
|
-
return this.tokenInstance;
|
|
1254
|
-
}
|
|
1255
|
-
/**
|
|
1256
|
-
* Get Token instance for refresh token
|
|
1257
|
-
* @returns {Token|null} Token instance or null if no refresh token
|
|
1258
|
-
*/
|
|
1259
|
-
getRefreshTokenInstance() {
|
|
1260
|
-
const currentRefreshToken = this.getRefreshToken();
|
|
1261
|
-
if (!currentRefreshToken) {
|
|
1262
|
-
this._refreshTokenInstance = null;
|
|
1263
|
-
return null;
|
|
1264
|
-
}
|
|
1265
|
-
if (!this._refreshTokenInstance || this._refreshTokenInstance.token !== currentRefreshToken) {
|
|
1266
|
-
this._refreshTokenInstance = new Token(currentRefreshToken);
|
|
1267
|
-
}
|
|
1268
|
-
return this._refreshTokenInstance;
|
|
1269
|
-
}
|
|
1270
|
-
/**
|
|
1271
|
-
* Decode JWT token payload (client-side only, no verification)
|
|
1272
|
-
* @param {string} token - JWT token
|
|
1273
|
-
* @returns {object|null} Decoded payload or null if invalid
|
|
1274
|
-
*/
|
|
1275
|
-
decode(token = null) {
|
|
1276
|
-
const jwt = token || this.getToken();
|
|
1277
|
-
return new Token(jwt).decode();
|
|
1278
|
-
}
|
|
1279
|
-
/**
|
|
1280
|
-
* Get user ID from token
|
|
1281
|
-
* @returns {string|null} User ID or null if not found
|
|
1282
|
-
*/
|
|
1283
|
-
getUserId() {
|
|
1284
|
-
const currentToken = this.getTokenInstance();
|
|
1285
|
-
return currentToken ? currentToken.getUserId() : null;
|
|
1286
|
-
}
|
|
1287
|
-
/**
|
|
1288
|
-
* Check if current token is valid (exists and not expired)
|
|
1289
|
-
* @returns {boolean} True if token is valid
|
|
1290
|
-
*/
|
|
1291
|
-
isValid() {
|
|
1292
|
-
const currentToken = this.getTokenInstance();
|
|
1293
|
-
return currentToken ? currentToken.isValid() : false;
|
|
1294
|
-
}
|
|
1295
|
-
/**
|
|
1296
|
-
* Check if token will expire soon
|
|
1297
|
-
* @param {number} thresholdMinutes - Minutes before expiry to consider "soon"
|
|
1298
|
-
* @returns {boolean} True if expiring soon
|
|
1299
|
-
*/
|
|
1300
|
-
isExpiringSoon(thresholdMinutes = 5) {
|
|
1301
|
-
const currentToken = this.getTokenInstance();
|
|
1302
|
-
return currentToken ? currentToken.isExpiringSoon(thresholdMinutes) : false;
|
|
1303
|
-
}
|
|
1304
|
-
/**
|
|
1305
|
-
* Get authorization header value
|
|
1306
|
-
* @returns {string|null} Bearer token string or null if no token
|
|
1307
|
-
*/
|
|
1308
|
-
getAuthHeader() {
|
|
1309
|
-
const currentToken = this.getTokenInstance();
|
|
1310
|
-
return currentToken ? currentToken.getAuthHeader() : null;
|
|
1311
|
-
}
|
|
1312
|
-
/**
|
|
1313
|
-
* Get basic user info from token
|
|
1314
|
-
* @returns {object|null} User info or null
|
|
1315
|
-
*/
|
|
1316
|
-
getUserInfo() {
|
|
1317
|
-
const currentToken = this.getTokenInstance();
|
|
1318
|
-
return currentToken ? currentToken.getUserInfo() : null;
|
|
1319
|
-
}
|
|
1320
|
-
/**
|
|
1321
|
-
* Check current token status and determine what action is needed
|
|
1322
|
-
* @returns {object} Status object with action and details
|
|
1323
|
-
*/
|
|
1324
|
-
checkTokenStatus() {
|
|
1325
|
-
const token = this.getTokenInstance();
|
|
1326
|
-
const refreshToken = this.getRefreshTokenInstance();
|
|
1327
|
-
if (!token || !token.isValid() || token.isExpired()) {
|
|
1328
|
-
if (!refreshToken || !refreshToken.isValid() || refreshToken.isExpired()) {
|
|
1329
|
-
return {
|
|
1330
|
-
action: "logout",
|
|
1331
|
-
reason: "Both access and refresh tokens are invalid/expired"
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
return {
|
|
1335
|
-
action: "refresh",
|
|
1336
|
-
reason: "Access token invalid/expired but refresh token valid"
|
|
1337
|
-
};
|
|
1338
|
-
}
|
|
1339
|
-
if (token.isExpiringSoon(10) || token.getAgeMinutes() && token.getAgeMinutes() > 60) {
|
|
1340
|
-
if (!refreshToken || !refreshToken.isValid() || refreshToken.isExpired()) {
|
|
1341
|
-
return {
|
|
1342
|
-
action: "none",
|
|
1343
|
-
reason: "Access token expiring but refresh token invalid"
|
|
1344
|
-
};
|
|
1345
|
-
}
|
|
1346
|
-
return {
|
|
1347
|
-
action: "refresh",
|
|
1348
|
-
reason: "Access token expiring soon or aged"
|
|
1349
|
-
};
|
|
1350
|
-
}
|
|
1351
|
-
return {
|
|
1352
|
-
action: "none",
|
|
1353
|
-
reason: "All tokens valid and not expiring soon"
|
|
1354
|
-
};
|
|
1355
|
-
}
|
|
1356
|
-
/**
|
|
1357
|
-
* Check tokens and take appropriate action
|
|
1358
|
-
* @param {object} app - App instance for events and API calls
|
|
1359
|
-
* @returns {Promise<boolean>} True if action was taken
|
|
1360
|
-
*/
|
|
1361
|
-
async checkAndRefreshTokens(app) {
|
|
1362
|
-
const status = this.checkTokenStatus();
|
|
1363
|
-
switch (status.action) {
|
|
1364
|
-
case "logout":
|
|
1365
|
-
app.events.emit("auth:unauthorized");
|
|
1366
|
-
this.stopAutoRefresh();
|
|
1367
|
-
return true;
|
|
1368
|
-
case "refresh":
|
|
1369
|
-
await this.refreshToken(app);
|
|
1370
|
-
return true;
|
|
1371
|
-
default:
|
|
1372
|
-
return false;
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
startAutoRefresh(app) {
|
|
1376
|
-
this.stopAutoRefresh();
|
|
1377
|
-
this._tokenWatcher = setInterval(() => {
|
|
1378
|
-
this.checkAndRefreshTokens(app);
|
|
1379
|
-
}, 6e4);
|
|
1380
|
-
}
|
|
1381
|
-
stopAutoRefresh() {
|
|
1382
|
-
if (this._tokenWatcher) {
|
|
1383
|
-
clearInterval(this._tokenWatcher);
|
|
1384
|
-
this._tokenWatcher = null;
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
async refreshToken(app) {
|
|
1388
|
-
const refreshTokenInstance = this.getRefreshTokenInstance();
|
|
1389
|
-
if (!refreshTokenInstance || !refreshTokenInstance.isValid() || refreshTokenInstance.isExpired()) {
|
|
1390
|
-
app.events.emit("auth:unauthorized");
|
|
1391
|
-
this.stopAutoRefresh();
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
try {
|
|
1395
|
-
const response = await app.rest.POST("/api/token/refresh", {
|
|
1396
|
-
refresh_token: refreshTokenInstance.token
|
|
1397
|
-
});
|
|
1398
|
-
const { access_token, refresh_token } = response.data.data;
|
|
1399
|
-
this.tokenInstance = null;
|
|
1400
|
-
this._refreshTokenInstance = null;
|
|
1401
|
-
this.setTokens(access_token, refresh_token);
|
|
1402
|
-
app.rest.setAuthToken(access_token);
|
|
1403
|
-
app.events.emit("auth:token:refreshed", {
|
|
1404
|
-
newToken: access_token,
|
|
1405
|
-
newRefreshToken: refresh_token
|
|
1406
|
-
});
|
|
1407
|
-
console.log("Token refreshed successfully");
|
|
1408
|
-
} catch (error) {
|
|
1409
|
-
if (error.status === 401 || error.status === 403) {
|
|
1410
|
-
app.events.emit("auth:unauthorized");
|
|
1411
|
-
this.stopAutoRefresh();
|
|
1412
|
-
} else {
|
|
1413
|
-
app.events.emit("auth:token:refresh:failed", { error });
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
export {
|
|
1419
|
-
SimpleSearchView as S,
|
|
1420
|
-
TokenManager as T,
|
|
1421
|
-
TopNav as a
|
|
1422
|
-
};
|
|
1423
|
-
//# sourceMappingURL=TokenManager-DoN9e6q6.js.map
|