web-mojo 2.1.46
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/LICENSE +198 -0
- package/README.md +510 -0
- package/dist/admin.cjs.js +2 -0
- package/dist/admin.cjs.js.map +1 -0
- package/dist/admin.css +621 -0
- package/dist/admin.es.js +7973 -0
- package/dist/admin.es.js.map +1 -0
- package/dist/auth.cjs.js +2 -0
- package/dist/auth.cjs.js.map +1 -0
- package/dist/auth.css +804 -0
- package/dist/auth.es.js +2168 -0
- package/dist/auth.es.js.map +1 -0
- package/dist/charts.cjs.js +2 -0
- package/dist/charts.cjs.js.map +1 -0
- package/dist/charts.css +1002 -0
- package/dist/charts.es.js +16 -0
- package/dist/charts.es.js.map +1 -0
- package/dist/chunks/ContextMenu-BrHqj0fn.js +80 -0
- package/dist/chunks/ContextMenu-BrHqj0fn.js.map +1 -0
- package/dist/chunks/ContextMenu-gEcpSz56.js +2 -0
- package/dist/chunks/ContextMenu-gEcpSz56.js.map +1 -0
- package/dist/chunks/DataView-DPryYpEW.js +2 -0
- package/dist/chunks/DataView-DPryYpEW.js.map +1 -0
- package/dist/chunks/DataView-DjZQrpba.js +843 -0
- package/dist/chunks/DataView-DjZQrpba.js.map +1 -0
- package/dist/chunks/Dialog-BsRx4eg3.js +2 -0
- package/dist/chunks/Dialog-BsRx4eg3.js.map +1 -0
- package/dist/chunks/Dialog-DSlctbon.js +1377 -0
- package/dist/chunks/Dialog-DSlctbon.js.map +1 -0
- package/dist/chunks/FilePreviewView-BmFHzK5K.js +5868 -0
- package/dist/chunks/FilePreviewView-BmFHzK5K.js.map +1 -0
- package/dist/chunks/FilePreviewView-DcdRl_ta.js +2 -0
- package/dist/chunks/FilePreviewView-DcdRl_ta.js.map +1 -0
- package/dist/chunks/FormView-CmBuwKGD.js +2 -0
- package/dist/chunks/FormView-CmBuwKGD.js.map +1 -0
- package/dist/chunks/FormView-DqUBMPJ9.js +5054 -0
- package/dist/chunks/FormView-DqUBMPJ9.js.map +1 -0
- package/dist/chunks/MetricsChart-CM4CI6eA.js +2095 -0
- package/dist/chunks/MetricsChart-CM4CI6eA.js.map +1 -0
- package/dist/chunks/MetricsChart-CPidSMaN.js +2 -0
- package/dist/chunks/MetricsChart-CPidSMaN.js.map +1 -0
- package/dist/chunks/PDFViewer-BNQlnS83.js +2 -0
- package/dist/chunks/PDFViewer-BNQlnS83.js.map +1 -0
- package/dist/chunks/PDFViewer-Dyo-Oeyd.js +946 -0
- package/dist/chunks/PDFViewer-Dyo-Oeyd.js.map +1 -0
- package/dist/chunks/Page-B524zSQs.js +351 -0
- package/dist/chunks/Page-B524zSQs.js.map +1 -0
- package/dist/chunks/Page-BFgj0pAA.js +2 -0
- package/dist/chunks/Page-BFgj0pAA.js.map +1 -0
- package/dist/chunks/TokenManager-BXNva8Jk.js +287 -0
- package/dist/chunks/TokenManager-BXNva8Jk.js.map +1 -0
- package/dist/chunks/TokenManager-Bzn4guFm.js +2 -0
- package/dist/chunks/TokenManager-Bzn4guFm.js.map +1 -0
- package/dist/chunks/TopNav-D3I3_25f.js +371 -0
- package/dist/chunks/TopNav-D3I3_25f.js.map +1 -0
- package/dist/chunks/TopNav-MDjL4kV0.js +2 -0
- package/dist/chunks/TopNav-MDjL4kV0.js.map +1 -0
- package/dist/chunks/User-BalfYTEF.js +3 -0
- package/dist/chunks/User-BalfYTEF.js.map +1 -0
- package/dist/chunks/User-DwIT-CTQ.js +1937 -0
- package/dist/chunks/User-DwIT-CTQ.js.map +1 -0
- package/dist/chunks/WebApp-B6mgbNn2.js +4767 -0
- package/dist/chunks/WebApp-B6mgbNn2.js.map +1 -0
- package/dist/chunks/WebApp-DqDowtkl.js +2 -0
- package/dist/chunks/WebApp-DqDowtkl.js.map +1 -0
- package/dist/chunks/WebSocketClient-D6i85jl2.js +2 -0
- package/dist/chunks/WebSocketClient-D6i85jl2.js.map +1 -0
- package/dist/chunks/WebSocketClient-Dvl3AYx1.js +297 -0
- package/dist/chunks/WebSocketClient-Dvl3AYx1.js.map +1 -0
- package/dist/core.css +1181 -0
- package/dist/css/web-mojo.css +17 -0
- package/dist/css-manifest.json +6 -0
- package/dist/docit.cjs.js +2 -0
- package/dist/docit.cjs.js.map +1 -0
- package/dist/docit.es.js +959 -0
- package/dist/docit.es.js.map +1 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.es.js +2681 -0
- package/dist/index.es.js.map +1 -0
- package/dist/lightbox.cjs.js +2 -0
- package/dist/lightbox.cjs.js.map +1 -0
- package/dist/lightbox.css +606 -0
- package/dist/lightbox.es.js +3737 -0
- package/dist/lightbox.es.js.map +1 -0
- package/dist/loader.es.js +115 -0
- package/dist/loader.umd.js +85 -0
- package/dist/portal.css +2446 -0
- package/dist/table.css +639 -0
- package/dist/toast.css +181 -0
- package/package.json +179 -0
package/dist/index.es.js
ADDED
|
@@ -0,0 +1,2681 @@
|
|
|
1
|
+
import { V as View, W as WebApp, d as dataFormatter, M as Mustache } from "./chunks/WebApp-B6mgbNn2.js";
|
|
2
|
+
import { B, D, g, E, h, r, R, b, a, c, e, f } from "./chunks/WebApp-B6mgbNn2.js";
|
|
3
|
+
import { P as Page } from "./chunks/Page-B524zSQs.js";
|
|
4
|
+
import { G as GroupList, T as ToastService, U as User, a as Group } from "./chunks/User-DwIT-CTQ.js";
|
|
5
|
+
import { C, b as b2, M, e as e2, f as f2, g as g2, h as h2, i, d, c as c2 } from "./chunks/User-DwIT-CTQ.js";
|
|
6
|
+
import { E as E2, i as i2, h as h3, o, q, p, u, w, v, r as r2, t, s, F, e as e3, am, an, z, I, y, x, B as B2, J, K, A, G, H, C as C2, D as D2, U, V, _, $, X, W, Y, Z, a1, a3, a2, a0, L, c as c3, a4, a5, M as M2, k, j, a6, a8, a7, ab, a9, aa, P, ag, ak, ah, ai, aj, ac, ad, ae, al, af, O, Q, R as R2, N, S, g as g3, f as f3, l, n, m, d as d2, b as b3, a as a10, T, ao, as, ap, aq, ar } from "./chunks/FilePreviewView-BmFHzK5K.js";
|
|
7
|
+
import { T as TopNav } from "./chunks/TopNav-D3I3_25f.js";
|
|
8
|
+
import { T as TokenManager } from "./chunks/TokenManager-BXNva8Jk.js";
|
|
9
|
+
import Dialog from "./chunks/Dialog-DSlctbon.js";
|
|
10
|
+
import { default as default2 } from "./chunks/DataView-DjZQrpba.js";
|
|
11
|
+
import { F as F2, a as a11 } from "./chunks/FormView-DqUBMPJ9.js";
|
|
12
|
+
import { W as W2 } from "./chunks/WebSocketClient-Dvl3AYx1.js";
|
|
13
|
+
class ResultsView extends View {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
super({
|
|
16
|
+
className: "search-results-view flex-grow-1 overflow-auto d-flex flex-column",
|
|
17
|
+
template: `
|
|
18
|
+
<div class="flex-grow-1 overflow-auto">
|
|
19
|
+
{{#data.loading}}
|
|
20
|
+
<div class="text-center p-4">
|
|
21
|
+
<div class="spinner-border spinner-border-sm text-muted" role="status">
|
|
22
|
+
<span class="visually-hidden">Loading...</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="mt-2 small text-muted">{{data.loadingText}}</div>
|
|
25
|
+
</div>
|
|
26
|
+
{{/data.loading}}
|
|
27
|
+
|
|
28
|
+
{{^data.loading}}
|
|
29
|
+
{{#data.items}}
|
|
30
|
+
<div class="simple-search-item position-relative"
|
|
31
|
+
data-action="select-item"
|
|
32
|
+
data-item-index="{{index}}">
|
|
33
|
+
{{{itemContent}}}
|
|
34
|
+
<i class="bi bi-chevron-right position-absolute end-0 top-50 translate-middle-y me-3 text-muted"></i>
|
|
35
|
+
</div>
|
|
36
|
+
{{/data.items}}
|
|
37
|
+
|
|
38
|
+
{{#data.showNoResults}}
|
|
39
|
+
<div class="text-center p-4">
|
|
40
|
+
<i class="bi bi-search text-muted mb-2" style="font-size: 1.5rem;"></i>
|
|
41
|
+
<div class="text-muted small">{{data.noResultsText}}</div>
|
|
42
|
+
<button type="button"
|
|
43
|
+
class="btn btn-link btn-sm mt-2 p-0"
|
|
44
|
+
data-action="clear-search">
|
|
45
|
+
Clear search
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
{{/data.showNoResults}}
|
|
49
|
+
|
|
50
|
+
{{#data.showEmpty}}
|
|
51
|
+
<div class="text-center p-4">
|
|
52
|
+
<i class="{{data.emptyIcon}} text-muted mb-2" style="font-size: 2rem;"></i>
|
|
53
|
+
<div class="text-muted small mb-2">{{data.emptyText}}</div>
|
|
54
|
+
{{#data.emptySubtext}}
|
|
55
|
+
<div class="text-muted" style="font-size: 0.75rem;">
|
|
56
|
+
{{data.emptySubtext}}
|
|
57
|
+
</div>
|
|
58
|
+
{{/data.emptySubtext}}
|
|
59
|
+
</div>
|
|
60
|
+
{{/data.showEmpty}}
|
|
61
|
+
{{/data.loading}}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{{#data.showResultsCount}}
|
|
65
|
+
<div class="border-top bg-light p-2 text-center">
|
|
66
|
+
<small class="text-muted">
|
|
67
|
+
{{data.filteredCount}} of {{data.totalCount}}
|
|
68
|
+
</small>
|
|
69
|
+
</div>
|
|
70
|
+
{{/data.showResultsCount}}
|
|
71
|
+
`,
|
|
72
|
+
...options
|
|
73
|
+
});
|
|
74
|
+
this.parentView = options.parentView;
|
|
75
|
+
}
|
|
76
|
+
async handleActionSelectItem(event, element) {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
const itemIndex = parseInt(element.getAttribute("data-item-index"));
|
|
79
|
+
if (this.parentView) {
|
|
80
|
+
this.parentView.handleItemSelection(itemIndex);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async handleActionClearSearch(event, _element) {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
if (this.parentView) {
|
|
86
|
+
this.parentView.clearSearch();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
class SimpleSearchView extends View {
|
|
91
|
+
constructor(options = {}) {
|
|
92
|
+
super({
|
|
93
|
+
className: "simple-search-view h-100 d-flex flex-column",
|
|
94
|
+
template: `
|
|
95
|
+
<div class="p-3 border-bottom bg-light">
|
|
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
|
+
<div class="position-relative">
|
|
112
|
+
<input type="text"
|
|
113
|
+
class="form-control form-control-sm pe-5"
|
|
114
|
+
placeholder="{{data.searchPlaceholder}}"
|
|
115
|
+
value="{{data.searchValue}}"
|
|
116
|
+
data-filter="live-search"
|
|
117
|
+
data-filter-debounce="{{data.debounceMs}}"
|
|
118
|
+
data-change-action="search-items">
|
|
119
|
+
<button class="btn btn-link p-0 position-absolute top-50 end-0 translate-middle-y me-2 text-muted simple-search-clear-btn"
|
|
120
|
+
type="button"
|
|
121
|
+
data-action="clear-search"
|
|
122
|
+
title="Clear search"
|
|
123
|
+
aria-label="Clear search">
|
|
124
|
+
<i class="bi bi-x-circle-fill" aria-hidden="true"></i>
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div data-container="results"></div>
|
|
130
|
+
|
|
131
|
+
{{#data.showFooter}}
|
|
132
|
+
<div class="p-3 border-top bg-light">
|
|
133
|
+
<small class="text-muted">
|
|
134
|
+
<i class="{{data.footerIcon}} me-1"></i>
|
|
135
|
+
{{{data.footerContent}}}
|
|
136
|
+
</small>
|
|
137
|
+
</div>
|
|
138
|
+
{{/data.showFooter}}
|
|
139
|
+
`,
|
|
140
|
+
...options
|
|
141
|
+
});
|
|
142
|
+
this.Collection = options.Collection;
|
|
143
|
+
this.collection = options.collection;
|
|
144
|
+
this.itemTemplate = options.itemTemplate || this.getDefaultItemTemplate();
|
|
145
|
+
this.searchFields = options.searchFields || ["name"];
|
|
146
|
+
this.collectionParams = { size: 25, ...options.collectionParams };
|
|
147
|
+
this.headerText = options.headerText || "Select Item";
|
|
148
|
+
this.headerIcon = options.headerIcon || "bi bi-list";
|
|
149
|
+
this.searchPlaceholder = options.searchPlaceholder || "Search...";
|
|
150
|
+
this.loadingText = options.loadingText || "Loading items...";
|
|
151
|
+
this.noResultsText = options.noResultsText || "No items match your search";
|
|
152
|
+
this.emptyText = options.emptyText || "No items available";
|
|
153
|
+
this.emptySubtext = options.emptySubtext || null;
|
|
154
|
+
this.emptyIcon = options.emptyIcon || "bi bi-inbox";
|
|
155
|
+
this.footerContent = options.footerContent || null;
|
|
156
|
+
this.footerIcon = options.footerIcon || "bi bi-info-circle";
|
|
157
|
+
this.showExitButton = options.showExitButton || false;
|
|
158
|
+
this.searchValue = "";
|
|
159
|
+
this.filteredItems = [];
|
|
160
|
+
this.loading = false;
|
|
161
|
+
this.hasSearched = false;
|
|
162
|
+
this.searchTimer = null;
|
|
163
|
+
this.debounceMs = options.debounceMs || 800;
|
|
164
|
+
this.resultsView = new ResultsView({
|
|
165
|
+
parentView: this
|
|
166
|
+
});
|
|
167
|
+
if (!this.collection && this.Collection) {
|
|
168
|
+
this.collection = new this.Collection();
|
|
169
|
+
}
|
|
170
|
+
this.addChild(this.resultsView);
|
|
171
|
+
}
|
|
172
|
+
onInit() {
|
|
173
|
+
if (this.collection) {
|
|
174
|
+
this.setupCollection();
|
|
175
|
+
}
|
|
176
|
+
if (this.collection && this.options.autoLoad !== false) {
|
|
177
|
+
this.loadItems();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
setupCollection() {
|
|
181
|
+
Object.assign(this.collection.params, this.collectionParams);
|
|
182
|
+
this.collection.on("fetch:success", () => {
|
|
183
|
+
this.loading = false;
|
|
184
|
+
this.updateFilteredItems();
|
|
185
|
+
});
|
|
186
|
+
this.collection.on("fetch:error", () => {
|
|
187
|
+
this.loading = false;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async loadItems() {
|
|
191
|
+
if (!this.collection) {
|
|
192
|
+
console.warn("SimpleSearchView: No collection provided");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
this.loading = true;
|
|
196
|
+
this.updateResultsView();
|
|
197
|
+
try {
|
|
198
|
+
await this.collection.fetch();
|
|
199
|
+
this.updateFilteredItems();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error("Error loading items:", error);
|
|
202
|
+
const app = this.getApp();
|
|
203
|
+
app?.showError?.("Failed to load items. Please try again.");
|
|
204
|
+
} finally {
|
|
205
|
+
this.loading = false;
|
|
206
|
+
this.updateFilteredItems();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
updateFilteredItems() {
|
|
210
|
+
if (!this.collection) {
|
|
211
|
+
this.filteredItems = [];
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const items = this.collection.toJSON();
|
|
215
|
+
if (!this.searchValue || !this.searchValue.trim()) {
|
|
216
|
+
this.filteredItems = items;
|
|
217
|
+
} else {
|
|
218
|
+
const searchTerm = this.searchValue.toLowerCase().trim();
|
|
219
|
+
this.filteredItems = items.filter((item) => {
|
|
220
|
+
return this.searchFields.some((field) => {
|
|
221
|
+
const value = this.getNestedValue(item, field);
|
|
222
|
+
return value && value.toString().toLowerCase().includes(searchTerm);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
this.updateResultsView();
|
|
227
|
+
}
|
|
228
|
+
getNestedValue(obj, path) {
|
|
229
|
+
return path.split(".").reduce((current, key) => current?.[key], obj);
|
|
230
|
+
}
|
|
231
|
+
async getViewData() {
|
|
232
|
+
return {
|
|
233
|
+
searchValue: this.searchValue,
|
|
234
|
+
showFooter: !!this.footerContent,
|
|
235
|
+
showExitButton: this.showExitButton,
|
|
236
|
+
debounceMs: this.debounceMs,
|
|
237
|
+
// UI text
|
|
238
|
+
headerText: this.headerText,
|
|
239
|
+
headerIcon: this.headerIcon,
|
|
240
|
+
searchPlaceholder: this.searchPlaceholder,
|
|
241
|
+
footerContent: this.footerContent,
|
|
242
|
+
footerIcon: this.footerIcon
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
updateResultsView() {
|
|
246
|
+
if (!this.resultsView) return;
|
|
247
|
+
const hasItems = this.collection && this.collection.length() > 0;
|
|
248
|
+
const hasFilteredItems = this.filteredItems.length > 0;
|
|
249
|
+
const hasSearchValue = this.searchValue.length > 0;
|
|
250
|
+
const processedItems = this.filteredItems.map((item, index2) => {
|
|
251
|
+
return {
|
|
252
|
+
...item,
|
|
253
|
+
index: index2,
|
|
254
|
+
itemContent: this.processItemTemplate(item)
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
this.resultsView.data = {
|
|
258
|
+
loading: this.loading,
|
|
259
|
+
items: processedItems,
|
|
260
|
+
showEmpty: !this.loading && !hasItems,
|
|
261
|
+
showNoResults: !this.loading && hasItems && !hasFilteredItems && hasSearchValue,
|
|
262
|
+
showResultsCount: !this.loading && hasItems,
|
|
263
|
+
filteredCount: this.filteredItems.length,
|
|
264
|
+
totalCount: this.collection?.restEnabled ? this.collection?.meta?.count || 0 : this.collection?.length() || 0,
|
|
265
|
+
// UI text
|
|
266
|
+
loadingText: this.loadingText,
|
|
267
|
+
noResultsText: this.noResultsText,
|
|
268
|
+
emptyText: this.emptyText,
|
|
269
|
+
emptySubtext: this.emptySubtext,
|
|
270
|
+
emptyIcon: this.emptyIcon
|
|
271
|
+
};
|
|
272
|
+
this.resultsView.render();
|
|
273
|
+
}
|
|
274
|
+
processItemTemplate(item) {
|
|
275
|
+
let template = this.itemTemplate;
|
|
276
|
+
template = template.replace(/\{\{(\w+)\}\}/g, (match, prop) => {
|
|
277
|
+
return this.getNestedValue(item, prop) || "";
|
|
278
|
+
});
|
|
279
|
+
return template;
|
|
280
|
+
}
|
|
281
|
+
getDefaultItemTemplate() {
|
|
282
|
+
return `
|
|
283
|
+
<div class="p-3 border-bottom">
|
|
284
|
+
<div class="fw-semibold text-dark">{{name}}</div>
|
|
285
|
+
<small class="text-muted">{{id}}</small>
|
|
286
|
+
</div>
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
async onPassThruActionSearchItems(event, element) {
|
|
290
|
+
const searchValue = element.value || "";
|
|
291
|
+
console.log("search change...");
|
|
292
|
+
this.searchValue = searchValue;
|
|
293
|
+
this.hasSearched = true;
|
|
294
|
+
if (this.searchTimer) {
|
|
295
|
+
clearTimeout(this.searchTimer);
|
|
296
|
+
}
|
|
297
|
+
this.performSearch();
|
|
298
|
+
}
|
|
299
|
+
async performSearch() {
|
|
300
|
+
const searchParams = { ...this.collectionParams };
|
|
301
|
+
if (this.searchValue && this.searchValue.length > 1) {
|
|
302
|
+
searchParams.search = this.searchValue.trim();
|
|
303
|
+
}
|
|
304
|
+
this.collection.setParams(searchParams, true);
|
|
305
|
+
}
|
|
306
|
+
handleItemSelection(itemIndex) {
|
|
307
|
+
if (isNaN(itemIndex) || itemIndex < 0 || itemIndex >= this.filteredItems.length) {
|
|
308
|
+
console.error("Invalid item index:", itemIndex);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const item = this.filteredItems[itemIndex];
|
|
312
|
+
const model = this.collection ? this.collection.get(item.id) : null;
|
|
313
|
+
this.emit("item:selected", {
|
|
314
|
+
item,
|
|
315
|
+
model,
|
|
316
|
+
index: itemIndex
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Set the collection for this search view
|
|
321
|
+
*/
|
|
322
|
+
setCollection(collection) {
|
|
323
|
+
this.collection = collection;
|
|
324
|
+
this.setupCollection();
|
|
325
|
+
return this;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Set the item template
|
|
329
|
+
*/
|
|
330
|
+
setItemTemplate(template) {
|
|
331
|
+
this.itemTemplate = template;
|
|
332
|
+
this.updateResultsView();
|
|
333
|
+
return this;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Set search fields
|
|
337
|
+
*/
|
|
338
|
+
setSearchFields(fields) {
|
|
339
|
+
this.searchFields = Array.isArray(fields) ? fields : [fields];
|
|
340
|
+
return this;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Refresh items list
|
|
344
|
+
*/
|
|
345
|
+
async refresh() {
|
|
346
|
+
await this.loadItems();
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Focus the search input
|
|
350
|
+
*/
|
|
351
|
+
focusSearch() {
|
|
352
|
+
const searchInput = this.element?.querySelector('input[data-action="search-items"]');
|
|
353
|
+
if (searchInput) {
|
|
354
|
+
searchInput.focus();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Handle exit button click - emits event instead of closing
|
|
359
|
+
*/
|
|
360
|
+
async handleActionExitView(event, element) {
|
|
361
|
+
this.emit("exit", { view: this });
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Clear search and reset
|
|
365
|
+
*/
|
|
366
|
+
async handleActionClearSearch(event, element) {
|
|
367
|
+
this.clearSearch();
|
|
368
|
+
}
|
|
369
|
+
clearSearch() {
|
|
370
|
+
this.searchValue = "";
|
|
371
|
+
this.hasSearched = false;
|
|
372
|
+
const searchInput = this.element?.querySelector('input[data-change-action="search-items"]');
|
|
373
|
+
if (searchInput) {
|
|
374
|
+
searchInput.value = "";
|
|
375
|
+
searchInput.focus();
|
|
376
|
+
}
|
|
377
|
+
this.performSearch();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Get the number of available items
|
|
381
|
+
*/
|
|
382
|
+
getItemCount() {
|
|
383
|
+
return this.collection ? this.collection.length() : 0;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get the number of filtered items
|
|
387
|
+
*/
|
|
388
|
+
getFilteredItemCount() {
|
|
389
|
+
return this.filteredItems.length;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Check if items are loaded
|
|
393
|
+
*/
|
|
394
|
+
hasItems() {
|
|
395
|
+
return this.getItemCount() > 0;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Get current search value
|
|
399
|
+
*/
|
|
400
|
+
getSearchValue() {
|
|
401
|
+
return this.searchValue;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Set search value programmatically
|
|
405
|
+
*/
|
|
406
|
+
setSearchValue(value) {
|
|
407
|
+
this.searchValue = value || "";
|
|
408
|
+
this.hasSearched = !!this.searchValue;
|
|
409
|
+
const searchInput = this.element?.querySelector('input[data-action="search-items"]');
|
|
410
|
+
if (searchInput) {
|
|
411
|
+
searchInput.value = this.searchValue;
|
|
412
|
+
}
|
|
413
|
+
this.performSearch();
|
|
414
|
+
return this;
|
|
415
|
+
}
|
|
416
|
+
async onAfterRender() {
|
|
417
|
+
await super.onAfterRender();
|
|
418
|
+
if (this.resultsView && !this.resultsView.isMounted()) {
|
|
419
|
+
const container = this.element?.querySelector('[data-container="results"]');
|
|
420
|
+
if (container) {
|
|
421
|
+
await this.resultsView.render(true, container);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
this.updateResultsView();
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Cleanup on destroy
|
|
428
|
+
*/
|
|
429
|
+
async onBeforeDestroy() {
|
|
430
|
+
if (this.searchTimer) {
|
|
431
|
+
clearTimeout(this.searchTimer);
|
|
432
|
+
}
|
|
433
|
+
if (this.collection) {
|
|
434
|
+
this.collection.off("update");
|
|
435
|
+
}
|
|
436
|
+
await super.onBeforeDestroy();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
class Sidebar extends View {
|
|
440
|
+
constructor(options = {}) {
|
|
441
|
+
super({
|
|
442
|
+
tagName: "nav",
|
|
443
|
+
className: "sidebar",
|
|
444
|
+
id: "sidebar",
|
|
445
|
+
...options
|
|
446
|
+
});
|
|
447
|
+
this.menus = /* @__PURE__ */ new Map();
|
|
448
|
+
this.activeMenuName = null;
|
|
449
|
+
this.currentRoute = null;
|
|
450
|
+
this.showToggle = options.showToggle;
|
|
451
|
+
this.isCollapsed = false;
|
|
452
|
+
this.sidebarTheme = options.theme || "sidebar-light";
|
|
453
|
+
this.customView = null;
|
|
454
|
+
if (this.sidebarTheme) {
|
|
455
|
+
this.addClass(this.sidebarTheme);
|
|
456
|
+
}
|
|
457
|
+
this.initializeMenus(options);
|
|
458
|
+
this.setupRouteListeners();
|
|
459
|
+
if (options.autoCollapseMobile !== false) {
|
|
460
|
+
this.setupResponsiveBehavior();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Initialize sidebar and auto-switch to correct menu based on current route
|
|
465
|
+
*/
|
|
466
|
+
async onInit() {
|
|
467
|
+
await super.onInit();
|
|
468
|
+
const app = this.getApp();
|
|
469
|
+
const router = app?.router;
|
|
470
|
+
if (router) {
|
|
471
|
+
const currentPath = router.getCurrentPath();
|
|
472
|
+
if (currentPath) {
|
|
473
|
+
this.autoSwitchToMenuForRoute(currentPath);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
this.initializeTooltips();
|
|
477
|
+
this.searchView = new SimpleSearchView({
|
|
478
|
+
noAppend: true,
|
|
479
|
+
showExitButton: true,
|
|
480
|
+
headerText: "Select Group",
|
|
481
|
+
containerId: "sidebar-search-container",
|
|
482
|
+
Collection: GroupList,
|
|
483
|
+
itemTemplate: `
|
|
484
|
+
<div class="p-3 border-bottom">
|
|
485
|
+
<div class="fw-semibold text-dark">{{name}}</div>
|
|
486
|
+
<small class="text-muted">#{{id}} {{kind}}</small>
|
|
487
|
+
</div>
|
|
488
|
+
`
|
|
489
|
+
});
|
|
490
|
+
this.addChild(this.searchView);
|
|
491
|
+
this.searchView.on("item:selected", (evt) => {
|
|
492
|
+
console.log(evt);
|
|
493
|
+
this.getApp().setActiveGroup(evt.model);
|
|
494
|
+
});
|
|
495
|
+
this.searchView.on("exit", (item) => {
|
|
496
|
+
console.log(item);
|
|
497
|
+
this.hideGroupSearch();
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
showGroupSearch() {
|
|
501
|
+
this.setClass("sidebar");
|
|
502
|
+
this.showSearch = true;
|
|
503
|
+
this.render();
|
|
504
|
+
}
|
|
505
|
+
hideGroupSearch() {
|
|
506
|
+
this.setClass("sidebar");
|
|
507
|
+
this.showSearch = false;
|
|
508
|
+
this.render();
|
|
509
|
+
}
|
|
510
|
+
onActionShowGroupSearch() {
|
|
511
|
+
this.showGroupSearch();
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Find and switch to the menu that contains the given route
|
|
515
|
+
*/
|
|
516
|
+
autoSwitchToMenuForRoute(route) {
|
|
517
|
+
for (const [menuName, menuConfig] of this.menus) {
|
|
518
|
+
if (menuConfig.groupKind && !this.getApp().activeGroup)
|
|
519
|
+
continue;
|
|
520
|
+
if (this.menuContainsRoute(menuConfig, route)) {
|
|
521
|
+
this._setActiveMenu(menuName);
|
|
522
|
+
this.currentRoute = route;
|
|
523
|
+
this.clearAllActiveStates();
|
|
524
|
+
this.setActiveItemByRoute(route);
|
|
525
|
+
this.render();
|
|
526
|
+
console.log(`Auto-switched to menu '${menuName}' for route '${route}'`);
|
|
527
|
+
this.emit("menu-auto-switched", {
|
|
528
|
+
menuName,
|
|
529
|
+
route,
|
|
530
|
+
config: menuConfig,
|
|
531
|
+
sidebar: this
|
|
532
|
+
});
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Clear active state from all menu items in all menus
|
|
540
|
+
*/
|
|
541
|
+
clearAllActiveStates() {
|
|
542
|
+
for (const [menuName, menuConfig] of this.menus) {
|
|
543
|
+
for (const item of menuConfig.items || []) {
|
|
544
|
+
item.active = false;
|
|
545
|
+
if (item.children) {
|
|
546
|
+
for (const child of item.children) {
|
|
547
|
+
child.active = false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Set active state for item matching the given route
|
|
555
|
+
*/
|
|
556
|
+
setActiveItemByRoute(route) {
|
|
557
|
+
const normalizeRoute = (r3) => {
|
|
558
|
+
if (!r3) return "/";
|
|
559
|
+
const decoded = decodeURIComponent(r3);
|
|
560
|
+
return decoded.startsWith("/") ? decoded : `/${decoded}`;
|
|
561
|
+
};
|
|
562
|
+
const targetRoute = normalizeRoute(route);
|
|
563
|
+
for (const [menuName, menuConfig] of this.menus) {
|
|
564
|
+
if (menuConfig.groupKind && !this.getApp().activeGroup)
|
|
565
|
+
continue;
|
|
566
|
+
for (const item of menuConfig.items || []) {
|
|
567
|
+
if (item.route) {
|
|
568
|
+
const itemRoute = normalizeRoute(item.route);
|
|
569
|
+
if (this.routesMatch(targetRoute, itemRoute)) {
|
|
570
|
+
item.active = true;
|
|
571
|
+
this.activeMenuItem = item;
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (item.children) {
|
|
576
|
+
for (const child of item.children) {
|
|
577
|
+
if (child.route) {
|
|
578
|
+
const childRoute = normalizeRoute(child.route);
|
|
579
|
+
if (this.routesMatch(targetRoute, childRoute)) {
|
|
580
|
+
child.active = true;
|
|
581
|
+
item.active = true;
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Check if a menu contains a specific route in its items or children
|
|
593
|
+
*/
|
|
594
|
+
menuContainsRoute(menuConfig, route) {
|
|
595
|
+
const normalizeRoute = (r3) => {
|
|
596
|
+
if (!r3) return "/";
|
|
597
|
+
const decoded = decodeURIComponent(r3);
|
|
598
|
+
return decoded.startsWith("/") ? decoded : `/${decoded}`;
|
|
599
|
+
};
|
|
600
|
+
const targetRoute = normalizeRoute(route);
|
|
601
|
+
for (const item of menuConfig.items || []) {
|
|
602
|
+
if (item.route) {
|
|
603
|
+
const itemRoute = normalizeRoute(item.route);
|
|
604
|
+
if (this.routesMatch(targetRoute, itemRoute)) {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (item.children) {
|
|
609
|
+
for (const child of item.children) {
|
|
610
|
+
if (child.route) {
|
|
611
|
+
const childRoute = normalizeRoute(child.route);
|
|
612
|
+
if (this.routesMatch(targetRoute, childRoute)) {
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Check if two routes match (using same logic as isItemActive)
|
|
623
|
+
*/
|
|
624
|
+
routesMatch(currentRoute, itemRoute) {
|
|
625
|
+
return this.getApp().router.doRoutesMatch(currentRoute, itemRoute);
|
|
626
|
+
}
|
|
627
|
+
getTemplate() {
|
|
628
|
+
if (this.customView) {
|
|
629
|
+
return '<div class="sidebar-container" id="sidebar-custom-view-container"></div>';
|
|
630
|
+
}
|
|
631
|
+
if (this.showSearch) return this.getSearchTemplate();
|
|
632
|
+
return this.getMenuTemplate();
|
|
633
|
+
}
|
|
634
|
+
getSearchTemplate() {
|
|
635
|
+
return `
|
|
636
|
+
<div class="sidebar-container" id="sidebar-search-container">
|
|
637
|
+
</div>
|
|
638
|
+
`;
|
|
639
|
+
}
|
|
640
|
+
getMenuTemplate() {
|
|
641
|
+
return `
|
|
642
|
+
<div class="sidebar-container">
|
|
643
|
+
{{#data.currentMenu}}
|
|
644
|
+
<!-- Header -->
|
|
645
|
+
{{#header}}
|
|
646
|
+
<div class="sidebar-header">
|
|
647
|
+
{{{header}}}
|
|
648
|
+
{{#showToggle}}
|
|
649
|
+
<button class="sidebar-toggle" data-action="toggle-sidebar"
|
|
650
|
+
aria-label="Toggle Sidebar">
|
|
651
|
+
<i class="bi bi-chevron-left toggle-icon"></i>
|
|
652
|
+
<i class="bi bi-chevron-right toggle-icon"></i>
|
|
653
|
+
</button>
|
|
654
|
+
{{/showToggle}}
|
|
655
|
+
</div>
|
|
656
|
+
{{/header}}
|
|
657
|
+
|
|
658
|
+
<!-- Navigation Items -->
|
|
659
|
+
<div class="sidebar-body">
|
|
660
|
+
<ul class="nav nav-pills flex-column sidebar-nav" id="sidebar-nav-menu">
|
|
661
|
+
{{#items}}
|
|
662
|
+
{{>nav-item}}
|
|
663
|
+
{{/items}}
|
|
664
|
+
</ul>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<!-- Footer -->
|
|
668
|
+
{{#footer}}
|
|
669
|
+
<div class="sidebar-footer">
|
|
670
|
+
{{{footer}}}
|
|
671
|
+
</div>
|
|
672
|
+
{{/footer}}
|
|
673
|
+
{{/data.currentMenu}}
|
|
674
|
+
|
|
675
|
+
{{^data.currentMenu}}
|
|
676
|
+
<div class="sidebar-empty">
|
|
677
|
+
<p class="text-danger text-center">No menu configured</p>
|
|
678
|
+
</div>
|
|
679
|
+
{{/data.currentMenu}}
|
|
680
|
+
</div>
|
|
681
|
+
`;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Get template partials for rendering
|
|
685
|
+
*/
|
|
686
|
+
getPartials() {
|
|
687
|
+
return {
|
|
688
|
+
"nav-item": `
|
|
689
|
+
{{#isDivider}}
|
|
690
|
+
{{>nav-divider}}
|
|
691
|
+
{{/isDivider}}
|
|
692
|
+
{{#isSpacer}}
|
|
693
|
+
{{>nav-spacer}}
|
|
694
|
+
{{/isSpacer}}
|
|
695
|
+
{{^isDivider}}{{^isSpacer}}
|
|
696
|
+
<li class="nav-item">
|
|
697
|
+
{{#hasChildren}}
|
|
698
|
+
<!-- Item with submenu -->
|
|
699
|
+
<a class="nav-link {{#active}}active{{/active}} has-children collapsed"
|
|
700
|
+
data-bs-toggle="collapse"
|
|
701
|
+
href="#collapse-{{id}}"
|
|
702
|
+
role="button"
|
|
703
|
+
aria-expanded="{{#active}}true{{/active}}{{^active}}false{{/active}}"
|
|
704
|
+
data-action="toggle-submenu">
|
|
705
|
+
{{#icon}}<i class="{{icon}} me-2"></i>{{/icon}}
|
|
706
|
+
<span class="nav-text">{{text}}</span>
|
|
707
|
+
{{#badge}}
|
|
708
|
+
<span class="{{badge.class}} ms-auto">{{badge.text}}</span>
|
|
709
|
+
{{/badge}}
|
|
710
|
+
<i class="bi bi-chevron-down nav-arrow ms-auto"></i>
|
|
711
|
+
</a>
|
|
712
|
+
<div class="collapse {{#active}}show{{/active}}" id="collapse-{{id}}" data-bs-parent="#sidebar-nav-menu">
|
|
713
|
+
<ul class="nav flex-column nav-submenu">
|
|
714
|
+
{{#children}}
|
|
715
|
+
<li class="nav-item">
|
|
716
|
+
<a class="nav-link {{#active}}active{{/active}}"
|
|
717
|
+
{{#action}}data-action="{{action}}"{{/action}}
|
|
718
|
+
{{#href}}href="{{href}}"{{/href}}>
|
|
719
|
+
{{#icon}}<i class="{{icon}} me-2"></i>{{/icon}}
|
|
720
|
+
<span class="nav-text">{{text}}</span>
|
|
721
|
+
{{#badge}}
|
|
722
|
+
<span class="{{badge.class}} ms-auto">{{badge.text}}</span>
|
|
723
|
+
{{/badge}}
|
|
724
|
+
</a>
|
|
725
|
+
</li>
|
|
726
|
+
{{/children}}
|
|
727
|
+
</ul>
|
|
728
|
+
</div>
|
|
729
|
+
{{/hasChildren}}
|
|
730
|
+
{{^hasChildren}}
|
|
731
|
+
<!-- Simple item -->
|
|
732
|
+
<a class="nav-link {{#active}}active{{/active}} {{#disabled}}disabled{{/disabled}}"
|
|
733
|
+
{{#action}}{{^disabled}}data-action="{{action}}"{{/disabled}}{{/action}}
|
|
734
|
+
{{#href}}{{^disabled}}href="{{href}}"{{/disabled}}{{/href}}>
|
|
735
|
+
{{#icon}}<i class="{{icon}} me-2"></i>{{/icon}}
|
|
736
|
+
<span class="nav-text">{{text}}</span>
|
|
737
|
+
{{#badge}}
|
|
738
|
+
<span class="{{badge.class}} ms-auto">{{badge.text}}</span>
|
|
739
|
+
{{/badge}}
|
|
740
|
+
</a>
|
|
741
|
+
{{/hasChildren}}
|
|
742
|
+
</li>
|
|
743
|
+
{{/isDivider}}{{/isSpacer}}
|
|
744
|
+
`,
|
|
745
|
+
"nav-divider": `
|
|
746
|
+
<li class="nav-divider-item">
|
|
747
|
+
<hr class="nav-divider-line">
|
|
748
|
+
</li>
|
|
749
|
+
`,
|
|
750
|
+
"nav-spacer": `
|
|
751
|
+
<li class="nav-spacer-item"></li>
|
|
752
|
+
`
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
getGroupHeader() {
|
|
756
|
+
return `
|
|
757
|
+
<div class="sidebar-group-header py-3" data-action="show-group-search">
|
|
758
|
+
<div class='text-center text-muted fs-7'>active group</div>
|
|
759
|
+
<div class='text-center fs-5 px-3'>{{group.name}}</div>
|
|
760
|
+
<div class='text-center fs-6'>kind: {{group.kind}}</div>
|
|
761
|
+
</div>
|
|
762
|
+
`;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Add a menu configuration
|
|
766
|
+
*/
|
|
767
|
+
addMenu(name, config) {
|
|
768
|
+
if (config.groupKind && !config.header) {
|
|
769
|
+
config.header = this.getGroupHeader();
|
|
770
|
+
}
|
|
771
|
+
this.menus.set(name, {
|
|
772
|
+
name,
|
|
773
|
+
groupKind: config.groupKind || null,
|
|
774
|
+
header: config.header || null,
|
|
775
|
+
footer: config.footer || null,
|
|
776
|
+
items: config.items || [],
|
|
777
|
+
data: config.data || {},
|
|
778
|
+
className: config.className || "sidebar sidebar-dark"
|
|
779
|
+
});
|
|
780
|
+
if (!this.activeMenuName) {
|
|
781
|
+
this._setActiveMenu(name);
|
|
782
|
+
}
|
|
783
|
+
return this;
|
|
784
|
+
}
|
|
785
|
+
_setActiveMenu(name) {
|
|
786
|
+
this.showSearch = false;
|
|
787
|
+
this.activeMenuName = name;
|
|
788
|
+
const config = this.getCurrentMenuConfig();
|
|
789
|
+
if (config.className) {
|
|
790
|
+
this.setClass(config.className);
|
|
791
|
+
} else {
|
|
792
|
+
this.setClass("sidebar");
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Set the active menu
|
|
797
|
+
*/
|
|
798
|
+
async setActiveMenu(name) {
|
|
799
|
+
if (!this.menus.has(name)) {
|
|
800
|
+
console.warn(`Menu '${name}' not found`);
|
|
801
|
+
return this;
|
|
802
|
+
}
|
|
803
|
+
const menuConfig = this.menus.get(name);
|
|
804
|
+
if (menuConfig.groupKind) {
|
|
805
|
+
this.lastGroupMenu = menuConfig;
|
|
806
|
+
if (!this.getApp().activeGroup) {
|
|
807
|
+
this.showGroupSearch();
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
this._setActiveMenu(name);
|
|
812
|
+
await this.render();
|
|
813
|
+
this.emit("menu-changed", {
|
|
814
|
+
menuName: name,
|
|
815
|
+
config: menuConfig,
|
|
816
|
+
sidebar: this
|
|
817
|
+
});
|
|
818
|
+
return this;
|
|
819
|
+
}
|
|
820
|
+
getGroupMenu(group) {
|
|
821
|
+
if (!group) {
|
|
822
|
+
console.warn("No group provided");
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
let targetMenu = this.lastGroupMenu;
|
|
826
|
+
let anyGroupMenu = null;
|
|
827
|
+
if (group._.kind) {
|
|
828
|
+
for (const [menuName, menuConfig] of this.menus) {
|
|
829
|
+
if (menuConfig.groupKind === group._.kind) {
|
|
830
|
+
targetMenu = menuConfig;
|
|
831
|
+
break;
|
|
832
|
+
} else if (menuConfig.groupKind === "any") {
|
|
833
|
+
anyGroupMenu = menuConfig;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (!targetMenu) {
|
|
838
|
+
return anyGroupMenu;
|
|
839
|
+
}
|
|
840
|
+
return targetMenu;
|
|
841
|
+
}
|
|
842
|
+
showMenuForGroup(group) {
|
|
843
|
+
if (!group) {
|
|
844
|
+
console.warn("No group provided");
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
let targetMenu = this.getGroupMenu(group);
|
|
848
|
+
if (!targetMenu) {
|
|
849
|
+
console.warn(`No menu found for group kind: ${group.kind}`);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
this._setActiveMenu(targetMenu.name);
|
|
853
|
+
this.render();
|
|
854
|
+
this.emit("menu-changed", {
|
|
855
|
+
menuName: targetMenu.name,
|
|
856
|
+
config: targetMenu,
|
|
857
|
+
sidebar: this
|
|
858
|
+
});
|
|
859
|
+
return this;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Get menu configuration
|
|
863
|
+
*/
|
|
864
|
+
getMenuConfig(name) {
|
|
865
|
+
return this.menus.get(name) || null;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Get current active menu configuration
|
|
869
|
+
*/
|
|
870
|
+
getCurrentMenuConfig() {
|
|
871
|
+
return this.activeMenuName ? this.menus.get(this.activeMenuName) : null;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Update menu configuration
|
|
875
|
+
*/
|
|
876
|
+
updateMenu(name, updates) {
|
|
877
|
+
const menu = this.menus.get(name);
|
|
878
|
+
if (!menu) {
|
|
879
|
+
console.warn(`Menu '${name}' not found`);
|
|
880
|
+
return this;
|
|
881
|
+
}
|
|
882
|
+
Object.assign(menu, updates);
|
|
883
|
+
if (this.activeMenuName === name) {
|
|
884
|
+
this.render();
|
|
885
|
+
}
|
|
886
|
+
return this;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Remove a menu
|
|
890
|
+
*/
|
|
891
|
+
removeMenu(name) {
|
|
892
|
+
this.menus.delete(name);
|
|
893
|
+
if (this.activeMenuName === name) {
|
|
894
|
+
const remainingMenus = Array.from(this.menus.keys());
|
|
895
|
+
this.activeMenuName = remainingMenus.length > 0 ? remainingMenus[0] : null;
|
|
896
|
+
this.render();
|
|
897
|
+
}
|
|
898
|
+
return this;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Get view data for template rendering
|
|
902
|
+
*/
|
|
903
|
+
async onBeforeRender() {
|
|
904
|
+
const currentMenu = this.getCurrentMenuConfig();
|
|
905
|
+
if (!currentMenu) {
|
|
906
|
+
return { currentMenu: null };
|
|
907
|
+
}
|
|
908
|
+
let subData = {
|
|
909
|
+
version: this.getApp().version || null,
|
|
910
|
+
group: this.getApp().activeGroup || null,
|
|
911
|
+
user: this.getApp.activeUser || null
|
|
912
|
+
};
|
|
913
|
+
this.data = {
|
|
914
|
+
currentMenu: {
|
|
915
|
+
header: this.renderTemplateString(currentMenu.header || "", subData),
|
|
916
|
+
footer: this.renderTemplateString(currentMenu.footer || "", subData),
|
|
917
|
+
items: this.processNavItems(currentMenu.items, currentMenu.groupKind),
|
|
918
|
+
data: currentMenu.data,
|
|
919
|
+
showToggle: this.showToggle
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
async onAfterRender() {
|
|
924
|
+
if (this.isCollapsedState()) {
|
|
925
|
+
setTimeout(() => this.initializeTooltips(), 50);
|
|
926
|
+
} else {
|
|
927
|
+
this.destroyTooltips();
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
setCustomView(view) {
|
|
931
|
+
if (this.customView) {
|
|
932
|
+
this.removeChild(this.customView.id);
|
|
933
|
+
}
|
|
934
|
+
this.customView = view;
|
|
935
|
+
if (view) {
|
|
936
|
+
view.containerId = "sidebar-custom-view-container";
|
|
937
|
+
this.addChild(view);
|
|
938
|
+
}
|
|
939
|
+
this.render();
|
|
940
|
+
return this;
|
|
941
|
+
}
|
|
942
|
+
clearCustomView() {
|
|
943
|
+
if (this.customView) {
|
|
944
|
+
this.removeChild(this.customView.id);
|
|
945
|
+
this.customView = null;
|
|
946
|
+
}
|
|
947
|
+
this.render();
|
|
948
|
+
return this;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Process navigation items - add IDs, active states, and proper hrefs
|
|
952
|
+
*/
|
|
953
|
+
processNavItems(items, groupKind) {
|
|
954
|
+
const app = this.getApp();
|
|
955
|
+
const activeUser = app?.activeUser;
|
|
956
|
+
const activeGroup = app?.activeGroup;
|
|
957
|
+
const updateRouteWithGroup = (route) => {
|
|
958
|
+
if (groupKind && activeGroup && activeGroup.id) {
|
|
959
|
+
const separator = route.includes("?") ? "&" : "?";
|
|
960
|
+
return `${route}${separator}group=${activeGroup.id}`;
|
|
961
|
+
}
|
|
962
|
+
return route;
|
|
963
|
+
};
|
|
964
|
+
return items.map((item, index2) => {
|
|
965
|
+
if (item === "" || typeof item === "object" && item.divider) {
|
|
966
|
+
return {
|
|
967
|
+
isDivider: true,
|
|
968
|
+
id: `divider-${index2}`
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
if (typeof item === "object" && item.spacer) {
|
|
972
|
+
return {
|
|
973
|
+
isSpacer: true,
|
|
974
|
+
id: `spacer-${index2}`
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
const processedItem = { ...item };
|
|
978
|
+
if (processedItem.permissions) {
|
|
979
|
+
if (!activeUser || !activeUser.hasPermission(processedItem.permissions)) {
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (!processedItem.id) {
|
|
984
|
+
processedItem.id = `nav-${index2}`;
|
|
985
|
+
}
|
|
986
|
+
if (processedItem.route) {
|
|
987
|
+
processedItem.href = updateRouteWithGroup(processedItem.route);
|
|
988
|
+
} else if (processedItem.page) {
|
|
989
|
+
const baseRoute = processedItem.page.startsWith("/") ? processedItem.page : `/${processedItem.page}`;
|
|
990
|
+
processedItem.href = updateRouteWithGroup(baseRoute);
|
|
991
|
+
processedItem.route = processedItem.href;
|
|
992
|
+
}
|
|
993
|
+
if (processedItem.children) {
|
|
994
|
+
processedItem.children = processedItem.children.map((child) => {
|
|
995
|
+
const processedChild = { ...child };
|
|
996
|
+
if (processedChild.permissions && activeUser) {
|
|
997
|
+
if (!activeUser.hasPermission(processedChild.permissions)) {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (processedChild.route) {
|
|
1002
|
+
processedChild.href = updateRouteWithGroup(processedChild.route);
|
|
1003
|
+
} else if (processedChild.page) {
|
|
1004
|
+
const baseRoute = processedChild.page.startsWith("/") ? processedChild.page : `/${processedChild.page}`;
|
|
1005
|
+
processedChild.href = updateRouteWithGroup(baseRoute);
|
|
1006
|
+
processedChild.route = processedChild.href;
|
|
1007
|
+
}
|
|
1008
|
+
return processedChild;
|
|
1009
|
+
}).filter((child) => child !== null);
|
|
1010
|
+
processedItem.hasChildren = !!(processedItem.children && processedItem.children.length > 0);
|
|
1011
|
+
} else {
|
|
1012
|
+
processedItem.hasChildren = false;
|
|
1013
|
+
}
|
|
1014
|
+
return processedItem;
|
|
1015
|
+
}).filter((item) => item !== null);
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Check if navigation item should be active (similar to TopNav)
|
|
1019
|
+
*/
|
|
1020
|
+
isItemActive(item) {
|
|
1021
|
+
if (!item.route || !this.currentRoute) {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
const normalizeRoute = (route) => {
|
|
1025
|
+
if (!route) return "/";
|
|
1026
|
+
const decoded = decodeURIComponent(route);
|
|
1027
|
+
return decoded.startsWith("/") ? decoded : `/${decoded}`;
|
|
1028
|
+
};
|
|
1029
|
+
const itemRoute = normalizeRoute(item.route);
|
|
1030
|
+
const currentRoute = normalizeRoute(this.currentRoute);
|
|
1031
|
+
if (itemRoute === "/" && currentRoute === "/") {
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
if (itemRoute !== "/" && currentRoute !== "/") {
|
|
1035
|
+
return currentRoute.startsWith(itemRoute) || currentRoute === itemRoute;
|
|
1036
|
+
}
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Update active item based on current route (like TopNav)
|
|
1041
|
+
*/
|
|
1042
|
+
async updateActiveItem(route) {
|
|
1043
|
+
this.currentRoute = route;
|
|
1044
|
+
this.clearAllActiveStates();
|
|
1045
|
+
this.setActiveItemByRoute(route);
|
|
1046
|
+
await this.render();
|
|
1047
|
+
return this;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Action handler: Toggle submenu
|
|
1051
|
+
*/
|
|
1052
|
+
async handleActionToggleSubmenu(event, element) {
|
|
1053
|
+
const arrow = element.querySelector(".nav-arrow");
|
|
1054
|
+
if (arrow) {
|
|
1055
|
+
arrow.classList.toggle("rotated");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Action handler: Toggle sidebar collapsed/expanded state
|
|
1060
|
+
*/
|
|
1061
|
+
async handleActionToggleSidebar(event, element) {
|
|
1062
|
+
this.toggleSidebar();
|
|
1063
|
+
}
|
|
1064
|
+
onActionShowGroupMenu(action, event, el) {
|
|
1065
|
+
this.setActiveMenu("group_default");
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
async onActionDefault(action, event, el) {
|
|
1069
|
+
const config = this.getCurrentMenuConfig();
|
|
1070
|
+
if (!config) return;
|
|
1071
|
+
for (const item of config.items) {
|
|
1072
|
+
if (item.action == action && item.handler) {
|
|
1073
|
+
item.handler(action, event, el);
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Get all menu names
|
|
1081
|
+
*/
|
|
1082
|
+
getMenuNames() {
|
|
1083
|
+
return Array.from(this.menus.keys());
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Check if menu exists
|
|
1087
|
+
*/
|
|
1088
|
+
hasMenu(name) {
|
|
1089
|
+
return this.menus.has(name);
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Clear all menus
|
|
1093
|
+
*/
|
|
1094
|
+
clearMenus() {
|
|
1095
|
+
this.menus.clear();
|
|
1096
|
+
this.activeMenuName = null;
|
|
1097
|
+
this.render();
|
|
1098
|
+
return this;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Set data for current menu
|
|
1102
|
+
*/
|
|
1103
|
+
setMenuData(data) {
|
|
1104
|
+
const currentMenu = this.getCurrentMenuConfig();
|
|
1105
|
+
if (currentMenu) {
|
|
1106
|
+
currentMenu.data = { ...currentMenu.data, ...data };
|
|
1107
|
+
this.render();
|
|
1108
|
+
}
|
|
1109
|
+
return this;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Get data for current menu
|
|
1113
|
+
*/
|
|
1114
|
+
getMenuData() {
|
|
1115
|
+
const currentMenu = this.getCurrentMenuConfig();
|
|
1116
|
+
return currentMenu ? currentMenu.data : {};
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Setup listeners for route change events (like TopNav)
|
|
1120
|
+
*/
|
|
1121
|
+
setupRouteListeners() {
|
|
1122
|
+
const app = this.getApp();
|
|
1123
|
+
if (app && app.events) {
|
|
1124
|
+
app.events.on(["page:show", "page:hide", "page:denied"], (data) => {
|
|
1125
|
+
this.onRouteChanged(data);
|
|
1126
|
+
});
|
|
1127
|
+
app.events.on("group:changed", (data) => {
|
|
1128
|
+
this.showMenuForGroup(data.group);
|
|
1129
|
+
});
|
|
1130
|
+
app.events.on("portal:user-changed", (data) => {
|
|
1131
|
+
this.render();
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Handle route changed event - auto-switch menu and update active item
|
|
1137
|
+
*/
|
|
1138
|
+
onRouteChanged(data) {
|
|
1139
|
+
if (data.page && data.page.route) {
|
|
1140
|
+
const route = data.page.route;
|
|
1141
|
+
if (this.activeMenuItem && this.routesMatch(route, this.activeMenuItem.route)) {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const switchedMenu = this.autoSwitchToMenuForRoute(route);
|
|
1145
|
+
if (!switchedMenu) {
|
|
1146
|
+
this.clearAllActiveStates();
|
|
1147
|
+
this.setActiveItemByRoute(route);
|
|
1148
|
+
this.updateActiveItem(route);
|
|
1149
|
+
}
|
|
1150
|
+
if (switchedMenu) {
|
|
1151
|
+
console.log(`Route changed to '${route}', auto-switched menu`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Toggle sidebar between collapsed and expanded states
|
|
1157
|
+
*/
|
|
1158
|
+
toggleSidebar() {
|
|
1159
|
+
const portalContainer = document.querySelector(".portal-container");
|
|
1160
|
+
if (!portalContainer) return;
|
|
1161
|
+
this.hideAllTooltips();
|
|
1162
|
+
const isCurrentlyCollapsed = portalContainer.classList.contains("collapse-sidebar");
|
|
1163
|
+
const isCurrentlyHidden = portalContainer.classList.contains("hide-sidebar");
|
|
1164
|
+
if (isCurrentlyHidden) {
|
|
1165
|
+
portalContainer.classList.remove("hide-sidebar");
|
|
1166
|
+
this.isCollapsed = false;
|
|
1167
|
+
this.destroyTooltips();
|
|
1168
|
+
} else if (isCurrentlyCollapsed) {
|
|
1169
|
+
portalContainer.classList.remove("collapse-sidebar");
|
|
1170
|
+
this.isCollapsed = false;
|
|
1171
|
+
this.destroyTooltips();
|
|
1172
|
+
} else {
|
|
1173
|
+
portalContainer.classList.add("collapse-sidebar");
|
|
1174
|
+
this.isCollapsed = true;
|
|
1175
|
+
setTimeout(() => this.initializeTooltips(), 150);
|
|
1176
|
+
}
|
|
1177
|
+
return this;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Set sidebar state programmatically
|
|
1181
|
+
*/
|
|
1182
|
+
setSidebarState(state) {
|
|
1183
|
+
const portalContainer = document.querySelector(".portal-container");
|
|
1184
|
+
if (!portalContainer) return this;
|
|
1185
|
+
portalContainer.classList.remove("collapse-sidebar", "hide-sidebar");
|
|
1186
|
+
switch (state) {
|
|
1187
|
+
case "collapsed":
|
|
1188
|
+
portalContainer.classList.add("collapse-sidebar");
|
|
1189
|
+
this.isCollapsed = true;
|
|
1190
|
+
break;
|
|
1191
|
+
case "hidden":
|
|
1192
|
+
portalContainer.classList.add("hide-sidebar");
|
|
1193
|
+
this.isCollapsed = false;
|
|
1194
|
+
break;
|
|
1195
|
+
case "normal":
|
|
1196
|
+
default:
|
|
1197
|
+
this.isCollapsed = false;
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
if (this.isCollapsed) {
|
|
1201
|
+
this.hideAllTooltips();
|
|
1202
|
+
setTimeout(() => this.initializeTooltips(), 100);
|
|
1203
|
+
} else {
|
|
1204
|
+
this.destroyTooltips();
|
|
1205
|
+
}
|
|
1206
|
+
return this;
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Initialize tooltips for nav items when sidebar is collapsed
|
|
1210
|
+
*/
|
|
1211
|
+
initializeTooltips() {
|
|
1212
|
+
this.destroyTooltips();
|
|
1213
|
+
if (!this.isCollapsedState()) {
|
|
1214
|
+
return this;
|
|
1215
|
+
}
|
|
1216
|
+
const navLinks = this.element.querySelectorAll(".sidebar-nav .nav-link");
|
|
1217
|
+
navLinks.forEach((link) => {
|
|
1218
|
+
const navText = link.querySelector(".nav-text");
|
|
1219
|
+
if (navText && navText.textContent.trim()) {
|
|
1220
|
+
const tooltipText = navText.textContent.trim();
|
|
1221
|
+
link.setAttribute("data-bs-toggle", "tooltip");
|
|
1222
|
+
link.setAttribute("data-bs-placement", "right");
|
|
1223
|
+
link.setAttribute("data-bs-title", tooltipText);
|
|
1224
|
+
link.setAttribute("data-bs-container", "body");
|
|
1225
|
+
if (window.bootstrap && window.bootstrap.Tooltip) {
|
|
1226
|
+
const tooltip = new window.bootstrap.Tooltip(link, {
|
|
1227
|
+
placement: "right",
|
|
1228
|
+
container: "body",
|
|
1229
|
+
trigger: "hover",
|
|
1230
|
+
delay: { show: 500, hide: 100 },
|
|
1231
|
+
fallbackPlacements: ["top", "bottom", "left"]
|
|
1232
|
+
});
|
|
1233
|
+
link._tooltipInstance = tooltip;
|
|
1234
|
+
link.addEventListener("click", () => {
|
|
1235
|
+
tooltip.hide();
|
|
1236
|
+
});
|
|
1237
|
+
link.addEventListener("blur", () => {
|
|
1238
|
+
tooltip.hide();
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
this.addTooltipHideListeners();
|
|
1244
|
+
return this;
|
|
1245
|
+
}
|
|
1246
|
+
destroyTooltips() {
|
|
1247
|
+
this.removeTooltipHideListeners();
|
|
1248
|
+
const navLinks = this.element.querySelectorAll('.sidebar-nav .nav-link[data-bs-toggle="tooltip"]');
|
|
1249
|
+
navLinks.forEach((link) => {
|
|
1250
|
+
const tooltipInstance = link._tooltipInstance || window.bootstrap?.Tooltip?.getInstance(link);
|
|
1251
|
+
if (tooltipInstance) {
|
|
1252
|
+
tooltipInstance.hide();
|
|
1253
|
+
tooltipInstance.dispose();
|
|
1254
|
+
}
|
|
1255
|
+
delete link._tooltipInstance;
|
|
1256
|
+
link.removeAttribute("data-bs-toggle");
|
|
1257
|
+
link.removeAttribute("data-bs-placement");
|
|
1258
|
+
link.removeAttribute("data-bs-title");
|
|
1259
|
+
link.removeAttribute("data-bs-container");
|
|
1260
|
+
});
|
|
1261
|
+
return this;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Get current sidebar state
|
|
1265
|
+
*/
|
|
1266
|
+
getSidebarState() {
|
|
1267
|
+
const portalContainer = document.querySelector(".portal-container");
|
|
1268
|
+
if (!portalContainer) return "normal";
|
|
1269
|
+
if (portalContainer.classList.contains("hide-sidebar")) {
|
|
1270
|
+
return "hidden";
|
|
1271
|
+
} else if (portalContainer.classList.contains("collapse-sidebar")) {
|
|
1272
|
+
return "collapsed";
|
|
1273
|
+
} else {
|
|
1274
|
+
return "normal";
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Check if sidebar is collapsed
|
|
1279
|
+
*/
|
|
1280
|
+
isCollapsedState() {
|
|
1281
|
+
return this.getSidebarState() === "collapsed";
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Enable/disable toggle button
|
|
1285
|
+
*/
|
|
1286
|
+
setToggleEnabled(enabled) {
|
|
1287
|
+
this.showToggle = enabled;
|
|
1288
|
+
this.render();
|
|
1289
|
+
return this;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Initialize menus from options
|
|
1293
|
+
*/
|
|
1294
|
+
initializeMenus(options) {
|
|
1295
|
+
if (options.menus) {
|
|
1296
|
+
for (const menu of options.menus) {
|
|
1297
|
+
this.addMenu(menu.name, menu);
|
|
1298
|
+
}
|
|
1299
|
+
} else if (options.menu) {
|
|
1300
|
+
options.menu.name = options.menu.name || "default";
|
|
1301
|
+
this.addMenu(options.menu.name, options.menu);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Add global listeners to hide tooltips when needed
|
|
1306
|
+
*/
|
|
1307
|
+
addTooltipHideListeners() {
|
|
1308
|
+
this._tooltipScrollHandler = () => this.hideAllTooltips();
|
|
1309
|
+
this.element.addEventListener("scroll", this._tooltipScrollHandler, { passive: true });
|
|
1310
|
+
this._tooltipRouteHandler = () => this.hideAllTooltips();
|
|
1311
|
+
this.getApp();
|
|
1312
|
+
this._tooltipBlurHandler = () => this.hideAllTooltips();
|
|
1313
|
+
window.addEventListener("blur", this._tooltipBlurHandler);
|
|
1314
|
+
this._tooltipEscapeHandler = (e4) => {
|
|
1315
|
+
if (e4.key === "Escape") {
|
|
1316
|
+
this.hideAllTooltips();
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
document.addEventListener("keydown", this._tooltipEscapeHandler);
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Remove global tooltip hide listeners
|
|
1323
|
+
*/
|
|
1324
|
+
removeTooltipHideListeners() {
|
|
1325
|
+
if (this._tooltipScrollHandler) {
|
|
1326
|
+
this.element.removeEventListener("scroll", this._tooltipScrollHandler);
|
|
1327
|
+
delete this._tooltipScrollHandler;
|
|
1328
|
+
}
|
|
1329
|
+
if (this._tooltipBlurHandler) {
|
|
1330
|
+
window.removeEventListener("blur", this._tooltipBlurHandler);
|
|
1331
|
+
delete this._tooltipBlurHandler;
|
|
1332
|
+
}
|
|
1333
|
+
if (this._tooltipEscapeHandler) {
|
|
1334
|
+
document.removeEventListener("keydown", this._tooltipEscapeHandler);
|
|
1335
|
+
delete this._tooltipEscapeHandler;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Force hide all visible tooltips
|
|
1340
|
+
*/
|
|
1341
|
+
hideAllTooltips() {
|
|
1342
|
+
const navLinks = this.element.querySelectorAll('.sidebar-nav .nav-link[data-bs-toggle="tooltip"]');
|
|
1343
|
+
navLinks.forEach((link) => {
|
|
1344
|
+
const tooltip = link._tooltipInstance || window.bootstrap?.Tooltip?.getInstance(link);
|
|
1345
|
+
if (tooltip) {
|
|
1346
|
+
tooltip.hide();
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
const visibleTooltips = document.querySelectorAll(".tooltip.show");
|
|
1350
|
+
visibleTooltips.forEach((tooltip) => {
|
|
1351
|
+
tooltip.remove();
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Cleanup on destroy
|
|
1356
|
+
*/
|
|
1357
|
+
async onBeforeDestroy() {
|
|
1358
|
+
this.destroyTooltips();
|
|
1359
|
+
await super.onBeforeDestroy();
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Setup responsive behavior for mobile
|
|
1363
|
+
*/
|
|
1364
|
+
setupResponsiveBehavior() {
|
|
1365
|
+
const checkMobile = () => {
|
|
1366
|
+
const isMobile = window.innerWidth <= 768;
|
|
1367
|
+
const portalContainer = document.querySelector(".portal-container");
|
|
1368
|
+
if (portalContainer) {
|
|
1369
|
+
if (isMobile) {
|
|
1370
|
+
portalContainer.classList.add("sidebar-mobile");
|
|
1371
|
+
} else {
|
|
1372
|
+
portalContainer.classList.remove("sidebar-mobile", "sidebar-open");
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
checkMobile();
|
|
1377
|
+
window.addEventListener("resize", checkMobile);
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Static method to create a sidebar with common configuration
|
|
1381
|
+
*/
|
|
1382
|
+
static createDefault(options = {}) {
|
|
1383
|
+
return new Sidebar({
|
|
1384
|
+
theme: "sidebar-clean",
|
|
1385
|
+
showToggle: true,
|
|
1386
|
+
autoCollapseMobile: true,
|
|
1387
|
+
...options
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Static method to create a minimal sidebar
|
|
1392
|
+
*/
|
|
1393
|
+
static createMinimal(options = {}) {
|
|
1394
|
+
return new Sidebar({
|
|
1395
|
+
theme: "sidebar-clean",
|
|
1396
|
+
showToggle: false,
|
|
1397
|
+
autoCollapseMobile: false,
|
|
1398
|
+
...options
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Set sidebar theme
|
|
1403
|
+
*/
|
|
1404
|
+
setSidebarTheme(theme) {
|
|
1405
|
+
this.removeClass("sidebar-light sidebar-dark sidebar-clean");
|
|
1406
|
+
this.sidebarTheme = theme;
|
|
1407
|
+
this.addClass(theme);
|
|
1408
|
+
return this;
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Quick method to show/hide the sidebar
|
|
1412
|
+
*/
|
|
1413
|
+
show() {
|
|
1414
|
+
return this.setSidebarState("normal");
|
|
1415
|
+
}
|
|
1416
|
+
hide() {
|
|
1417
|
+
return this.setSidebarState("hidden");
|
|
1418
|
+
}
|
|
1419
|
+
collapse() {
|
|
1420
|
+
return this.setSidebarState("collapsed");
|
|
1421
|
+
}
|
|
1422
|
+
expand() {
|
|
1423
|
+
return this.setSidebarState("normal");
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Add pulse effect to toggle button
|
|
1427
|
+
*/
|
|
1428
|
+
pulseToggle() {
|
|
1429
|
+
const toggleButton = this.element.querySelector(".sidebar-toggle");
|
|
1430
|
+
if (toggleButton) {
|
|
1431
|
+
toggleButton.classList.add("pulse");
|
|
1432
|
+
const removePulse = () => {
|
|
1433
|
+
toggleButton.classList.remove("pulse");
|
|
1434
|
+
toggleButton.removeEventListener("click", removePulse);
|
|
1435
|
+
};
|
|
1436
|
+
toggleButton.addEventListener("click", removePulse, { once: true });
|
|
1437
|
+
setTimeout(removePulse, 3e3);
|
|
1438
|
+
}
|
|
1439
|
+
return this;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Utility method to quickly add a simple menu item
|
|
1443
|
+
*/
|
|
1444
|
+
addSimpleMenuItem(menuName, text, route, icon = "bi-circle") {
|
|
1445
|
+
const menu = this.menus.get(menuName);
|
|
1446
|
+
if (menu) {
|
|
1447
|
+
menu.items = menu.items || [];
|
|
1448
|
+
menu.items.push({
|
|
1449
|
+
text,
|
|
1450
|
+
route,
|
|
1451
|
+
icon
|
|
1452
|
+
});
|
|
1453
|
+
if (this.activeMenuName === menuName) {
|
|
1454
|
+
this.render();
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return this;
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Utility method to quickly create and set a simple menu
|
|
1461
|
+
*/
|
|
1462
|
+
setSimpleMenu(name, header, items) {
|
|
1463
|
+
const menu = {
|
|
1464
|
+
name,
|
|
1465
|
+
header,
|
|
1466
|
+
items
|
|
1467
|
+
};
|
|
1468
|
+
this.addMenu(name, menu);
|
|
1469
|
+
this.setActiveMenu(name);
|
|
1470
|
+
return this;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
class DeniedPage extends Page {
|
|
1474
|
+
constructor(options = {}) {
|
|
1475
|
+
super({
|
|
1476
|
+
pageName: "Access Denied",
|
|
1477
|
+
route: "/denied",
|
|
1478
|
+
title: "Access Denied",
|
|
1479
|
+
pageIcon: "bi bi-shield-x",
|
|
1480
|
+
template: `
|
|
1481
|
+
<div class="container mt-5">
|
|
1482
|
+
<div class="row justify-content-center">
|
|
1483
|
+
<div class="col-md-8 col-lg-6">
|
|
1484
|
+
<div class="text-center mb-4">
|
|
1485
|
+
<i class="bi bi-shield-x text-muted" style="font-size: 3rem;"></i>
|
|
1486
|
+
<h2 class="mt-3 mb-2">Access Denied</h2>
|
|
1487
|
+
<p class="text-muted">You don't have permission to access this page.</p>
|
|
1488
|
+
</div>
|
|
1489
|
+
|
|
1490
|
+
{{#deniedPage}}
|
|
1491
|
+
<div class="card border-0 shadow-sm mb-4">
|
|
1492
|
+
<div class="card-body">
|
|
1493
|
+
<h6 class="card-subtitle mb-2 text-muted">Requested Page</h6>
|
|
1494
|
+
<h5 class="card-title">
|
|
1495
|
+
<i class="{{pageIcon}} me-2"></i>
|
|
1496
|
+
{{displayName}}
|
|
1497
|
+
</h5>
|
|
1498
|
+
{{#route}}
|
|
1499
|
+
<p class="card-text text-muted small">{{route}}</p>
|
|
1500
|
+
{{/route}}
|
|
1501
|
+
{{#description}}
|
|
1502
|
+
<p class="card-text">{{description}}</p>
|
|
1503
|
+
{{/description}}
|
|
1504
|
+
|
|
1505
|
+
{{#requiredPermissions}}
|
|
1506
|
+
<div class="mt-3">
|
|
1507
|
+
<h6 class="mb-2">Required Permissions:</h6>
|
|
1508
|
+
{{#permissions}}
|
|
1509
|
+
<span class="badge bg-light text-dark me-1 mb-1">{{.}}</span>
|
|
1510
|
+
{{/permissions}}
|
|
1511
|
+
{{^permissions}}
|
|
1512
|
+
<span class="text-muted small">Authentication required</span>
|
|
1513
|
+
{{/permissions}}
|
|
1514
|
+
</div>
|
|
1515
|
+
{{/requiredPermissions}}
|
|
1516
|
+
</div>
|
|
1517
|
+
</div>
|
|
1518
|
+
{{/deniedPage}}
|
|
1519
|
+
|
|
1520
|
+
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
|
1521
|
+
<button type="button" class="btn btn-primary" data-action="go-back">
|
|
1522
|
+
<i class="bi bi-arrow-left me-1"></i>
|
|
1523
|
+
Go Back
|
|
1524
|
+
</button>
|
|
1525
|
+
<button type="button" class="btn btn-outline-secondary" data-action="go-home">
|
|
1526
|
+
<i class="bi bi-house me-1"></i>
|
|
1527
|
+
Home
|
|
1528
|
+
</button>
|
|
1529
|
+
{{#showLogin}}
|
|
1530
|
+
<button type="button" class="btn btn-outline-primary" data-action="login">
|
|
1531
|
+
<i class="bi bi-box-arrow-in-right me-1"></i>
|
|
1532
|
+
Login
|
|
1533
|
+
</button>
|
|
1534
|
+
{{/showLogin}}
|
|
1535
|
+
</div>
|
|
1536
|
+
|
|
1537
|
+
{{#currentUser}}
|
|
1538
|
+
<div class="text-center mt-4">
|
|
1539
|
+
<small class="text-muted">
|
|
1540
|
+
Logged in as <strong>{{username}}</strong>
|
|
1541
|
+
</small>
|
|
1542
|
+
</div>
|
|
1543
|
+
{{/currentUser}}
|
|
1544
|
+
</div>
|
|
1545
|
+
</div>
|
|
1546
|
+
</div>
|
|
1547
|
+
`,
|
|
1548
|
+
...options
|
|
1549
|
+
});
|
|
1550
|
+
this.deniedPage = null;
|
|
1551
|
+
this.deniedPageOptions = null;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Handle route parameters - expect denied page info
|
|
1555
|
+
*/
|
|
1556
|
+
async onParams(params = {}, query = {}) {
|
|
1557
|
+
await super.onParams(params, query);
|
|
1558
|
+
if (params.page) {
|
|
1559
|
+
this.deniedPage = params.page;
|
|
1560
|
+
this.deniedPageOptions = params.page.options || params.page.pageOptions || {};
|
|
1561
|
+
} else if (query.page) {
|
|
1562
|
+
this.deniedPageName = query.page;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Set the denied page instance
|
|
1567
|
+
*/
|
|
1568
|
+
setDeniedPage(pageInstance) {
|
|
1569
|
+
this.deniedPage = pageInstance;
|
|
1570
|
+
this.deniedPageOptions = pageInstance?.options || pageInstance?.pageOptions || {};
|
|
1571
|
+
return this;
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Get view data for template rendering
|
|
1575
|
+
*/
|
|
1576
|
+
async getViewData() {
|
|
1577
|
+
const app = this.getApp();
|
|
1578
|
+
const currentUser = app?.activeUser || app?.getCurrentUser?.() || null;
|
|
1579
|
+
let deniedPageInfo = null;
|
|
1580
|
+
if (this.deniedPage) {
|
|
1581
|
+
const permissions = this.deniedPageOptions?.permissions || this.deniedPage.options?.permissions || this.deniedPage.pageOptions?.permissions;
|
|
1582
|
+
deniedPageInfo = {
|
|
1583
|
+
displayName: this.deniedPage.displayName || this.deniedPage.pageName || this.deniedPage.title || "Unknown Page",
|
|
1584
|
+
pageName: this.deniedPage.pageName,
|
|
1585
|
+
route: this.deniedPage.route,
|
|
1586
|
+
description: this.deniedPage.pageDescription || this.deniedPage.description,
|
|
1587
|
+
pageIcon: this.deniedPage.pageIcon || "bi bi-file-text",
|
|
1588
|
+
requiredPermissions: permissions ? {
|
|
1589
|
+
permissions: Array.isArray(permissions) ? permissions : [permissions]
|
|
1590
|
+
} : null
|
|
1591
|
+
};
|
|
1592
|
+
} else if (this.deniedPageName) {
|
|
1593
|
+
deniedPageInfo = {
|
|
1594
|
+
displayName: this.deniedPageName,
|
|
1595
|
+
pageName: this.deniedPageName,
|
|
1596
|
+
pageIcon: "bi bi-file-text"
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
return {
|
|
1600
|
+
deniedPage: deniedPageInfo,
|
|
1601
|
+
currentUser: currentUser ? {
|
|
1602
|
+
username: currentUser.username || currentUser.name || currentUser.email || "Unknown User",
|
|
1603
|
+
name: currentUser.name,
|
|
1604
|
+
email: currentUser.email
|
|
1605
|
+
} : null,
|
|
1606
|
+
showLogin: !currentUser
|
|
1607
|
+
// Show login button if not authenticated
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Handle going back to previous page
|
|
1612
|
+
*/
|
|
1613
|
+
async handleActionGoBack(event, element) {
|
|
1614
|
+
event.preventDefault();
|
|
1615
|
+
if (window.history.length > 1) {
|
|
1616
|
+
window.history.back();
|
|
1617
|
+
} else {
|
|
1618
|
+
await this.handleActionGoHome(event, element);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Handle navigation to home page
|
|
1623
|
+
*/
|
|
1624
|
+
async handleActionGoHome(event, element) {
|
|
1625
|
+
event.preventDefault();
|
|
1626
|
+
const app = this.getApp();
|
|
1627
|
+
if (app) {
|
|
1628
|
+
await app.navigateToDefault();
|
|
1629
|
+
} else {
|
|
1630
|
+
window.location.href = "/";
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Handle login action
|
|
1635
|
+
*/
|
|
1636
|
+
async handleActionLogin(event, element) {
|
|
1637
|
+
event.preventDefault();
|
|
1638
|
+
const app = this.getApp();
|
|
1639
|
+
if (app) {
|
|
1640
|
+
try {
|
|
1641
|
+
await app.showPage("login");
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
try {
|
|
1644
|
+
await app.navigate("/login");
|
|
1645
|
+
} catch (navError) {
|
|
1646
|
+
this.emit("login-required", {
|
|
1647
|
+
returnUrl: this.deniedPage?.route || window.location.pathname
|
|
1648
|
+
});
|
|
1649
|
+
setTimeout(() => {
|
|
1650
|
+
app?.showInfo?.("Please contact your administrator for access.");
|
|
1651
|
+
}, 100);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Called when entering this page
|
|
1658
|
+
*/
|
|
1659
|
+
async onEnter() {
|
|
1660
|
+
await super.onEnter();
|
|
1661
|
+
const pageName = this.deniedPage?.pageName || this.deniedPageName;
|
|
1662
|
+
if (pageName) {
|
|
1663
|
+
this.setMeta({
|
|
1664
|
+
title: `Access Denied - ${pageName}`
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
console.warn("Access denied to page:", {
|
|
1668
|
+
page: this.deniedPage?.pageName || this.deniedPageName,
|
|
1669
|
+
route: this.deniedPage?.route,
|
|
1670
|
+
permissions: this.deniedPageOptions?.permissions,
|
|
1671
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Static helper to show access denied for a specific page
|
|
1676
|
+
*/
|
|
1677
|
+
static showForPage(app, pageInstance) {
|
|
1678
|
+
const deniedPage = new DeniedPage();
|
|
1679
|
+
deniedPage.setDeniedPage(pageInstance);
|
|
1680
|
+
return app.showPage(deniedPage);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
class NotFoundPage extends Page {
|
|
1684
|
+
constructor(options = {}) {
|
|
1685
|
+
super({
|
|
1686
|
+
pageName: "404",
|
|
1687
|
+
route: "/404",
|
|
1688
|
+
title: "404 - Page Not Found",
|
|
1689
|
+
pageIcon: "bi bi-search",
|
|
1690
|
+
template: `
|
|
1691
|
+
<div class="container mt-5">
|
|
1692
|
+
<div class="row justify-content-center">
|
|
1693
|
+
<div class="col-md-8 col-lg-6">
|
|
1694
|
+
<div class="text-center mb-4">
|
|
1695
|
+
<i class="bi bi-search text-muted" style="font-size: 3rem;"></i>
|
|
1696
|
+
<h2 class="mt-3 mb-2">Page Not Found</h2>
|
|
1697
|
+
<p class="text-muted">The page you're looking for doesn't exist.</p>
|
|
1698
|
+
</div>
|
|
1699
|
+
|
|
1700
|
+
{{#path}}
|
|
1701
|
+
<div class="card border-0 shadow-sm mb-4">
|
|
1702
|
+
<div class="card-body text-center">
|
|
1703
|
+
<h6 class="card-subtitle mb-2 text-muted">Requested Path</h6>
|
|
1704
|
+
<code class="text-primary">{{path}}</code>
|
|
1705
|
+
</div>
|
|
1706
|
+
</div>
|
|
1707
|
+
{{/path}}
|
|
1708
|
+
|
|
1709
|
+
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
|
1710
|
+
<button type="button" class="btn btn-primary" data-action="go-back">
|
|
1711
|
+
<i class="bi bi-arrow-left me-1"></i>
|
|
1712
|
+
Go Back
|
|
1713
|
+
</button>
|
|
1714
|
+
<button type="button" class="btn btn-outline-secondary" data-action="go-home">
|
|
1715
|
+
<i class="bi bi-house me-1"></i>
|
|
1716
|
+
Home
|
|
1717
|
+
</button>
|
|
1718
|
+
</div>
|
|
1719
|
+
</div>
|
|
1720
|
+
</div>
|
|
1721
|
+
</div>
|
|
1722
|
+
`,
|
|
1723
|
+
...options
|
|
1724
|
+
});
|
|
1725
|
+
this.path = null;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Handle route parameters
|
|
1729
|
+
*/
|
|
1730
|
+
async onParams(params = {}, query = {}) {
|
|
1731
|
+
await super.onParams(params, query);
|
|
1732
|
+
if (params.path) {
|
|
1733
|
+
this.path = params.path;
|
|
1734
|
+
}
|
|
1735
|
+
if (query.path) {
|
|
1736
|
+
this.path = query.path;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Set not found path
|
|
1741
|
+
*/
|
|
1742
|
+
setInfo(path) {
|
|
1743
|
+
this.path = path || null;
|
|
1744
|
+
return this;
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Handle going back to previous page
|
|
1748
|
+
*/
|
|
1749
|
+
async handleActionGoBack(event, _element) {
|
|
1750
|
+
event.preventDefault();
|
|
1751
|
+
if (window.history.length > 1) {
|
|
1752
|
+
window.history.back();
|
|
1753
|
+
} else {
|
|
1754
|
+
await this.handleActionGoHome(event, _element);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Handle navigation to home page
|
|
1759
|
+
*/
|
|
1760
|
+
async handleActionGoHome(event, _element) {
|
|
1761
|
+
event.preventDefault();
|
|
1762
|
+
const app = this.getApp();
|
|
1763
|
+
if (app) {
|
|
1764
|
+
await app.navigateToDefault();
|
|
1765
|
+
} else {
|
|
1766
|
+
window.location.href = "/";
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Called when entering this page
|
|
1771
|
+
*/
|
|
1772
|
+
async onEnter() {
|
|
1773
|
+
await super.onEnter();
|
|
1774
|
+
if (this.path) {
|
|
1775
|
+
this.setMeta({
|
|
1776
|
+
title: `404 - ${this.path} Not Found`
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
console.warn("404 Not Found:", {
|
|
1780
|
+
path: this.path,
|
|
1781
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Static helper to show 404 for a specific path
|
|
1786
|
+
*/
|
|
1787
|
+
static showForPath(app, path) {
|
|
1788
|
+
const notFoundPage = new NotFoundPage();
|
|
1789
|
+
notFoundPage.setInfo(path);
|
|
1790
|
+
return notFoundPage.render();
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
class PortalApp extends WebApp {
|
|
1794
|
+
constructor(config = {}) {
|
|
1795
|
+
super(config);
|
|
1796
|
+
this.sidebarConfig = {};
|
|
1797
|
+
if (config.sidebar && config.sidebar.menus) {
|
|
1798
|
+
this.sidebarConfig.menus = config.sidebar.menus;
|
|
1799
|
+
} else if (config.sidebar.menu) {
|
|
1800
|
+
this.sidebarConfig.menu = config.sidebar.menu;
|
|
1801
|
+
} else if (config.sidebar.items) {
|
|
1802
|
+
this.sidebarConfig.menu = config.sidebar;
|
|
1803
|
+
}
|
|
1804
|
+
this.topbarConfig = config.topbar || {};
|
|
1805
|
+
if (config.topnav && !config.topbar) {
|
|
1806
|
+
this.topbarConfig = config.topnav;
|
|
1807
|
+
}
|
|
1808
|
+
this.sidebar = null;
|
|
1809
|
+
this.topbar = null;
|
|
1810
|
+
this.topnav = null;
|
|
1811
|
+
this.tokenManager = new TokenManager();
|
|
1812
|
+
this.activeGroup = null;
|
|
1813
|
+
if (!this.isMobile()) {
|
|
1814
|
+
this.sidebarCollapsed = this.loadSidebarState() ?? (this.sidebarConfig.defaultCollapsed || false);
|
|
1815
|
+
} else {
|
|
1816
|
+
this.sidebarCollapsed = this.sidebarConfig.defaultCollapsed || false;
|
|
1817
|
+
}
|
|
1818
|
+
this.setupPageContainer();
|
|
1819
|
+
this.toast = new ToastService();
|
|
1820
|
+
this.registerPage("denied", DeniedPage);
|
|
1821
|
+
this.registerPage("404", NotFoundPage);
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Override WebApp start to setup portal layout
|
|
1825
|
+
*/
|
|
1826
|
+
async start() {
|
|
1827
|
+
await this.checkAuthStatus();
|
|
1828
|
+
this.events.on("auth:unauthorized", () => {
|
|
1829
|
+
this.showError("You have been logged out");
|
|
1830
|
+
this.setActiveUser(null);
|
|
1831
|
+
return;
|
|
1832
|
+
});
|
|
1833
|
+
this.events.on("auth:logout", () => {
|
|
1834
|
+
this.showError("You have been logged out");
|
|
1835
|
+
this.tokenManager.clearTokens();
|
|
1836
|
+
this.setActiveUser(null);
|
|
1837
|
+
return;
|
|
1838
|
+
});
|
|
1839
|
+
this.events.on("portal:action", this.onPortalAction.bind(this));
|
|
1840
|
+
await this.checkActiveGroup();
|
|
1841
|
+
console.log("Setting up router...");
|
|
1842
|
+
await this.setupRouter();
|
|
1843
|
+
this.isStarted = true;
|
|
1844
|
+
this.events.emit("app:ready", { app: this });
|
|
1845
|
+
console.log(`${this.title} portal ready`);
|
|
1846
|
+
}
|
|
1847
|
+
async checkAuthStatus() {
|
|
1848
|
+
const token = this.tokenManager.getTokenInstance();
|
|
1849
|
+
if (!token || !token.isValid()) {
|
|
1850
|
+
this.events.emit("auth:unauthorized", { app: this });
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
if (token.isExpired()) {
|
|
1854
|
+
this.events.emit("auth:expired", { app: this });
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
if (token.isExpiringSoon()) {
|
|
1858
|
+
this.events.emit("auth:expiring", { app: this });
|
|
1859
|
+
}
|
|
1860
|
+
this.tokenManager.startAutoRefresh(this);
|
|
1861
|
+
this.rest.setAuthToken(token.token);
|
|
1862
|
+
const user = new User({ id: token.getUserId() });
|
|
1863
|
+
await user.fetch();
|
|
1864
|
+
this.setActiveUser(user);
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Check and load active group from storage
|
|
1868
|
+
*/
|
|
1869
|
+
async checkActiveGroup() {
|
|
1870
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
1871
|
+
const urlGroupId = urlParams.get("group");
|
|
1872
|
+
const groupId = urlGroupId || this.loadActiveGroupId();
|
|
1873
|
+
if (groupId) {
|
|
1874
|
+
try {
|
|
1875
|
+
const group = new Group({ id: groupId });
|
|
1876
|
+
const resp = await group.fetch();
|
|
1877
|
+
if (!resp.success || !resp.data.status) {
|
|
1878
|
+
this.clearActiveGroup();
|
|
1879
|
+
console.warn("Failed to load active group:", resp.statusText);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
this.activeGroup = group;
|
|
1883
|
+
if (urlGroupId) {
|
|
1884
|
+
this.saveActiveGroupId(groupId);
|
|
1885
|
+
}
|
|
1886
|
+
console.log("Loaded active group:", group.get("name"));
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
console.warn("Failed to load active group:", error);
|
|
1889
|
+
if (urlGroupId && !this.loadActiveGroupId()) {
|
|
1890
|
+
this.clearActiveGroupId();
|
|
1891
|
+
} else if (urlGroupId) {
|
|
1892
|
+
const storedGroupId = this.loadActiveGroupId();
|
|
1893
|
+
if (storedGroupId && storedGroupId !== urlGroupId) {
|
|
1894
|
+
try {
|
|
1895
|
+
const fallbackGroup = new Group({ id: storedGroupId });
|
|
1896
|
+
await fallbackGroup.fetch();
|
|
1897
|
+
this.activeGroup = fallbackGroup;
|
|
1898
|
+
console.log("Fell back to stored active group:", fallbackGroup.get("name"));
|
|
1899
|
+
} catch (fallbackError) {
|
|
1900
|
+
console.warn("Fallback to stored group also failed:", fallbackError);
|
|
1901
|
+
this.clearActiveGroupId();
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Set the active group
|
|
1910
|
+
*/
|
|
1911
|
+
async setActiveGroup(group) {
|
|
1912
|
+
const previousGroup = this.activeGroup;
|
|
1913
|
+
this.activeGroup = group;
|
|
1914
|
+
if (group && group.get("id")) {
|
|
1915
|
+
this.saveActiveGroupId(group.get("id"));
|
|
1916
|
+
} else {
|
|
1917
|
+
this.clearActiveGroupId();
|
|
1918
|
+
}
|
|
1919
|
+
this.events.emit("group:changed", {
|
|
1920
|
+
group,
|
|
1921
|
+
previousGroup,
|
|
1922
|
+
app: this
|
|
1923
|
+
});
|
|
1924
|
+
this.router.updateUrl({ group: group.id }, { replace: true });
|
|
1925
|
+
console.log("Active group set to:", group ? group.get("name") : "none");
|
|
1926
|
+
return this;
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Get the active group
|
|
1930
|
+
*/
|
|
1931
|
+
getActiveGroup() {
|
|
1932
|
+
return this.activeGroup;
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Clear the active group
|
|
1936
|
+
*/
|
|
1937
|
+
async clearActiveGroup() {
|
|
1938
|
+
const previousGroup = this.activeGroup;
|
|
1939
|
+
this.activeGroup = null;
|
|
1940
|
+
this.clearActiveGroupId();
|
|
1941
|
+
this.events.emit("group:cleared", {
|
|
1942
|
+
previousGroup,
|
|
1943
|
+
app: this
|
|
1944
|
+
});
|
|
1945
|
+
return this;
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Save active group ID to localStorage
|
|
1949
|
+
*/
|
|
1950
|
+
saveActiveGroupId(groupId) {
|
|
1951
|
+
try {
|
|
1952
|
+
const key = this.getActiveGroupStorageKey();
|
|
1953
|
+
localStorage.setItem(key, groupId.toString());
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
console.warn("Failed to save active group ID:", error);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Load active group ID from localStorage
|
|
1960
|
+
*/
|
|
1961
|
+
loadActiveGroupId() {
|
|
1962
|
+
try {
|
|
1963
|
+
const key = this.getActiveGroupStorageKey();
|
|
1964
|
+
return localStorage.getItem(key);
|
|
1965
|
+
} catch (error) {
|
|
1966
|
+
console.warn("Failed to load active group ID:", error);
|
|
1967
|
+
return null;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Clear active group ID from localStorage
|
|
1972
|
+
*/
|
|
1973
|
+
clearActiveGroupId() {
|
|
1974
|
+
try {
|
|
1975
|
+
const key = this.getActiveGroupStorageKey();
|
|
1976
|
+
localStorage.removeItem(key);
|
|
1977
|
+
} catch (error) {
|
|
1978
|
+
console.warn("Failed to clear active group ID:", error);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
/**
|
|
1982
|
+
* Get storage key for active group ID
|
|
1983
|
+
*/
|
|
1984
|
+
getActiveGroupStorageKey() {
|
|
1985
|
+
return `mojo_active_group_id`;
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Check if user needs to select a group
|
|
1989
|
+
*/
|
|
1990
|
+
needsGroupSelection() {
|
|
1991
|
+
return !this.activeGroup;
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Setup layout based on configuration
|
|
1995
|
+
*/
|
|
1996
|
+
setupPageContainer() {
|
|
1997
|
+
const container = typeof this.container === "string" ? document.querySelector(this.container) : this.container;
|
|
1998
|
+
if (!container) {
|
|
1999
|
+
throw new Error(`Portal container not found: ${this.container}`);
|
|
2000
|
+
}
|
|
2001
|
+
const showSidebar = this.sidebarConfig && Object.keys(this.sidebarConfig).length > 0;
|
|
2002
|
+
const showTopbar = this.topbarConfig && Object.keys(this.topbarConfig).length > 0;
|
|
2003
|
+
container.innerHTML = `
|
|
2004
|
+
<div class="portal-layout hide-sidebar">
|
|
2005
|
+
${showSidebar ? '<div id="portal-sidebar"></div>' : ""}
|
|
2006
|
+
<div class="portal-body">
|
|
2007
|
+
${showTopbar ? '<div id="portal-topnav"></div>' : ""}
|
|
2008
|
+
<div class="portal-content" id="page-container">
|
|
2009
|
+
<!-- Pages render here -->
|
|
2010
|
+
</div>
|
|
2011
|
+
</div>
|
|
2012
|
+
</div>
|
|
2013
|
+
`;
|
|
2014
|
+
this.pageContainer = "#page-container";
|
|
2015
|
+
container.classList.add("portal-container");
|
|
2016
|
+
this.setupPortalComponents();
|
|
2017
|
+
this.applySidebarState(container);
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Setup portal components
|
|
2021
|
+
*/
|
|
2022
|
+
async setupPortalComponents() {
|
|
2023
|
+
await this.setupSidebar();
|
|
2024
|
+
await this.setupTopbar();
|
|
2025
|
+
this.setupPortalEvents();
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Setup sidebar component
|
|
2029
|
+
*/
|
|
2030
|
+
async setupSidebar() {
|
|
2031
|
+
if (!this.sidebarConfig || Object.keys(this.sidebarConfig).length === 0) return;
|
|
2032
|
+
this.sidebar = new Sidebar({
|
|
2033
|
+
containerId: "portal-sidebar",
|
|
2034
|
+
...this.sidebarConfig
|
|
2035
|
+
});
|
|
2036
|
+
await this.sidebar.render();
|
|
2037
|
+
}
|
|
2038
|
+
/**
|
|
2039
|
+
* Setup topbar component
|
|
2040
|
+
*/
|
|
2041
|
+
async setupTopbar() {
|
|
2042
|
+
if (!this.topbarConfig || Object.keys(this.topbarConfig).length === 0) return;
|
|
2043
|
+
this.topbar = new TopNav({
|
|
2044
|
+
containerId: "portal-topnav",
|
|
2045
|
+
className: this.topbarConfig.className || "navbar navbar-expand-lg navbar-dark",
|
|
2046
|
+
brandText: this.topbarConfig.brand || this.brand || this.title,
|
|
2047
|
+
brandRoute: this.topbarConfig.brandRoute || "/",
|
|
2048
|
+
brandIcon: this.topbarConfig.brandIcon || this.brandIcon,
|
|
2049
|
+
navItems: this.topbarConfig.leftItems || [],
|
|
2050
|
+
rightItems: this.topbarConfig.rightItems || [],
|
|
2051
|
+
displayMode: this.topbarConfig.displayMode || "both",
|
|
2052
|
+
showSidebarToggle: this.topbarConfig.showSidebarToggle || false,
|
|
2053
|
+
...this.topbarConfig
|
|
2054
|
+
});
|
|
2055
|
+
await this.topbar.render();
|
|
2056
|
+
this.topnav = this.topbar;
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Setup portal event handling
|
|
2060
|
+
*/
|
|
2061
|
+
setupPortalEvents() {
|
|
2062
|
+
document.addEventListener("click", (event) => {
|
|
2063
|
+
if (event.target.closest('[data-action="toggle-sidebar"]')) {
|
|
2064
|
+
event.preventDefault();
|
|
2065
|
+
this.toggleSidebar();
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
if (window.ResizeObserver) {
|
|
2069
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
2070
|
+
this.handleResponsive();
|
|
2071
|
+
});
|
|
2072
|
+
resizeObserver.observe(document.body);
|
|
2073
|
+
this._resizeObserver = resizeObserver;
|
|
2074
|
+
} else {
|
|
2075
|
+
this._resizeHandler = () => this.handleResponsive();
|
|
2076
|
+
window.addEventListener("resize", this._resizeHandler);
|
|
2077
|
+
}
|
|
2078
|
+
this.handleResponsive();
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* Toggle sidebar state
|
|
2082
|
+
*/
|
|
2083
|
+
toggleSidebar() {
|
|
2084
|
+
if (!this.sidebar) return;
|
|
2085
|
+
const container = document.querySelector(".portal-container");
|
|
2086
|
+
const isMobile = this.isMobile();
|
|
2087
|
+
if (isMobile) {
|
|
2088
|
+
container.classList.toggle("hide-sidebar");
|
|
2089
|
+
} else {
|
|
2090
|
+
container.classList.toggle("collapse-sidebar");
|
|
2091
|
+
this.sidebarCollapsed = !this.sidebarCollapsed;
|
|
2092
|
+
this.saveSidebarState(this.sidebarCollapsed);
|
|
2093
|
+
}
|
|
2094
|
+
this.events.emit("sidebar:toggled", {
|
|
2095
|
+
collapsed: this.sidebarCollapsed,
|
|
2096
|
+
mobile: isMobile
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
/**
|
|
2100
|
+
* Handle responsive layout
|
|
2101
|
+
*/
|
|
2102
|
+
handleResponsive() {
|
|
2103
|
+
const container = document.querySelector(".portal-container");
|
|
2104
|
+
if (!container) return;
|
|
2105
|
+
const isMobile = this.isMobile();
|
|
2106
|
+
if (isMobile) {
|
|
2107
|
+
container.classList.add("mobile-layout");
|
|
2108
|
+
if (!container.classList.contains("hide-sidebar")) {
|
|
2109
|
+
container.classList.add("hide-sidebar");
|
|
2110
|
+
}
|
|
2111
|
+
} else {
|
|
2112
|
+
container.classList.remove("mobile-layout", "hide-sidebar");
|
|
2113
|
+
}
|
|
2114
|
+
this.events.emit("responsive:changed", { mobile: isMobile });
|
|
2115
|
+
}
|
|
2116
|
+
getPortalContainer() {
|
|
2117
|
+
return document.querySelector(".portal-container");
|
|
2118
|
+
}
|
|
2119
|
+
isMobile() {
|
|
2120
|
+
return window.innerWidth < 768;
|
|
2121
|
+
}
|
|
2122
|
+
hasMobileLayout() {
|
|
2123
|
+
return this.getPortalContainer().classList.contains("mobile-layout");
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* Override showPage to update navigation
|
|
2127
|
+
*/
|
|
2128
|
+
async showPage(page, query = {}, params = {}, options = {}) {
|
|
2129
|
+
const result = await super.showPage(page, query, params, options);
|
|
2130
|
+
if (this.hasMobileLayout()) {
|
|
2131
|
+
this.getPortalContainer().classList.add("hide-sidebar");
|
|
2132
|
+
}
|
|
2133
|
+
if (result && this.currentPageInstance) {
|
|
2134
|
+
this.updateNavigation(this.currentPageInstance);
|
|
2135
|
+
}
|
|
2136
|
+
return result;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Update navigation active states
|
|
2140
|
+
*/
|
|
2141
|
+
updateNavigation(page) {
|
|
2142
|
+
if (this.sidebar && this.sidebar.setActivePage) {
|
|
2143
|
+
this.sidebar.setActivePage(page.route);
|
|
2144
|
+
}
|
|
2145
|
+
if (this.topbar && this.topbar.setActivePage) {
|
|
2146
|
+
this.topbar.setActivePage(page.route);
|
|
2147
|
+
}
|
|
2148
|
+
this.events.emit("portal:page-changed", { page });
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Set active user
|
|
2152
|
+
*/
|
|
2153
|
+
setActiveUser(user) {
|
|
2154
|
+
this.activeUser = user;
|
|
2155
|
+
if (this.topbar) {
|
|
2156
|
+
this.topbar.setUser(user);
|
|
2157
|
+
}
|
|
2158
|
+
console.log("Active user set:", user);
|
|
2159
|
+
this.events.emit("portal:user-changed", { user });
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Get the active user (for backward compatibility)
|
|
2163
|
+
*/
|
|
2164
|
+
getActiveUser() {
|
|
2165
|
+
return this.activeUser;
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Save sidebar state to localStorage
|
|
2169
|
+
*/
|
|
2170
|
+
saveSidebarState(collapsed) {
|
|
2171
|
+
try {
|
|
2172
|
+
const key = this.getSidebarStorageKey();
|
|
2173
|
+
localStorage.setItem(key, JSON.stringify(collapsed));
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
console.warn("Failed to save sidebar state:", error);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Load sidebar state from localStorage
|
|
2180
|
+
*/
|
|
2181
|
+
loadSidebarState() {
|
|
2182
|
+
try {
|
|
2183
|
+
const key = this.getSidebarStorageKey();
|
|
2184
|
+
const saved = localStorage.getItem(key);
|
|
2185
|
+
return saved !== null ? JSON.parse(saved) : null;
|
|
2186
|
+
} catch (error) {
|
|
2187
|
+
console.warn("Failed to load sidebar state:", error);
|
|
2188
|
+
return null;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Get storage key for sidebar state (allows multiple apps on same domain)
|
|
2193
|
+
*/
|
|
2194
|
+
getSidebarStorageKey() {
|
|
2195
|
+
const appKey = this.title ? this.title.replace(/\s+/g, "_").toLowerCase() : "portal_app";
|
|
2196
|
+
return `${appKey}_sidebar_collapsed`;
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* Apply saved sidebar state to the UI
|
|
2200
|
+
*/
|
|
2201
|
+
applySidebarState(container = null) {
|
|
2202
|
+
if (!container) {
|
|
2203
|
+
container = document.querySelector(".portal-container");
|
|
2204
|
+
}
|
|
2205
|
+
if (!container) return;
|
|
2206
|
+
if (this.sidebarCollapsed) {
|
|
2207
|
+
container.classList.add("collapse-sidebar");
|
|
2208
|
+
} else {
|
|
2209
|
+
container.classList.remove("collapse-sidebar");
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Clear saved sidebar state
|
|
2214
|
+
*/
|
|
2215
|
+
clearSidebarState() {
|
|
2216
|
+
try {
|
|
2217
|
+
const key = this.getSidebarStorageKey();
|
|
2218
|
+
localStorage.removeItem(key);
|
|
2219
|
+
} catch (error) {
|
|
2220
|
+
console.warn("Failed to clear sidebar state:", error);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
onPortalAction(action) {
|
|
2224
|
+
switch (action.action) {
|
|
2225
|
+
case "logout":
|
|
2226
|
+
this.showError("You have been logged out");
|
|
2227
|
+
this.tokenManager.clearTokens();
|
|
2228
|
+
this.setActiveUser(null);
|
|
2229
|
+
break;
|
|
2230
|
+
case "profile":
|
|
2231
|
+
this.showProfile();
|
|
2232
|
+
break;
|
|
2233
|
+
default:
|
|
2234
|
+
console.warn(`Unknown portal action: ${action}`);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
async showProfile() {
|
|
2238
|
+
if (!this.activeUser) {
|
|
2239
|
+
this.showError("No user is currently logged in");
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
try {
|
|
2243
|
+
if (this.activeUser?.attributes) {
|
|
2244
|
+
console.log("activeUser.attributes:", this.activeUser.attributes);
|
|
2245
|
+
}
|
|
2246
|
+
const result = await Dialog.showModelForm({
|
|
2247
|
+
title: "Edit Profile",
|
|
2248
|
+
size: "lg",
|
|
2249
|
+
fileHandling: "base64",
|
|
2250
|
+
model: this.activeUser,
|
|
2251
|
+
fields: [
|
|
2252
|
+
// Profile Header
|
|
2253
|
+
{
|
|
2254
|
+
type: "header",
|
|
2255
|
+
text: "Profile Information",
|
|
2256
|
+
level: 4,
|
|
2257
|
+
class: "text-primary mb-3"
|
|
2258
|
+
},
|
|
2259
|
+
// Avatar and Basic Info
|
|
2260
|
+
{
|
|
2261
|
+
type: "group",
|
|
2262
|
+
columns: { xs: 12, md: 4 },
|
|
2263
|
+
title: "Avatar",
|
|
2264
|
+
fields: [
|
|
2265
|
+
{
|
|
2266
|
+
type: "image",
|
|
2267
|
+
name: "avatar",
|
|
2268
|
+
size: "lg",
|
|
2269
|
+
imageSize: { width: 200, height: 200 },
|
|
2270
|
+
placeholder: "Upload your avatar",
|
|
2271
|
+
help: "Square images work best"
|
|
2272
|
+
}
|
|
2273
|
+
]
|
|
2274
|
+
},
|
|
2275
|
+
// Profile Details
|
|
2276
|
+
{
|
|
2277
|
+
type: "group",
|
|
2278
|
+
columns: { xs: 12, md: 8 },
|
|
2279
|
+
title: "Details",
|
|
2280
|
+
fields: [
|
|
2281
|
+
{
|
|
2282
|
+
type: "text",
|
|
2283
|
+
name: "display_name",
|
|
2284
|
+
label: "Display Name",
|
|
2285
|
+
required: true,
|
|
2286
|
+
columns: 12,
|
|
2287
|
+
placeholder: "Enter first name"
|
|
2288
|
+
},
|
|
2289
|
+
{
|
|
2290
|
+
type: "email",
|
|
2291
|
+
name: "email",
|
|
2292
|
+
label: "Email Address",
|
|
2293
|
+
required: true,
|
|
2294
|
+
columns: 8,
|
|
2295
|
+
placeholder: "your.email@example.com"
|
|
2296
|
+
},
|
|
2297
|
+
{
|
|
2298
|
+
type: "tel",
|
|
2299
|
+
name: "phone_number",
|
|
2300
|
+
label: "Phone Number",
|
|
2301
|
+
columns: 4,
|
|
2302
|
+
placeholder: "(555) 123-4567"
|
|
2303
|
+
}
|
|
2304
|
+
]
|
|
2305
|
+
},
|
|
2306
|
+
// Account Settings
|
|
2307
|
+
{
|
|
2308
|
+
type: "group",
|
|
2309
|
+
columns: 12,
|
|
2310
|
+
title: "Account Settings",
|
|
2311
|
+
class: "pt-3",
|
|
2312
|
+
fields: [
|
|
2313
|
+
{
|
|
2314
|
+
type: "select",
|
|
2315
|
+
name: "timezone",
|
|
2316
|
+
label: "Timezone",
|
|
2317
|
+
columns: 6,
|
|
2318
|
+
options: [
|
|
2319
|
+
{ value: "America/New_York", text: "Eastern Time" },
|
|
2320
|
+
{ value: "America/Chicago", text: "Central Time" },
|
|
2321
|
+
{ value: "America/Denver", text: "Mountain Time" },
|
|
2322
|
+
{ value: "America/Los_Angeles", text: "Pacific Time" },
|
|
2323
|
+
{ value: "UTC", text: "UTC" }
|
|
2324
|
+
]
|
|
2325
|
+
},
|
|
2326
|
+
{
|
|
2327
|
+
type: "select",
|
|
2328
|
+
name: "language",
|
|
2329
|
+
label: "Language",
|
|
2330
|
+
columns: 6,
|
|
2331
|
+
options: [
|
|
2332
|
+
{ value: "en", text: "English" },
|
|
2333
|
+
{ value: "es", text: "Spanish" },
|
|
2334
|
+
{ value: "fr", text: "French" },
|
|
2335
|
+
{ value: "de", text: "German" }
|
|
2336
|
+
]
|
|
2337
|
+
},
|
|
2338
|
+
{
|
|
2339
|
+
type: "switch",
|
|
2340
|
+
name: "email_notifications",
|
|
2341
|
+
label: "Email Notifications",
|
|
2342
|
+
columns: 4
|
|
2343
|
+
},
|
|
2344
|
+
{
|
|
2345
|
+
type: "switch",
|
|
2346
|
+
name: "two_factor_enabled",
|
|
2347
|
+
label: "Two-Factor Authentication",
|
|
2348
|
+
columns: 4
|
|
2349
|
+
},
|
|
2350
|
+
{
|
|
2351
|
+
type: "switch",
|
|
2352
|
+
name: "profile_public",
|
|
2353
|
+
label: "Public Profile",
|
|
2354
|
+
columns: 4
|
|
2355
|
+
}
|
|
2356
|
+
]
|
|
2357
|
+
}
|
|
2358
|
+
],
|
|
2359
|
+
submitText: "Save Profile",
|
|
2360
|
+
cancelText: "Cancel"
|
|
2361
|
+
});
|
|
2362
|
+
if (result && result.success) {
|
|
2363
|
+
console.log("Profile saved successfully:", result);
|
|
2364
|
+
this.showSuccess("Profile updated successfully!");
|
|
2365
|
+
} else if (result && !result.success) {
|
|
2366
|
+
console.log("Profile save failed:", result);
|
|
2367
|
+
}
|
|
2368
|
+
} catch (error) {
|
|
2369
|
+
console.error("Error showing profile form:", error);
|
|
2370
|
+
this.showError("Failed to load profile form");
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Clean up portal resources
|
|
2375
|
+
*/
|
|
2376
|
+
async destroy() {
|
|
2377
|
+
this.activeGroup = null;
|
|
2378
|
+
if (this._resizeObserver) {
|
|
2379
|
+
this._resizeObserver.disconnect();
|
|
2380
|
+
}
|
|
2381
|
+
if (this._resizeHandler) {
|
|
2382
|
+
window.removeEventListener("resize", this._resizeHandler);
|
|
2383
|
+
}
|
|
2384
|
+
if (this.topbar) {
|
|
2385
|
+
await this.topbar.destroy();
|
|
2386
|
+
this.topbar = null;
|
|
2387
|
+
this.topnav = null;
|
|
2388
|
+
}
|
|
2389
|
+
if (this.sidebar) {
|
|
2390
|
+
await this.sidebar.destroy();
|
|
2391
|
+
this.sidebar = null;
|
|
2392
|
+
}
|
|
2393
|
+
await super.destroy();
|
|
2394
|
+
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Static factory method
|
|
2397
|
+
*/
|
|
2398
|
+
static create(config = {}) {
|
|
2399
|
+
return new PortalApp(config);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
class MustacheFormatter {
|
|
2403
|
+
constructor() {
|
|
2404
|
+
this.formatter = dataFormatter;
|
|
2405
|
+
this.compiledTemplates = /* @__PURE__ */ new Map();
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Render template with data
|
|
2409
|
+
* Pipes are now handled by Model.get() and View.get() automatically
|
|
2410
|
+
*
|
|
2411
|
+
* @param {string} template - Mustache template
|
|
2412
|
+
* @param {object} data - Data to render (View, Model, or plain object)
|
|
2413
|
+
* @param {object} partials - Mustache partials
|
|
2414
|
+
* @returns {string} Rendered template
|
|
2415
|
+
*/
|
|
2416
|
+
render(template, data, partials = {}) {
|
|
2417
|
+
return Mustache.render(template, data, partials);
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* Compile template for reuse
|
|
2421
|
+
* @param {string} template - Template to compile
|
|
2422
|
+
* @returns {object} Compiled template tokens
|
|
2423
|
+
*/
|
|
2424
|
+
compile(template) {
|
|
2425
|
+
const compiled = Mustache.parse(template);
|
|
2426
|
+
this.compiledTemplates.set(template, compiled);
|
|
2427
|
+
return compiled;
|
|
2428
|
+
}
|
|
2429
|
+
/**
|
|
2430
|
+
* Render with compiled template
|
|
2431
|
+
* @param {object} compiled - Compiled template tokens
|
|
2432
|
+
* @param {object} data - Data to render
|
|
2433
|
+
* @param {object} partials - Mustache partials
|
|
2434
|
+
* @returns {string} Rendered template
|
|
2435
|
+
*/
|
|
2436
|
+
renderCompiled(compiled, data, partials = {}) {
|
|
2437
|
+
return Mustache.render(compiled, data, partials);
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Clear compiled template cache
|
|
2441
|
+
*/
|
|
2442
|
+
clearCache() {
|
|
2443
|
+
this.compiledTemplates.clear();
|
|
2444
|
+
Mustache.clearCache();
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Process and cache a template
|
|
2448
|
+
* @param {string} key - Cache key
|
|
2449
|
+
* @param {string} template - Template to cache
|
|
2450
|
+
* @returns {object} Cached template info
|
|
2451
|
+
*/
|
|
2452
|
+
cache(key, template) {
|
|
2453
|
+
const compiled = this.compile(template);
|
|
2454
|
+
return { key, template, compiled };
|
|
2455
|
+
}
|
|
2456
|
+
/**
|
|
2457
|
+
* Get cached template
|
|
2458
|
+
* @param {string} key - Cache key
|
|
2459
|
+
* @returns {object|null} Cached template info or null
|
|
2460
|
+
*/
|
|
2461
|
+
getCached(key) {
|
|
2462
|
+
for (const [template, compiled] of this.compiledTemplates) {
|
|
2463
|
+
if (template === key || compiled === key) {
|
|
2464
|
+
return { key, template, compiled };
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
return null;
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Register a custom formatter with DataFormatter
|
|
2471
|
+
* @param {string} name - Formatter name
|
|
2472
|
+
* @param {function} formatter - Formatter function
|
|
2473
|
+
* @returns {MustacheFormatter} This instance for chaining
|
|
2474
|
+
*/
|
|
2475
|
+
registerFormatter(name, formatter) {
|
|
2476
|
+
this.formatter.register(name, formatter);
|
|
2477
|
+
return this;
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Check if a string contains pipe syntax
|
|
2481
|
+
* @param {string} template - Template string to check
|
|
2482
|
+
* @returns {boolean} True if contains pipes
|
|
2483
|
+
*/
|
|
2484
|
+
hasPipes(template) {
|
|
2485
|
+
return /\{\{[{]?[^}|]+\|[^}]+\}[}]?\}/.test(template);
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Pre-process data with pipe formatters
|
|
2489
|
+
* This is now handled automatically by get() methods, but kept for backward compatibility
|
|
2490
|
+
*
|
|
2491
|
+
* @param {object} data - Data object
|
|
2492
|
+
* @param {object} pipes - Object mapping keys to pipe strings
|
|
2493
|
+
* @returns {object} Processed data
|
|
2494
|
+
*/
|
|
2495
|
+
processData(data, pipes) {
|
|
2496
|
+
const processed = { ...data };
|
|
2497
|
+
for (const [key, pipeString] of Object.entries(pipes)) {
|
|
2498
|
+
if (data && typeof data.get === "function") {
|
|
2499
|
+
processed[key] = data.get(`${key}|${pipeString}`);
|
|
2500
|
+
} else {
|
|
2501
|
+
const value = this.getValueFromPath(data, key);
|
|
2502
|
+
processed[key] = this.formatter.pipe(value, pipeString);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
return processed;
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Get value from object using dot notation path
|
|
2509
|
+
* Kept for backward compatibility, but MOJOUtils.getContextData is preferred
|
|
2510
|
+
*
|
|
2511
|
+
* @param {object} obj - Source object
|
|
2512
|
+
* @param {string} path - Dot notation path
|
|
2513
|
+
* @returns {*} Value at path
|
|
2514
|
+
*/
|
|
2515
|
+
getValueFromPath(obj, path) {
|
|
2516
|
+
if (!obj || !path) return void 0;
|
|
2517
|
+
if (obj && typeof obj.get === "function") {
|
|
2518
|
+
return obj.get(path);
|
|
2519
|
+
}
|
|
2520
|
+
const keys = path.split(".");
|
|
2521
|
+
let current = obj;
|
|
2522
|
+
for (const key of keys) {
|
|
2523
|
+
if (current === null || current === void 0) {
|
|
2524
|
+
return void 0;
|
|
2525
|
+
}
|
|
2526
|
+
if (!isNaN(key) && Array.isArray(current)) {
|
|
2527
|
+
current = current[parseInt(key)];
|
|
2528
|
+
} else {
|
|
2529
|
+
current = current[key];
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
return current;
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Process template to handle pipe formatters
|
|
2536
|
+
* @deprecated Pipes are now handled by get() methods automatically
|
|
2537
|
+
* @param {string} template - Original template
|
|
2538
|
+
* @param {object} data - Original data
|
|
2539
|
+
* @returns {object} {template: processedTemplate, data: processedData}
|
|
2540
|
+
*/
|
|
2541
|
+
processTemplate(template, data) {
|
|
2542
|
+
return { template, data };
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
const mustacheFormatter = new MustacheFormatter();
|
|
2546
|
+
const FRAMEWORK_NAME = "MOJO";
|
|
2547
|
+
const PACKAGE_NAME = "web-mojo";
|
|
2548
|
+
const index = {
|
|
2549
|
+
FRAMEWORK_NAME,
|
|
2550
|
+
PACKAGE_NAME
|
|
2551
|
+
};
|
|
2552
|
+
export {
|
|
2553
|
+
B as BUILD_TIME,
|
|
2554
|
+
C as Collection,
|
|
2555
|
+
default2 as DataView,
|
|
2556
|
+
D as DataWrapper,
|
|
2557
|
+
Dialog,
|
|
2558
|
+
E2 as EmailDomain,
|
|
2559
|
+
i2 as EmailDomainForms,
|
|
2560
|
+
h3 as EmailDomainList,
|
|
2561
|
+
o as EmailTemplate,
|
|
2562
|
+
q as EmailTemplateForms,
|
|
2563
|
+
p as EmailTemplateList,
|
|
2564
|
+
g as EventBus,
|
|
2565
|
+
E as EventDelegate,
|
|
2566
|
+
FRAMEWORK_NAME,
|
|
2567
|
+
u as File,
|
|
2568
|
+
w as FileForms,
|
|
2569
|
+
v as FileList,
|
|
2570
|
+
r2 as FileManager,
|
|
2571
|
+
t as FileManagerForms,
|
|
2572
|
+
s as FileManagerList,
|
|
2573
|
+
F as FilePreviewView,
|
|
2574
|
+
e3 as FileUpload,
|
|
2575
|
+
F2 as FormView,
|
|
2576
|
+
am as GeoLocatedIP,
|
|
2577
|
+
an as GeoLocatedIPList,
|
|
2578
|
+
Group,
|
|
2579
|
+
b2 as GroupForms,
|
|
2580
|
+
GroupList,
|
|
2581
|
+
z as Incident,
|
|
2582
|
+
I as IncidentEvent,
|
|
2583
|
+
y as IncidentEventForms,
|
|
2584
|
+
x as IncidentEventList,
|
|
2585
|
+
B2 as IncidentForms,
|
|
2586
|
+
J as IncidentHistory,
|
|
2587
|
+
K as IncidentHistoryList,
|
|
2588
|
+
A as IncidentList,
|
|
2589
|
+
G as IncidentRule,
|
|
2590
|
+
H as IncidentRuleList,
|
|
2591
|
+
C2 as IncidentRuleSet,
|
|
2592
|
+
D2 as IncidentRuleSetList,
|
|
2593
|
+
U as IncidentStats,
|
|
2594
|
+
V as Job,
|
|
2595
|
+
_ as JobEvent,
|
|
2596
|
+
$ as JobEventList,
|
|
2597
|
+
X as JobForms,
|
|
2598
|
+
W as JobList,
|
|
2599
|
+
Y as JobLog,
|
|
2600
|
+
Z as JobLogList,
|
|
2601
|
+
a1 as JobRunner,
|
|
2602
|
+
a3 as JobRunnerForms,
|
|
2603
|
+
a2 as JobRunnerList,
|
|
2604
|
+
a0 as JobsEngineStats,
|
|
2605
|
+
L as ListView,
|
|
2606
|
+
c3 as ListViewItem,
|
|
2607
|
+
a4 as Log,
|
|
2608
|
+
a5 as LogList,
|
|
2609
|
+
h as MOJOUtils,
|
|
2610
|
+
M2 as Mailbox,
|
|
2611
|
+
k as MailboxForms,
|
|
2612
|
+
j as MailboxList,
|
|
2613
|
+
a6 as Member,
|
|
2614
|
+
a8 as MemberForms,
|
|
2615
|
+
a7 as MemberList,
|
|
2616
|
+
ab as MetricsForms,
|
|
2617
|
+
a9 as MetricsPermission,
|
|
2618
|
+
aa as MetricsPermissionList,
|
|
2619
|
+
M as Model,
|
|
2620
|
+
mustacheFormatter as MustacheFormatter,
|
|
2621
|
+
PACKAGE_NAME,
|
|
2622
|
+
Page,
|
|
2623
|
+
PortalApp,
|
|
2624
|
+
P as ProgressView,
|
|
2625
|
+
ag as PushConfig,
|
|
2626
|
+
ak as PushConfigForms,
|
|
2627
|
+
ah as PushConfigList,
|
|
2628
|
+
ai as PushDelivery,
|
|
2629
|
+
aj as PushDeliveryList,
|
|
2630
|
+
ac as PushDevice,
|
|
2631
|
+
ad as PushDeviceList,
|
|
2632
|
+
ae as PushTemplate,
|
|
2633
|
+
al as PushTemplateForms,
|
|
2634
|
+
af as PushTemplateList,
|
|
2635
|
+
r as Rest,
|
|
2636
|
+
R as Router,
|
|
2637
|
+
O as Rule,
|
|
2638
|
+
Q as RuleList,
|
|
2639
|
+
R2 as RuleSet,
|
|
2640
|
+
N as RuleSetList,
|
|
2641
|
+
S as S3Bucket,
|
|
2642
|
+
g3 as S3BucketForms,
|
|
2643
|
+
f3 as S3BucketList,
|
|
2644
|
+
l as SentMessage,
|
|
2645
|
+
n as SentMessageForms,
|
|
2646
|
+
m as SentMessageList,
|
|
2647
|
+
Sidebar,
|
|
2648
|
+
SimpleSearchView,
|
|
2649
|
+
d2 as TabView,
|
|
2650
|
+
b3 as TablePage,
|
|
2651
|
+
a10 as TableRow,
|
|
2652
|
+
T as TableView,
|
|
2653
|
+
ao as Ticket,
|
|
2654
|
+
as as TicketForms,
|
|
2655
|
+
ap as TicketList,
|
|
2656
|
+
aq as TicketNote,
|
|
2657
|
+
ar as TicketNoteList,
|
|
2658
|
+
ToastService,
|
|
2659
|
+
TokenManager,
|
|
2660
|
+
TopNav,
|
|
2661
|
+
User,
|
|
2662
|
+
e2 as UserDataView,
|
|
2663
|
+
f2 as UserDevice,
|
|
2664
|
+
g2 as UserDeviceList,
|
|
2665
|
+
h2 as UserDeviceLocation,
|
|
2666
|
+
i as UserDeviceLocationList,
|
|
2667
|
+
d as UserForms,
|
|
2668
|
+
c2 as UserList,
|
|
2669
|
+
b as VERSION,
|
|
2670
|
+
a as VERSION_INFO,
|
|
2671
|
+
c as VERSION_MAJOR,
|
|
2672
|
+
e as VERSION_MINOR,
|
|
2673
|
+
f as VERSION_REVISION,
|
|
2674
|
+
View,
|
|
2675
|
+
WebApp,
|
|
2676
|
+
W2 as WebSocketClient,
|
|
2677
|
+
a11 as applyFileDropMixin,
|
|
2678
|
+
dataFormatter,
|
|
2679
|
+
index as default
|
|
2680
|
+
};
|
|
2681
|
+
//# sourceMappingURL=index.es.js.map
|