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.
Files changed (91) hide show
  1. package/LICENSE +198 -0
  2. package/README.md +510 -0
  3. package/dist/admin.cjs.js +2 -0
  4. package/dist/admin.cjs.js.map +1 -0
  5. package/dist/admin.css +621 -0
  6. package/dist/admin.es.js +7973 -0
  7. package/dist/admin.es.js.map +1 -0
  8. package/dist/auth.cjs.js +2 -0
  9. package/dist/auth.cjs.js.map +1 -0
  10. package/dist/auth.css +804 -0
  11. package/dist/auth.es.js +2168 -0
  12. package/dist/auth.es.js.map +1 -0
  13. package/dist/charts.cjs.js +2 -0
  14. package/dist/charts.cjs.js.map +1 -0
  15. package/dist/charts.css +1002 -0
  16. package/dist/charts.es.js +16 -0
  17. package/dist/charts.es.js.map +1 -0
  18. package/dist/chunks/ContextMenu-BrHqj0fn.js +80 -0
  19. package/dist/chunks/ContextMenu-BrHqj0fn.js.map +1 -0
  20. package/dist/chunks/ContextMenu-gEcpSz56.js +2 -0
  21. package/dist/chunks/ContextMenu-gEcpSz56.js.map +1 -0
  22. package/dist/chunks/DataView-DPryYpEW.js +2 -0
  23. package/dist/chunks/DataView-DPryYpEW.js.map +1 -0
  24. package/dist/chunks/DataView-DjZQrpba.js +843 -0
  25. package/dist/chunks/DataView-DjZQrpba.js.map +1 -0
  26. package/dist/chunks/Dialog-BsRx4eg3.js +2 -0
  27. package/dist/chunks/Dialog-BsRx4eg3.js.map +1 -0
  28. package/dist/chunks/Dialog-DSlctbon.js +1377 -0
  29. package/dist/chunks/Dialog-DSlctbon.js.map +1 -0
  30. package/dist/chunks/FilePreviewView-BmFHzK5K.js +5868 -0
  31. package/dist/chunks/FilePreviewView-BmFHzK5K.js.map +1 -0
  32. package/dist/chunks/FilePreviewView-DcdRl_ta.js +2 -0
  33. package/dist/chunks/FilePreviewView-DcdRl_ta.js.map +1 -0
  34. package/dist/chunks/FormView-CmBuwKGD.js +2 -0
  35. package/dist/chunks/FormView-CmBuwKGD.js.map +1 -0
  36. package/dist/chunks/FormView-DqUBMPJ9.js +5054 -0
  37. package/dist/chunks/FormView-DqUBMPJ9.js.map +1 -0
  38. package/dist/chunks/MetricsChart-CM4CI6eA.js +2095 -0
  39. package/dist/chunks/MetricsChart-CM4CI6eA.js.map +1 -0
  40. package/dist/chunks/MetricsChart-CPidSMaN.js +2 -0
  41. package/dist/chunks/MetricsChart-CPidSMaN.js.map +1 -0
  42. package/dist/chunks/PDFViewer-BNQlnS83.js +2 -0
  43. package/dist/chunks/PDFViewer-BNQlnS83.js.map +1 -0
  44. package/dist/chunks/PDFViewer-Dyo-Oeyd.js +946 -0
  45. package/dist/chunks/PDFViewer-Dyo-Oeyd.js.map +1 -0
  46. package/dist/chunks/Page-B524zSQs.js +351 -0
  47. package/dist/chunks/Page-B524zSQs.js.map +1 -0
  48. package/dist/chunks/Page-BFgj0pAA.js +2 -0
  49. package/dist/chunks/Page-BFgj0pAA.js.map +1 -0
  50. package/dist/chunks/TokenManager-BXNva8Jk.js +287 -0
  51. package/dist/chunks/TokenManager-BXNva8Jk.js.map +1 -0
  52. package/dist/chunks/TokenManager-Bzn4guFm.js +2 -0
  53. package/dist/chunks/TokenManager-Bzn4guFm.js.map +1 -0
  54. package/dist/chunks/TopNav-D3I3_25f.js +371 -0
  55. package/dist/chunks/TopNav-D3I3_25f.js.map +1 -0
  56. package/dist/chunks/TopNav-MDjL4kV0.js +2 -0
  57. package/dist/chunks/TopNav-MDjL4kV0.js.map +1 -0
  58. package/dist/chunks/User-BalfYTEF.js +3 -0
  59. package/dist/chunks/User-BalfYTEF.js.map +1 -0
  60. package/dist/chunks/User-DwIT-CTQ.js +1937 -0
  61. package/dist/chunks/User-DwIT-CTQ.js.map +1 -0
  62. package/dist/chunks/WebApp-B6mgbNn2.js +4767 -0
  63. package/dist/chunks/WebApp-B6mgbNn2.js.map +1 -0
  64. package/dist/chunks/WebApp-DqDowtkl.js +2 -0
  65. package/dist/chunks/WebApp-DqDowtkl.js.map +1 -0
  66. package/dist/chunks/WebSocketClient-D6i85jl2.js +2 -0
  67. package/dist/chunks/WebSocketClient-D6i85jl2.js.map +1 -0
  68. package/dist/chunks/WebSocketClient-Dvl3AYx1.js +297 -0
  69. package/dist/chunks/WebSocketClient-Dvl3AYx1.js.map +1 -0
  70. package/dist/core.css +1181 -0
  71. package/dist/css/web-mojo.css +17 -0
  72. package/dist/css-manifest.json +6 -0
  73. package/dist/docit.cjs.js +2 -0
  74. package/dist/docit.cjs.js.map +1 -0
  75. package/dist/docit.es.js +959 -0
  76. package/dist/docit.es.js.map +1 -0
  77. package/dist/index.cjs.js +2 -0
  78. package/dist/index.cjs.js.map +1 -0
  79. package/dist/index.es.js +2681 -0
  80. package/dist/index.es.js.map +1 -0
  81. package/dist/lightbox.cjs.js +2 -0
  82. package/dist/lightbox.cjs.js.map +1 -0
  83. package/dist/lightbox.css +606 -0
  84. package/dist/lightbox.es.js +3737 -0
  85. package/dist/lightbox.es.js.map +1 -0
  86. package/dist/loader.es.js +115 -0
  87. package/dist/loader.umd.js +85 -0
  88. package/dist/portal.css +2446 -0
  89. package/dist/table.css +639 -0
  90. package/dist/toast.css +181 -0
  91. package/package.json +179 -0
@@ -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