web-mojo 2.2.57 → 2.2.59

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