web-mojo 2.2.56 → 2.2.58

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 (116) 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 -10106
  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-CqkYoMmr.js +2 -0
  12. package/dist/chunks/{ChatView-9k6xBWXk.js.map → ChatView-CqkYoMmr.js.map} +1 -1
  13. package/dist/chunks/{ChatView-CdtuCDYm.js → ChatView-DFN9xt0c.js} +2 -2
  14. package/dist/chunks/{ChatView-CdtuCDYm.js.map → ChatView-DFN9xt0c.js.map} +1 -1
  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-CU1HG1XH.js +2 -0
  70. package/dist/chunks/version-CU1HG1XH.js.map +1 -0
  71. package/dist/chunks/{version-CQCmm8rw.js → version-DaB1uXvO.js} +2 -2
  72. package/dist/chunks/{version-CQCmm8rw.js.map → version-DaB1uXvO.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/Collection-DaiL0uGl.js +0 -1014
  99. package/dist/chunks/ContextMenu-sgvgSACY.js +0 -1535
  100. package/dist/chunks/DataView-Dzo0jbs2.js +0 -862
  101. package/dist/chunks/Dialog-DOGDalUq.js +0 -1579
  102. package/dist/chunks/FormPlugins-DY6e88YT.js +0 -124
  103. package/dist/chunks/FormView-Dz3mYasQ.js +0 -8636
  104. package/dist/chunks/ListView-X5w5jf51.js +0 -495
  105. package/dist/chunks/MetricsCountryMapView-B2xz6zUw.js +0 -1054
  106. package/dist/chunks/MetricsMiniChartWidget-DvKd7Qrk.js +0 -3283
  107. package/dist/chunks/PDFViewer-EJ9cOfPF.js +0 -946
  108. package/dist/chunks/Rest-CgSjfMaU.js +0 -2
  109. package/dist/chunks/Rest-CgSjfMaU.js.map +0 -1
  110. package/dist/chunks/Rest-W-sPfGh9.js +0 -4375
  111. package/dist/chunks/Rest-W-sPfGh9.js.map +0 -1
  112. package/dist/chunks/TokenManager-DoN9e6q6.js +0 -1423
  113. package/dist/chunks/WebApp-6qvqmOts.js +0 -1386
  114. package/dist/chunks/WebSocketClient-DG2olXpH.js +0 -209
  115. package/dist/chunks/version-MS6IWlRk.js +0 -38
  116. package/dist/chunks/version-MS6IWlRk.js.map +0 -1
package/dist/index.es.js CHANGED
@@ -1,3253 +1,2 @@
1
- import { B, a, V, b, c, d } from "./chunks/version-MS6IWlRk.js";
2
- import { V as View, d as dataFormatter, M as Mustache } from "./chunks/Rest-W-sPfGh9.js";
3
- import { D, E, a as a2, r } from "./chunks/Rest-W-sPfGh9.js";
4
- import { G as GroupList, a as Group, P as Page, T as ToastService, U as User } from "./chunks/ContextMenu-sgvgSACY.js";
5
- import { C, b as b2, e, f, g, h, i, d as d2, c as c2 } from "./chunks/ContextMenu-sgvgSACY.js";
6
- import { W as WebApp } from "./chunks/WebApp-6qvqmOts.js";
7
- import { E as E2, R } from "./chunks/WebApp-6qvqmOts.js";
8
- import { C as C2, M } from "./chunks/Collection-DaiL0uGl.js";
9
- import { M as Member } from "./chunks/ChatView-9k6xBWXk.js";
10
- import { a1, e as e2, d as d3, C as C3, a5, a6, a3, D as D2, E as E3, k, j, s, u, t, y, A, z, v, x, w, F, f as f2, az, aA, H, I, G, B as B2, K, U, V as V2, J, Q, R as R2, N, O, a0, a7, ac, ad, a9, a8, aa, ab, af, ah, ag, ae, L, ai, aj, l, n, m, a2 as a22, al, ak, ao, am, an, P, at, ax, au, av, aw, ap, aq, ar, ay, as, Z, $, _, W, Y, X, S, i as i2, h as h2, o, r as r2, q, c as c3, b as b3, a as a4, T, aB, aG, aF, aC, aD, aE, a4 as a42, g as g2, p } from "./chunks/ChatView-9k6xBWXk.js";
11
- import { S as SimpleSearchView, T as TokenManager, a as TopNav } from "./chunks/TokenManager-DoN9e6q6.js";
12
- import Dialog from "./chunks/Dialog-DOGDalUq.js";
13
- import { L as L2, a as a10 } from "./chunks/ListView-X5w5jf51.js";
14
- import { default as default2 } from "./chunks/DataView-Dzo0jbs2.js";
15
- import { F as FormView } from "./chunks/FormView-Dz3mYasQ.js";
16
- import { a as a11 } from "./chunks/FormView-Dz3mYasQ.js";
17
- import { W as W2 } from "./chunks/WebSocketClient-DG2olXpH.js";
18
- const __vite_import_meta_env__ = { "BASE_URL": "/", "DEV": false, "MODE": "production", "PROD": true, "SSR": false };
19
- const LEVELS = Object.freeze({
20
- silent: 0,
21
- error: 1,
22
- warn: 2,
23
- info: 3,
24
- log: 3,
25
- // alias to info
26
- debug: 4,
27
- trace: 5,
28
- all: 5
29
- // alias
30
- });
31
- const isDev = (() => {
32
- try {
33
- if (typeof import.meta !== "undefined" && import.meta && __vite_import_meta_env__ && true) {
34
- return false;
35
- }
36
- } catch {
37
- }
38
- if (typeof globalThis !== "undefined" && typeof globalThis.__DEV__ !== "undefined") {
39
- try {
40
- return !!globalThis.__DEV__;
41
- } catch {
42
- }
43
- }
44
- const hasProcess = typeof process !== "undefined" && process && typeof process.env === "object";
45
- if (hasProcess && typeof process.env.NODE_ENV === "string") {
46
- return process.env.NODE_ENV !== "production";
47
- }
48
- return false;
49
- })();
50
- const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
51
- const GLOBAL = typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : global;
52
- const ORIGINAL_CONSOLE = GLOBAL.console || {};
53
- const ORIGINALS = {};
54
- let INSTALLED = false;
55
- let CURRENT_LEVEL = null;
56
- const DEFAULT_DEV_LEVEL = "debug";
57
- const DEFAULT_PROD_LEVEL = "warn";
58
- function parseLevel(level) {
59
- if (typeof level === "number") {
60
- const min = LEVELS.silent;
61
- const max = LEVELS.trace;
62
- return Math.min(Math.max(level, min), max);
63
- }
64
- if (typeof level === "string") {
65
- const key = level.toLowerCase();
66
- if (Object.prototype.hasOwnProperty.call(LEVELS, key)) {
67
- return LEVELS[key];
68
- }
69
- }
70
- return null;
71
- }
72
- function getUrlLogLevel() {
73
- if (!isBrowser || typeof location === "undefined" || !location.search) return null;
74
- try {
75
- const params = new URLSearchParams(location.search);
76
- const keys = ["logLevel", "loglevel", "mojoLog"];
77
- for (const k2 of keys) {
78
- const v2 = params.get(k2);
79
- if (v2 != null) {
80
- const parsed = parseLevel(v2);
81
- if (parsed !== null) return parsed;
82
- }
83
- }
84
- } catch {
85
- }
86
- return null;
87
- }
88
- function getStoredLogLevel() {
89
- if (!isBrowser || !("localStorage" in GLOBAL)) return null;
90
- try {
91
- const v2 = GLOBAL.localStorage.getItem("MOJO_LOG_LEVEL");
92
- if (v2 != null) {
93
- const parsed = parseLevel(v2);
94
- if (parsed !== null) return parsed;
95
- }
96
- } catch {
97
- }
98
- return null;
99
- }
100
- function storeLogLevel(levelNumberOrName) {
101
- if (!isBrowser || !("localStorage" in GLOBAL)) return;
102
- try {
103
- const key = typeof levelNumberOrName === "string" ? levelNumberOrName : levelNumberOrName === null ? null : Object.entries(LEVELS).find(([, num]) => num === levelNumberOrName)?.[0] ?? null;
104
- if (key) {
105
- GLOBAL.localStorage.setItem("MOJO_LOG_LEVEL", key);
106
- } else {
107
- GLOBAL.localStorage.removeItem("MOJO_LOG_LEVEL");
108
- }
109
- } catch {
110
- }
111
- }
112
- function makeWrapper(methodName, methodLevel) {
113
- const original = ORIGINALS[methodName] || ORIGINAL_CONSOLE[methodName] || (() => {
114
- });
115
- return function wrappedConsoleMethod(...args) {
116
- if (CURRENT_LEVEL >= methodLevel) {
117
- return original.apply(ORIGINAL_CONSOLE, args);
118
- }
119
- return void 0;
120
- };
121
- }
122
- function makeAssertWrapper() {
123
- const original = ORIGINALS.assert || ORIGINAL_CONSOLE.assert || (() => {
124
- });
125
- return function wrappedAssert(condition, ...args) {
126
- if (!condition) {
127
- if (CURRENT_LEVEL >= LEVELS.error) {
128
- return original.apply(ORIGINAL_CONSOLE, [condition, ...args]);
129
- }
130
- return void 0;
131
- }
132
- return void 0;
133
- };
134
- }
135
- function determineInitialLevel(explicitLevel) {
136
- const explicit = parseLevel(explicitLevel);
137
- if (explicit !== null) return explicit;
138
- const urlLevel = getUrlLogLevel();
139
- if (urlLevel !== null) return urlLevel;
140
- const storedLevel = getStoredLogLevel();
141
- if (storedLevel !== null) return storedLevel;
142
- return parseLevel(isDev ? DEFAULT_DEV_LEVEL : DEFAULT_PROD_LEVEL);
143
- }
144
- function buildPatchedConsole() {
145
- const patched = { ...ORIGINAL_CONSOLE };
146
- const methodLevels = {
147
- // Critical
148
- error: LEVELS.error,
149
- warn: LEVELS.warn,
150
- // Informational
151
- info: LEVELS.info,
152
- log: LEVELS.info,
153
- dir: LEVELS.info,
154
- table: LEVELS.info,
155
- // Verbose / Debug
156
- debug: LEVELS.debug,
157
- group: LEVELS.debug,
158
- groupCollapsed: LEVELS.debug,
159
- groupEnd: LEVELS.debug,
160
- time: LEVELS.debug,
161
- timeEnd: LEVELS.debug,
162
- timeLog: LEVELS.debug,
163
- trace: LEVELS.trace
164
- };
165
- for (const name of Object.keys(methodLevels)) {
166
- ORIGINALS[name] = ORIGINAL_CONSOLE[name] || (() => {
167
- });
168
- patched[name] = makeWrapper(name, methodLevels[name]);
169
- }
170
- ORIGINALS.assert = ORIGINAL_CONSOLE.assert || (() => {
171
- });
172
- patched.assert = makeAssertWrapper();
173
- return patched;
174
- }
175
- const ConsoleSilencer = {
176
- // Install the silencer (idempotent)
177
- install(options = {}) {
178
- if (INSTALLED) {
179
- if (options && typeof options.level !== "undefined") {
180
- this.setLevel(options.level, { persist: !!options.persist });
181
- }
182
- return this;
183
- }
184
- if (!GLOBAL || !ORIGINAL_CONSOLE) {
185
- INSTALLED = true;
186
- return this;
187
- }
188
- CURRENT_LEVEL = determineInitialLevel(options.level);
189
- const patched = buildPatchedConsole();
190
- GLOBAL.console = patched;
191
- INSTALLED = true;
192
- GLOBAL.MOJOConsoleSilencer = this;
193
- return this;
194
- },
195
- // Uninstall and restore the original console
196
- uninstall() {
197
- if (!INSTALLED) return this;
198
- try {
199
- GLOBAL.console = ORIGINAL_CONSOLE;
200
- } catch {
201
- }
202
- INSTALLED = false;
203
- return this;
204
- },
205
- // Set current level at runtime; accepts string or number
206
- // levels: 'silent' | 'error' | 'warn' | 'info' | 'debug' | 'trace'
207
- setLevel(level, { persist = false } = {}) {
208
- const parsed = parseLevel(level);
209
- if (parsed === null) return this;
210
- CURRENT_LEVEL = parsed;
211
- if (persist) {
212
- storeLogLevel(level);
213
- }
214
- return this;
215
- },
216
- // Get the current numeric level
217
- getLevel() {
218
- return CURRENT_LEVEL;
219
- },
220
- // Get the current level name (best-effort)
221
- getLevelName() {
222
- const entry = Object.entries(LEVELS).find(([, num]) => num === CURRENT_LEVEL);
223
- return entry ? entry[0] : null;
224
- },
225
- // Convenience helpers
226
- criticalOnly({ persist = false } = {}) {
227
- return this.setLevel("warn", { persist });
228
- },
229
- errorsOnly({ persist = false } = {}) {
230
- return this.setLevel("error", { persist });
231
- },
232
- silent({ persist = false } = {}) {
233
- return this.setLevel("silent", { persist });
234
- },
235
- verbose({ persist = false } = {}) {
236
- return this.setLevel(isDev ? "debug" : "info", { persist });
237
- },
238
- allowAll({ persist = false } = {}) {
239
- return this.setLevel("trace", { persist });
240
- },
241
- // Run a block with a temporary level, then restore
242
- withTemporaryLevel(level, fn) {
243
- const prev = CURRENT_LEVEL;
244
- const parsed = parseLevel(level);
245
- if (parsed === null || typeof fn !== "function") return fn?.();
246
- CURRENT_LEVEL = parsed;
247
- try {
248
- return fn();
249
- } finally {
250
- CURRENT_LEVEL = prev;
251
- }
252
- },
253
- // Expose levels map for consumers
254
- LEVELS
255
- };
256
- const installConsoleSilencer = (options) => ConsoleSilencer.install(options);
257
- class GroupSearchView extends SimpleSearchView {
258
- constructor(options = {}) {
259
- super({
260
- ...options,
261
- className: `group-search-view ${options.className || ""}`.trim()
262
- });
263
- this.showKind = options.showKind !== void 0 ? options.showKind : true;
264
- this.parentField = options.parentField || "parent";
265
- this.kindField = options.kindField || "kind";
266
- this.treeData = [];
267
- this.flattenedItems = [];
268
- this.showLines = options.showLines !== void 0 ? options.showLines : true;
269
- }
270
- /**
271
- * Build tree hierarchy from flat list
272
- */
273
- buildTreeHierarchy(items) {
274
- if (!items || items.length === 0) {
275
- return [];
276
- }
277
- const itemsById = /* @__PURE__ */ new Map();
278
- items.forEach((item) => {
279
- if (!itemsById.has(item.id)) {
280
- itemsById.set(item.id, {
281
- ...item,
282
- children: [],
283
- level: 0,
284
- hasChildren: false
285
- });
286
- }
287
- const parentObj = item[this.parentField];
288
- if (parentObj && parentObj.id && !itemsById.has(parentObj.id)) {
289
- itemsById.set(parentObj.id, {
290
- ...parentObj,
291
- children: [],
292
- level: 0,
293
- hasChildren: false
294
- });
295
- }
296
- });
297
- const rootItems = [];
298
- itemsById.forEach((treeItem, itemId) => {
299
- const originalItem = items.find((i3) => i3.id === itemId) || treeItem;
300
- const parentId = originalItem[this.parentField]?.id;
301
- if (parentId && itemsById.has(parentId)) {
302
- const parent = itemsById.get(parentId);
303
- parent.children.push(treeItem);
304
- parent.hasChildren = true;
305
- } else {
306
- rootItems.push(treeItem);
307
- }
308
- });
309
- const calculateLevels = (nodes, level = 0) => {
310
- nodes.forEach((node) => {
311
- node.level = level;
312
- if (node.children.length > 0) {
313
- node.children.sort((a12, b4) => (a12.name || "").localeCompare(b4.name || ""));
314
- calculateLevels(node.children, level + 1);
315
- }
316
- });
317
- };
318
- rootItems.sort((a12, b4) => (a12.name || "").localeCompare(b4.name || ""));
319
- calculateLevels(rootItems);
320
- return rootItems;
321
- }
322
- /**
323
- * Flatten tree structure for rendering
324
- * Tracks ancestor "last child" flags for proper line rendering
325
- */
326
- flattenTree(nodes, result = [], ancestorLastFlags = []) {
327
- nodes.forEach((node, index2) => {
328
- node._isLastChild = index2 === nodes.length - 1;
329
- node._ancestorLastFlags = [...ancestorLastFlags];
330
- const allAncestorsLast = ancestorLastFlags.every((flag) => flag);
331
- node._isLastDescendant = allAncestorsLast && node._isLastChild && (!node.children || node.children.length === 0);
332
- result.push(node);
333
- if (node.children && node.children.length > 0) {
334
- const newFlags = [...ancestorLastFlags, node._isLastChild];
335
- this.flattenTree(node.children, result, newFlags);
336
- }
337
- });
338
- return result;
339
- }
340
- /**
341
- * Second pass: compute which vertical lines should continue for each item
342
- *
343
- * Vertical line at segment s shows if there are more siblings coming at level s+1
344
- * We need to look ahead until we find an item at level s+1 or shallower
345
- */
346
- computeVerticalLines(flatList) {
347
- for (let i3 = 0; i3 < flatList.length; i3++) {
348
- const item = flatList[i3];
349
- item._continueVertical = [];
350
- for (let s2 = 0; s2 < item.level; s2++) {
351
- let foundSibling = false;
352
- for (let j2 = i3 + 1; j2 < flatList.length; j2++) {
353
- const futureItem = flatList[j2];
354
- if (futureItem.level === s2 + 1) {
355
- foundSibling = true;
356
- break;
357
- } else if (futureItem.level <= s2) {
358
- break;
359
- }
360
- }
361
- item._continueVertical[s2] = foundSibling;
362
- }
363
- }
364
- }
365
- /**
366
- * Update filtered items with tree structure
367
- */
368
- updateFilteredItems() {
369
- if (!this.collection) {
370
- this.filteredItems = [];
371
- this.treeData = [];
372
- this.flattenedItems = [];
373
- return;
374
- }
375
- const items = this.collection.toJSON();
376
- this.treeData = this.buildTreeHierarchy(items);
377
- this.flattenedItems = this.flattenTree(this.treeData);
378
- this.computeVerticalLines(this.flattenedItems);
379
- this.filteredItems = this.flattenedItems;
380
- this.updateResultsView();
381
- }
382
- /**
383
- * Get tree-specific item template
384
- */
385
- getDefaultItemTemplate() {
386
- return `
387
- <div class="tree-item-content">
388
- <div class="tree-item-name">{{name}}</div>
389
- {{#showKind}}
390
- <div class="tree-item-kind">{{kind}}</div>
391
- {{/showKind}}
392
- </div>
393
- `;
394
- }
395
- /**
396
- * Process item template with tree structure
397
- */
398
- processItemTemplate(item) {
399
- let content = this.itemTemplate;
400
- content = content.replace(/\{\{(\w+)\}\}/g, (match, prop) => {
401
- if (prop === "showKind") {
402
- return this.showKind ? "true" : "";
403
- }
404
- return this.getNestedValue(item, prop) || "";
405
- });
406
- if (this.showKind) {
407
- content = content.replace(/\{\{#showKind\}\}(.*?)\{\{\/showKind\}\}/gs, "$1");
408
- } else {
409
- content = content.replace(/\{\{#showKind\}\}.*?\{\{\/showKind\}\}/gs, "");
410
- }
411
- let lineSegments = "";
412
- if (this.showLines && item.level > 0) {
413
- for (let i3 = 0; i3 < item.level; i3++) {
414
- const isLastSegment = i3 === item.level - 1;
415
- if (isLastSegment) {
416
- const segClass = item._isLastChild ? "tree-seg tree-seg-last" : "tree-seg tree-seg-mid";
417
- lineSegments += `<span class="${segClass}"></span>`;
418
- } else {
419
- const continueVertical = item._continueVertical && item._continueVertical[i3];
420
- if (continueVertical) {
421
- lineSegments += `<span class="tree-seg tree-seg-vert"></span>`;
422
- } else {
423
- lineSegments += `<span class="tree-seg"></span>`;
424
- }
425
- }
426
- }
427
- }
428
- const hasChildren = item.hasChildren ? " has-children" : "";
429
- const isLastChild = item._isLastChild ? " is-last-child" : "";
430
- return `
431
- <div class="tree-item-wrapper${hasChildren}${isLastChild}" data-tree-level="${item.level}">
432
- <div class="tree-lines">
433
- ${lineSegments}
434
- </div>
435
- <div class="tree-item-body flex-grow-1">
436
- ${content}
437
- </div>
438
- </div>
439
- `;
440
- }
441
- /**
442
- * Get all root items
443
- */
444
- getRootItems() {
445
- return this.treeData;
446
- }
447
- /**
448
- * Get children of a specific node
449
- */
450
- getNodeChildren(nodeId) {
451
- const findNode = (nodes, targetId) => {
452
- for (const node of nodes) {
453
- if (node.id === targetId) {
454
- return node.children;
455
- }
456
- if (node.children.length > 0) {
457
- const found = findNode(node.children, targetId);
458
- if (found) return found;
459
- }
460
- }
461
- return null;
462
- };
463
- return findNode(this.treeData, nodeId) || [];
464
- }
465
- }
466
- class Sidebar extends View {
467
- constructor(options = {}) {
468
- super({
469
- tagName: "nav",
470
- className: "sidebar",
471
- id: "sidebar",
472
- ...options
473
- });
474
- this.menus = /* @__PURE__ */ new Map();
475
- this.activeMenuName = null;
476
- this.currentRoute = null;
477
- this.showToggle = options.showToggle;
478
- this.isCollapsed = false;
479
- this.sidebarTheme = options.theme || "sidebar-light";
480
- this.customView = null;
481
- if (this.options.groupHeader) this.groupHeader = this.options.groupHeader;
482
- this.groupSelectorMode = options.groupSelectorMode || "inline";
483
- this.groupSelectorDialog = null;
484
- if (this.sidebarTheme) {
485
- this.addClass(this.sidebarTheme);
486
- }
487
- this.initializeMenus(options);
488
- this.setupRouteListeners();
489
- if (options.autoCollapseMobile !== false) {
490
- this.setupResponsiveBehavior();
491
- }
492
- }
493
- groupHeader = `
494
- {{#group.parent}}
495
- <div class="sidebar-parent-bar" data-action="select-group-parent">
496
- <div class="parent-info">
497
- <span class="parent-label">{{group.parent.kind}}:</span>
498
- <span class="parent-name collapsed-hidden">{{group.parent.name}}</span>
499
- </div>
500
- <i class="bi bi-chevron-down parent-expand collapsed-hidden"></i>
501
- </div>
502
- {{/group.parent}}
503
- <div class="sidebar-selected-group-row" data-action="show-group-search">
504
- <div class="selected-group-info">
505
- <div class='selected-group-name collapsed-hidden'>{{group.name}}</div>
506
- <div class='selected-group-meta collapsed-hidden'>
507
- <span class="selected-group-kind">{{group.kind}}</span>
508
- </div>
509
- </div>
510
- <i class="bi bi-chevron-down selected-group-chevron collapsed-hidden"></i>
511
- </div>
512
- `;
513
- /**
514
- * Initialize sidebar and auto-switch to correct menu based on current route
515
- */
516
- async onInit() {
517
- await super.onInit();
518
- const app = this.getApp();
519
- const router = app?.router;
520
- if (router) {
521
- const currentPath = router.getCurrentPath();
522
- if (currentPath) {
523
- this.autoSwitchToMenuForRoute(currentPath);
524
- }
525
- }
526
- this.initializeTooltips();
527
- this.searchView = new GroupSearchView({
528
- noAppend: true,
529
- showExitButton: true,
530
- headerText: "Select Group",
531
- containerId: "sidebar-search-container",
532
- Collection: GroupList,
533
- itemTemplate: `
534
- <div class="p-3 border-bottom">
535
- <div class="fw-semibold text-dark">{{name}}</div>
536
- <small class="text-muted">#{{id}} {{kind}}</small>
537
- </div>
538
- `
539
- });
540
- this.addChild(this.searchView);
541
- this.searchView.on("item:selected", (evt) => {
542
- console.log(evt);
543
- this.getApp().setActiveGroup(evt.model);
544
- });
545
- this.searchView.on("exit", (item) => {
546
- console.log(item);
547
- this.hideGroupSearch();
548
- });
549
- }
550
- showGroupSearch() {
551
- if (this.groupSelectorMode === "dialog") {
552
- this.showGroupSearchDialog();
553
- } else {
554
- this.setClass("sidebar");
555
- this.showSearch = true;
556
- this.render();
557
- }
558
- }
559
- hideGroupSearch() {
560
- if (this.groupSelectorMode === "dialog") {
561
- if (this.groupSelectorDialog) {
562
- this.groupSelectorDialog.hide();
563
- }
564
- } else {
565
- this.setClass("sidebar");
566
- this.showSearch = false;
567
- this.render();
568
- }
569
- }
570
- onActionShowGroupSearch() {
571
- this.showGroupSearch();
572
- }
573
- async onActionSelectGroupParent() {
574
- const group = this.getApp().activeGroup;
575
- const result = await Dialog.confirm(`Are you sure you want to navigate to the '${group.get("parent.name")}'?`);
576
- if (result) {
577
- this.getApp().showLoading();
578
- let parent = new Group({ id: group.get("parent.id") });
579
- await parent.fetch();
580
- this.getApp().setActiveGroup(parent);
581
- this.getApp().hideLoading();
582
- }
583
- }
584
- /**
585
- * Show group selector in a dialog (like TopNav)
586
- */
587
- async showGroupSearchDialog() {
588
- const collection = new GroupList();
589
- const searchView = new GroupSearchView({
590
- Collection: GroupList,
591
- collection,
592
- // Pass the collection instance
593
- searchFields: ["name"],
594
- headerText: null,
595
- searchPlaceholder: "Search groups...",
596
- headerIcon: null,
597
- maxHeight: Math.min(600, window.innerHeight - 200),
598
- showExitButton: false,
599
- showKind: true,
600
- // Show kind badges (default: true)
601
- parentField: "parent",
602
- // Field containing parent object
603
- kindField: "kind",
604
- // Field containing kind/type
605
- autoExpandRoot: true,
606
- // Auto-expand root items (default: true)
607
- autoExpandAll: false,
608
- // Auto-expand all nodes (default: false)
609
- indentSize: 20,
610
- // Pixels per level (default: 20)
611
- showLines: true
612
- });
613
- this.groupSelectorDialog = new Dialog({
614
- body: searchView,
615
- size: "md",
616
- header: null,
617
- noBodyPadding: true,
618
- scrollable: false,
619
- buttons: [],
620
- closeButton: true
621
- });
622
- searchView.on("item:selected", (evt) => {
623
- console.log(evt);
624
- this.getApp().setActiveGroup(evt.model);
625
- if (this.groupSelectorDialog) {
626
- this.groupSelectorDialog.hide();
627
- }
628
- });
629
- this.groupSelectorDialog.on("hidden", () => {
630
- this.groupSelectorDialog.destroy();
631
- this.groupSelectorDialog = null;
632
- });
633
- await this.groupSelectorDialog.render(true, document.body);
634
- this.groupSelectorDialog.show();
635
- }
636
- /**
637
- * Find and switch to the menu that contains the given route
638
- */
639
- autoSwitchToMenuForRoute(route) {
640
- for (const [menuName, menuConfig] of this.menus) {
641
- if (menuConfig.groupKind && !this.getApp().activeGroup)
642
- continue;
643
- if (this.menuContainsRoute(menuConfig, route)) {
644
- this._setActiveMenu(menuName);
645
- this.currentRoute = route;
646
- this.clearAllActiveStates();
647
- this.setActiveItemByRoute(route);
648
- this.render();
649
- console.log(`Auto-switched to menu '${menuName}' for route '${route}'`);
650
- this.emit("menu-auto-switched", {
651
- menuName,
652
- route,
653
- config: menuConfig,
654
- sidebar: this
655
- });
656
- return true;
657
- }
658
- }
659
- return false;
660
- }
661
- /**
662
- * Clear active state from all menu items in all menus
663
- */
664
- clearAllActiveStates() {
665
- for (const [menuName, menuConfig] of this.menus) {
666
- for (const item of menuConfig.items || []) {
667
- item.active = false;
668
- if (item.children) {
669
- for (const child of item.children) {
670
- child.active = false;
671
- }
672
- }
673
- }
674
- }
675
- }
676
- /**
677
- * Set active state for item matching the given route
678
- */
679
- setActiveItemByRoute(route) {
680
- const normalizeRoute = (r3) => {
681
- if (!r3) return "/";
682
- const decoded = decodeURIComponent(r3);
683
- return decoded.startsWith("/") ? decoded : `/${decoded}`;
684
- };
685
- const targetRoute = normalizeRoute(route);
686
- for (const [menuName, menuConfig] of this.menus) {
687
- if (menuConfig.groupKind && !this.getApp().activeGroup)
688
- continue;
689
- for (const item of menuConfig.items || []) {
690
- if (item.route) {
691
- const itemRoute = normalizeRoute(item.route);
692
- if (this.routesMatch(targetRoute, itemRoute)) {
693
- item.active = true;
694
- this.activeMenuItem = item;
695
- return true;
696
- }
697
- }
698
- if (item.children) {
699
- for (const child of item.children) {
700
- if (child.route) {
701
- const childRoute = normalizeRoute(child.route);
702
- if (this.routesMatch(targetRoute, childRoute)) {
703
- child.active = true;
704
- item.active = true;
705
- return true;
706
- }
707
- }
708
- }
709
- }
710
- }
711
- }
712
- return false;
713
- }
714
- /**
715
- * Check if a menu contains a specific route in its items or children
716
- */
717
- menuContainsRoute(menuConfig, route) {
718
- const normalizeRoute = (r3) => {
719
- if (!r3) return "/";
720
- const decoded = decodeURIComponent(r3);
721
- return decoded.startsWith("/") ? decoded : `/${decoded}`;
722
- };
723
- const targetRoute = normalizeRoute(route);
724
- for (const item of menuConfig.items || []) {
725
- if (item.route) {
726
- const itemRoute = normalizeRoute(item.route);
727
- if (this.routesMatch(targetRoute, itemRoute)) {
728
- return true;
729
- }
730
- }
731
- if (item.children) {
732
- for (const child of item.children) {
733
- if (child.route) {
734
- const childRoute = normalizeRoute(child.route);
735
- if (this.routesMatch(targetRoute, childRoute)) {
736
- return true;
737
- }
738
- }
739
- }
740
- }
741
- }
742
- return false;
743
- }
744
- /**
745
- * Check if two routes match (using same logic as isItemActive)
746
- */
747
- routesMatch(currentRoute, itemRoute) {
748
- return this.getApp().router.doRoutesMatch(currentRoute, itemRoute);
749
- }
750
- getTemplate() {
751
- if (this.customView) {
752
- return '<div class="sidebar-container" id="sidebar-custom-view-container"></div>';
753
- }
754
- if (this.showSearch) return this.getSearchTemplate();
755
- return this.getMenuTemplate();
756
- }
757
- getSearchTemplate() {
758
- return `
759
- <div class="sidebar-container" id="sidebar-search-container">
760
- </div>
761
- `;
762
- }
763
- getMenuTemplate() {
764
- return `
765
- <div class="sidebar-container">
766
- {{#data.currentMenu}}
767
- <!-- Header -->
768
- {{#header}}
769
- <div class="sidebar-header">
770
- {{{header}}}
771
- {{#showToggle}}
772
- <button class="sidebar-toggle" data-action="toggle-sidebar"
773
- aria-label="Toggle Sidebar">
774
- <i class="bi bi-chevron-left toggle-icon"></i>
775
- <i class="bi bi-chevron-right toggle-icon"></i>
776
- </button>
777
- {{/showToggle}}
778
- </div>
779
- {{/header}}
780
-
781
- <!-- Navigation Items -->
782
- <div class="sidebar-body">
783
- <ul class="nav nav-pills flex-column sidebar-nav" id="sidebar-nav-menu">
784
- {{#items}}
785
- {{>nav-item}}
786
- {{/items}}
787
- </ul>
788
- </div>
789
-
790
- <!-- Footer -->
791
- {{#footer}}
792
- <div class="sidebar-footer">
793
- {{{footer}}}
794
- </div>
795
- {{/footer}}
796
- {{/data.currentMenu}}
797
-
798
- {{^data.currentMenu}}
799
- <div class="sidebar-empty">
800
- <p class="text-danger text-center">No menu configured</p>
801
- </div>
802
- {{/data.currentMenu}}
803
- </div>
804
- `;
805
- }
806
- /**
807
- * Get template partials for rendering
808
- */
809
- getPartials() {
810
- return {
811
- "nav-item": `
812
- {{#isDivider}}
813
- {{>nav-divider}}
814
- {{/isDivider}}
815
- {{#isSpacer}}
816
- {{>nav-spacer}}
817
- {{/isSpacer}}
818
- {{#isLabel}}
819
- {{>nav-label}}
820
- {{/isLabel}}
821
-
822
- {{^isDivider}}
823
- {{^isSpacer}}
824
- {{^isLabel}}
825
- <li class="nav-item">
826
- {{#hasChildren}}
827
- <!-- Item with submenu -->
828
- <a class="nav-link {{#active}}active{{/active}} has-children collapsed"
829
- data-bs-toggle="collapse"
830
- href="#collapse-{{id}}"
831
- role="button"
832
- aria-expanded="{{#active}}true{{/active}}{{^active}}false{{/active}}"
833
- data-action="toggle-submenu">
834
- {{#icon}}<i class="{{icon}} me-2"></i>{{/icon}}
835
- <span class="nav-text">{{text}}</span>
836
- {{#badge}}
837
- <span class="{{badge.class}} ms-auto">{{badge.text}}</span>
838
- {{/badge}}
839
- <i class="bi bi-chevron-down nav-arrow ms-auto"></i>
840
- </a>
841
- <div class="collapse {{#active}}show{{/active}}" id="collapse-{{id}}" data-bs-parent="#sidebar-nav-menu">
842
- <ul class="nav flex-column nav-submenu">
843
- {{#children}}
844
- <li class="nav-item">
845
- <a class="nav-link {{#active}}active{{/active}}"
846
- {{#action}}data-action="{{action}}"{{/action}}
847
- {{#href}}href="{{href}}"{{/href}}>
848
- {{#icon}}<i class="{{icon}} me-2"></i>{{/icon}}
849
- <span class="nav-text">{{text}}</span>
850
- {{#badge}}
851
- <span class="{{badge.class}} ms-auto">{{badge.text}}</span>
852
- {{/badge}}
853
- </a>
854
- </li>
855
- {{/children}}
856
- </ul>
857
- </div>
858
- {{/hasChildren}}
859
- {{^hasChildren}}
860
- <!-- Simple item -->
861
- <a class="nav-link {{#active}}active{{/active}} {{#disabled}}disabled{{/disabled}}"
862
- {{#action}}{{^disabled}}data-action="{{action}}"{{/disabled}}{{/action}}
863
- {{#href}}{{^disabled}}href="{{href}}"{{/disabled}}{{/href}}>
864
- {{#icon}}<i class="{{icon}} me-2"></i>{{/icon}}
865
- <span class="nav-text">{{text}}</span>
866
- {{#badge}}
867
- <span class="{{badge.class}} ms-auto">{{badge.text}}</span>
868
- {{/badge}}
869
- </a>
870
- {{/hasChildren}}
871
- </li>
872
- {{/isLabel}}
873
- {{/isSpacer}}
874
- {{/isDivider}}
875
- `,
876
- "nav-divider": `
877
- <li class="nav-divider-item">
878
- <hr class="nav-divider-line">
879
- </li>
880
- `,
881
- "nav-spacer": `
882
- <li class="nav-spacer-item"></li>
883
- `,
884
- "nav-label": `
885
- <li class="nav-item {{className}}">
886
- <div class="nav-text px-3">{{text}}</div>
887
- </li>
888
- `
889
- };
890
- }
891
- getGroupHeader() {
892
- return this.groupHeader;
893
- }
894
- /**
895
- * Add a menu configuration
896
- */
897
- addMenu(name, config) {
898
- if (config.groupKind && !config.header) {
899
- config.header = this.getGroupHeader();
900
- }
901
- this.menus.set(name, {
902
- name,
903
- groupKind: config.groupKind || null,
904
- header: config.header || null,
905
- footer: config.footer || null,
906
- items: config.items || [],
907
- data: config.data || {},
908
- className: config.className || "sidebar sidebar-dark"
909
- });
910
- if (!this.activeMenuName) {
911
- this._setActiveMenu(name);
912
- }
913
- return this;
914
- }
915
- _setActiveMenu(name) {
916
- this.showSearch = false;
917
- this.activeMenuName = name;
918
- const config = this.getCurrentMenuConfig();
919
- if (config.className) {
920
- this.setClass(config.className);
921
- } else {
922
- this.setClass("sidebar");
923
- }
924
- }
925
- /**
926
- * Set the active menu
927
- */
928
- async setActiveMenu(name) {
929
- if (!this.menus.has(name)) {
930
- console.warn(`Menu '${name}' not found`);
931
- return this;
932
- }
933
- const menuConfig = this.menus.get(name);
934
- if (menuConfig.groupKind) {
935
- this.lastGroupMenu = menuConfig;
936
- if (!this.getApp().activeGroup) {
937
- this.showGroupSearch();
938
- return;
939
- }
940
- }
941
- this._setActiveMenu(name);
942
- await this.render();
943
- this.emit("menu-changed", {
944
- menuName: name,
945
- config: menuConfig,
946
- sidebar: this
947
- });
948
- return this;
949
- }
950
- getGroupMenu(group) {
951
- if (!group) {
952
- console.warn("No group provided");
953
- return null;
954
- }
955
- let targetMenu = this.lastGroupMenu;
956
- let anyGroupMenu = null;
957
- if (group._.kind) {
958
- for (const [menuName, menuConfig] of this.menus) {
959
- const matches = this._groupKindMatches(menuConfig.groupKind, group._.kind);
960
- if (matches) {
961
- targetMenu = menuConfig;
962
- break;
963
- } else if (menuConfig.groupKind === "any") {
964
- anyGroupMenu = menuConfig;
965
- }
966
- }
967
- }
968
- if (!targetMenu) {
969
- return anyGroupMenu;
970
- }
971
- return targetMenu;
972
- }
973
- /**
974
- * Check if a groupKind matches the group's kind
975
- * Supports both single string and array of strings
976
- * @param {string|string[]} groupKind - Single kind or array of kinds
977
- * @param {string} kind - The group's kind to match
978
- * @returns {boolean} True if matches
979
- */
980
- _groupKindMatches(groupKind, kind) {
981
- if (!groupKind || !kind) return false;
982
- if (Array.isArray(groupKind)) {
983
- return groupKind.includes(kind);
984
- }
985
- return groupKind === kind;
986
- }
987
- showMenuForGroup(group) {
988
- if (!group) {
989
- console.warn("No group provided");
990
- return;
991
- }
992
- let targetMenu = this.getGroupMenu(group);
993
- if (!targetMenu) {
994
- console.warn(`No menu found for group kind: ${group.kind}`);
995
- return;
996
- }
997
- this._setActiveMenu(targetMenu.name);
998
- this.render();
999
- this.emit("menu-changed", {
1000
- menuName: targetMenu.name,
1001
- config: targetMenu,
1002
- sidebar: this
1003
- });
1004
- return this;
1005
- }
1006
- /**
1007
- * Get menu configuration
1008
- */
1009
- getMenuConfig(name) {
1010
- return this.menus.get(name) || null;
1011
- }
1012
- /**
1013
- * Get current active menu configuration
1014
- */
1015
- getCurrentMenuConfig() {
1016
- return this.activeMenuName ? this.menus.get(this.activeMenuName) : null;
1017
- }
1018
- /**
1019
- * Update menu configuration
1020
- */
1021
- updateMenu(name, updates) {
1022
- const menu = this.menus.get(name);
1023
- if (!menu) {
1024
- console.warn(`Menu '${name}' not found`);
1025
- return this;
1026
- }
1027
- Object.assign(menu, updates);
1028
- if (this.activeMenuName === name) {
1029
- this.render();
1030
- }
1031
- return this;
1032
- }
1033
- /**
1034
- * Remove a menu
1035
- */
1036
- removeMenu(name) {
1037
- this.menus.delete(name);
1038
- if (this.activeMenuName === name) {
1039
- const remainingMenus = Array.from(this.menus.keys());
1040
- this.activeMenuName = remainingMenus.length > 0 ? remainingMenus[0] : null;
1041
- this.render();
1042
- }
1043
- return this;
1044
- }
1045
- /**
1046
- * Get view data for template rendering
1047
- */
1048
- async onBeforeRender() {
1049
- const currentMenu = this.getCurrentMenuConfig();
1050
- if (!currentMenu) {
1051
- return { currentMenu: null };
1052
- }
1053
- let subData = {
1054
- version: this.getApp().version || null,
1055
- group: this.getApp().activeGroup || null,
1056
- user: this.getApp.activeUser || null
1057
- };
1058
- this.data = {
1059
- currentMenu: {
1060
- header: this.renderTemplateString(currentMenu.header || "", subData),
1061
- footer: this.renderTemplateString(currentMenu.footer || "", subData),
1062
- items: this.processNavItems(currentMenu.items, currentMenu.groupKind),
1063
- data: currentMenu.data,
1064
- showToggle: this.showToggle
1065
- }
1066
- };
1067
- }
1068
- async onAfterRender() {
1069
- if (this.isCollapsedState()) {
1070
- setTimeout(() => this.initializeTooltips(), 50);
1071
- } else {
1072
- this.destroyTooltips();
1073
- }
1074
- }
1075
- setCustomView(view) {
1076
- if (this.customView) {
1077
- this.removeChild(this.customView.id);
1078
- }
1079
- this.customView = view;
1080
- if (view) {
1081
- view.containerId = "sidebar-custom-view-container";
1082
- this.addChild(view);
1083
- }
1084
- this.render();
1085
- return this;
1086
- }
1087
- clearCustomView() {
1088
- if (this.customView) {
1089
- this.removeChild(this.customView.id);
1090
- this.customView = null;
1091
- }
1092
- this.render();
1093
- return this;
1094
- }
1095
- /**
1096
- * Process navigation items - add IDs, active states, and proper hrefs
1097
- */
1098
- processNavItems(items, groupKind) {
1099
- const app = this.getApp();
1100
- const activeUser = app?.activeUser;
1101
- const activeGroup = app?.activeGroup;
1102
- const updateRouteWithGroup = (route) => {
1103
- let normalizedRoute = route;
1104
- if (route.startsWith("/") && !route.includes("?")) {
1105
- const pageName = route.substring(1) || "home";
1106
- normalizedRoute = `?page=${pageName}`;
1107
- }
1108
- if (groupKind && activeGroup && activeGroup.id) {
1109
- const separator = normalizedRoute.includes("?") ? "&" : "?";
1110
- return `${normalizedRoute}${separator}group=${activeGroup.id}`;
1111
- }
1112
- return normalizedRoute;
1113
- };
1114
- return items.map((item, index2) => {
1115
- if (item === "" || typeof item === "object" && item.divider) {
1116
- return {
1117
- isDivider: true,
1118
- id: `divider-${index2}`
1119
- };
1120
- }
1121
- if (typeof item === "object" && item.spacer) {
1122
- return {
1123
- isSpacer: true,
1124
- id: `spacer-${index2}`
1125
- };
1126
- }
1127
- const processedItem = { ...item };
1128
- if (processedItem.permissions) {
1129
- if (!activeUser || !activeUser.hasPermission(processedItem.permissions)) {
1130
- return null;
1131
- }
1132
- }
1133
- if (processedItem.requiresGroupKind) {
1134
- const groupKind2 = activeGroup?._.kind || activeGroup?.kind;
1135
- if (!groupKind2 || !this._groupKindMatches(processedItem.requiresGroupKind, groupKind2)) {
1136
- return null;
1137
- }
1138
- }
1139
- if (processedItem.kind === "label") {
1140
- processedItem.isLabel = true;
1141
- if (!processedItem.id) {
1142
- processedItem.id = `nav-label-${index2}`;
1143
- }
1144
- return processedItem;
1145
- }
1146
- if (!processedItem.id) {
1147
- processedItem.id = `nav-${index2}`;
1148
- }
1149
- if (processedItem.route) {
1150
- processedItem.href = updateRouteWithGroup(processedItem.route);
1151
- } else if (processedItem.page) {
1152
- const baseRoute = processedItem.page.startsWith("/") ? processedItem.page : `/${processedItem.page}`;
1153
- processedItem.href = updateRouteWithGroup(baseRoute);
1154
- processedItem.route = processedItem.href;
1155
- }
1156
- if (processedItem.children) {
1157
- processedItem.children = processedItem.children.map((child) => {
1158
- const processedChild = { ...child };
1159
- if (processedChild.permissions && activeUser) {
1160
- if (!activeUser.hasPermission(processedChild.permissions)) {
1161
- return null;
1162
- }
1163
- }
1164
- if (processedChild.requiresGroupKind) {
1165
- const groupKind2 = activeGroup?._.kind || activeGroup?.kind;
1166
- if (!groupKind2 || !this._groupKindMatches(processedChild.requiresGroupKind, groupKind2)) {
1167
- return null;
1168
- }
1169
- }
1170
- if (processedChild.route) {
1171
- processedChild.href = updateRouteWithGroup(processedChild.route);
1172
- } else if (processedChild.page) {
1173
- const baseRoute = processedChild.page.startsWith("/") ? processedChild.page : `/${processedChild.page}`;
1174
- processedChild.href = updateRouteWithGroup(baseRoute);
1175
- processedChild.route = processedChild.href;
1176
- }
1177
- return processedChild;
1178
- }).filter((child) => child !== null);
1179
- processedItem.hasChildren = !!(processedItem.children && processedItem.children.length > 0);
1180
- } else {
1181
- processedItem.hasChildren = false;
1182
- }
1183
- return processedItem;
1184
- }).filter((item) => item !== null);
1185
- }
1186
- /**
1187
- * Check if navigation item should be active (similar to TopNav)
1188
- */
1189
- isItemActive(item) {
1190
- if (!item.route || !this.currentRoute) {
1191
- return false;
1192
- }
1193
- const normalizeRoute = (route) => {
1194
- if (!route) return "/";
1195
- const decoded = decodeURIComponent(route);
1196
- return decoded.startsWith("/") ? decoded : `/${decoded}`;
1197
- };
1198
- const itemRoute = normalizeRoute(item.route);
1199
- const currentRoute = normalizeRoute(this.currentRoute);
1200
- if (itemRoute === "/" && currentRoute === "/") {
1201
- return true;
1202
- }
1203
- if (itemRoute !== "/" && currentRoute !== "/") {
1204
- return currentRoute.startsWith(itemRoute) || currentRoute === itemRoute;
1205
- }
1206
- return false;
1207
- }
1208
- /**
1209
- * Update active item based on current route (like TopNav)
1210
- */
1211
- async updateActiveItem(route) {
1212
- this.currentRoute = route;
1213
- this.clearAllActiveStates();
1214
- this.setActiveItemByRoute(route);
1215
- await this.render();
1216
- return this;
1217
- }
1218
- /**
1219
- * Action handler: Toggle submenu
1220
- */
1221
- async handleActionToggleSubmenu(event, element) {
1222
- const arrow = element.querySelector(".nav-arrow");
1223
- if (arrow) {
1224
- arrow.classList.toggle("rotated");
1225
- }
1226
- }
1227
- /**
1228
- * Action handler: Toggle sidebar collapsed/expanded state
1229
- */
1230
- async handleActionToggleSidebar(event, element) {
1231
- this.toggleSidebar();
1232
- }
1233
- onActionShowGroupMenu(action, event, el) {
1234
- this.setActiveMenu("group_default");
1235
- return false;
1236
- }
1237
- async onActionDefault(action, event, el) {
1238
- const config = this.getCurrentMenuConfig();
1239
- if (!config) return;
1240
- const findAndExecuteHandler = (items) => {
1241
- for (const item of items) {
1242
- if (item.action == action && item.handler) {
1243
- item.handler(action, event, el, this.getApp());
1244
- return true;
1245
- }
1246
- if (item.children && item.children.length > 0) {
1247
- if (findAndExecuteHandler(item.children)) {
1248
- return true;
1249
- }
1250
- }
1251
- }
1252
- return false;
1253
- };
1254
- return findAndExecuteHandler(config.items);
1255
- }
1256
- /**
1257
- * Get all menu names
1258
- */
1259
- getMenuNames() {
1260
- return Array.from(this.menus.keys());
1261
- }
1262
- /**
1263
- * Check if menu exists
1264
- */
1265
- hasMenu(name) {
1266
- return this.menus.has(name);
1267
- }
1268
- /**
1269
- * Clear all menus
1270
- */
1271
- clearMenus() {
1272
- this.menus.clear();
1273
- this.activeMenuName = null;
1274
- this.render();
1275
- return this;
1276
- }
1277
- /**
1278
- * Set data for current menu
1279
- */
1280
- setMenuData(data) {
1281
- const currentMenu = this.getCurrentMenuConfig();
1282
- if (currentMenu) {
1283
- currentMenu.data = { ...currentMenu.data, ...data };
1284
- this.render();
1285
- }
1286
- return this;
1287
- }
1288
- /**
1289
- * Get data for current menu
1290
- */
1291
- getMenuData() {
1292
- const currentMenu = this.getCurrentMenuConfig();
1293
- return currentMenu ? currentMenu.data : {};
1294
- }
1295
- /**
1296
- * Setup listeners for route change events (like TopNav)
1297
- */
1298
- setupRouteListeners() {
1299
- const app = this.getApp();
1300
- if (app && app.events) {
1301
- app.events.on(["page:showing"], (data) => {
1302
- this.onRouteChanged(data);
1303
- });
1304
- app.events.on("group:changed", (data) => {
1305
- this.showMenuForGroup(data.group);
1306
- });
1307
- app.events.on("portal:user-changed", (data) => {
1308
- this.render();
1309
- });
1310
- }
1311
- }
1312
- /**
1313
- * Handle route changed event - auto-switch menu and update active item
1314
- */
1315
- onRouteChanged(data) {
1316
- if (data.page && data.page.route) {
1317
- const route = data.page.route;
1318
- if (this.activeMenuItem && this.routesMatch(route, this.activeMenuItem.route)) {
1319
- return;
1320
- }
1321
- const switchedMenu = this.autoSwitchToMenuForRoute(route);
1322
- if (!switchedMenu) {
1323
- this.clearAllActiveStates();
1324
- this.setActiveItemByRoute(route);
1325
- this.updateActiveItem(route);
1326
- }
1327
- if (switchedMenu) {
1328
- console.log(`Route changed to '${route}', auto-switched menu`);
1329
- }
1330
- }
1331
- }
1332
- /**
1333
- * Toggle sidebar between collapsed and expanded states
1334
- */
1335
- toggleSidebar() {
1336
- const portalContainer = document.querySelector(".portal-container");
1337
- if (!portalContainer) return;
1338
- this.hideAllTooltips();
1339
- const isCurrentlyCollapsed = portalContainer.classList.contains("collapse-sidebar");
1340
- const isCurrentlyHidden = portalContainer.classList.contains("hide-sidebar");
1341
- if (isCurrentlyHidden) {
1342
- portalContainer.classList.remove("hide-sidebar");
1343
- this.isCollapsed = false;
1344
- this.destroyTooltips();
1345
- } else if (isCurrentlyCollapsed) {
1346
- portalContainer.classList.remove("collapse-sidebar");
1347
- this.isCollapsed = false;
1348
- this.destroyTooltips();
1349
- } else {
1350
- portalContainer.classList.add("collapse-sidebar");
1351
- this.isCollapsed = true;
1352
- setTimeout(() => this.initializeTooltips(), 150);
1353
- }
1354
- return this;
1355
- }
1356
- /**
1357
- * Set sidebar state programmatically
1358
- */
1359
- setSidebarState(state) {
1360
- const portalContainer = document.querySelector(".portal-container");
1361
- if (!portalContainer) return this;
1362
- portalContainer.classList.remove("collapse-sidebar", "hide-sidebar");
1363
- switch (state) {
1364
- case "collapsed":
1365
- portalContainer.classList.add("collapse-sidebar");
1366
- this.isCollapsed = true;
1367
- break;
1368
- case "hidden":
1369
- portalContainer.classList.add("hide-sidebar");
1370
- this.isCollapsed = false;
1371
- break;
1372
- case "normal":
1373
- default:
1374
- this.isCollapsed = false;
1375
- break;
1376
- }
1377
- if (this.isCollapsed) {
1378
- this.hideAllTooltips();
1379
- setTimeout(() => this.initializeTooltips(), 100);
1380
- } else {
1381
- this.destroyTooltips();
1382
- }
1383
- return this;
1384
- }
1385
- /**
1386
- * Initialize tooltips for nav items when sidebar is collapsed
1387
- */
1388
- initializeTooltips() {
1389
- this.destroyTooltips();
1390
- if (!this.isCollapsedState()) {
1391
- return this;
1392
- }
1393
- const navLinks = this.element.querySelectorAll(".sidebar-nav .nav-link");
1394
- navLinks.forEach((link) => {
1395
- const navText = link.querySelector(".nav-text");
1396
- if (navText && navText.textContent.trim()) {
1397
- const tooltipText = navText.textContent.trim();
1398
- link.setAttribute("data-bs-toggle", "tooltip");
1399
- link.setAttribute("data-bs-placement", "right");
1400
- link.setAttribute("data-bs-title", tooltipText);
1401
- link.setAttribute("data-bs-container", "body");
1402
- if (window.bootstrap && window.bootstrap.Tooltip) {
1403
- const theme = link.getAttribute("data-tooltip-theme");
1404
- const size = link.getAttribute("data-tooltip-size");
1405
- let customClass = "";
1406
- if (theme) customClass += `tooltip-${theme} `;
1407
- if (size) customClass += `tooltip-${size}`;
1408
- const tooltipOptions = {
1409
- placement: "right",
1410
- container: "body",
1411
- trigger: "hover",
1412
- delay: { show: 500, hide: 100 },
1413
- fallbackPlacements: ["top", "bottom", "left"]
1414
- };
1415
- const trimmedClass = customClass.trim();
1416
- if (trimmedClass) {
1417
- tooltipOptions.customClass = trimmedClass;
1418
- }
1419
- const tooltip = new window.bootstrap.Tooltip(link, tooltipOptions);
1420
- link._tooltipInstance = tooltip;
1421
- link.addEventListener("click", () => {
1422
- tooltip.hide();
1423
- });
1424
- link.addEventListener("blur", () => {
1425
- tooltip.hide();
1426
- });
1427
- }
1428
- }
1429
- });
1430
- this.addTooltipHideListeners();
1431
- return this;
1432
- }
1433
- destroyTooltips() {
1434
- this.removeTooltipHideListeners();
1435
- const navLinks = this.element.querySelectorAll('.sidebar-nav .nav-link[data-bs-toggle="tooltip"]');
1436
- navLinks.forEach((link) => {
1437
- const tooltipInstance = link._tooltipInstance || window.bootstrap?.Tooltip?.getInstance(link);
1438
- if (tooltipInstance) {
1439
- tooltipInstance.hide();
1440
- tooltipInstance.dispose();
1441
- }
1442
- delete link._tooltipInstance;
1443
- link.removeAttribute("data-bs-toggle");
1444
- link.removeAttribute("data-bs-placement");
1445
- link.removeAttribute("data-bs-title");
1446
- link.removeAttribute("data-bs-container");
1447
- });
1448
- return this;
1449
- }
1450
- /**
1451
- * Get current sidebar state
1452
- */
1453
- getSidebarState() {
1454
- const portalContainer = document.querySelector(".portal-container");
1455
- if (!portalContainer) return "normal";
1456
- if (portalContainer.classList.contains("hide-sidebar")) {
1457
- return "hidden";
1458
- } else if (portalContainer.classList.contains("collapse-sidebar")) {
1459
- return "collapsed";
1460
- } else {
1461
- return "normal";
1462
- }
1463
- }
1464
- /**
1465
- * Check if sidebar is collapsed
1466
- */
1467
- isCollapsedState() {
1468
- return this.getSidebarState() === "collapsed";
1469
- }
1470
- /**
1471
- * Enable/disable toggle button
1472
- */
1473
- setToggleEnabled(enabled) {
1474
- this.showToggle = enabled;
1475
- this.render();
1476
- return this;
1477
- }
1478
- /**
1479
- * Initialize menus from options
1480
- */
1481
- initializeMenus(options) {
1482
- if (options.menus) {
1483
- for (const menu of options.menus) {
1484
- this.addMenu(menu.name, menu);
1485
- }
1486
- } else if (options.menu) {
1487
- options.menu.name = options.menu.name || "default";
1488
- this.addMenu(options.menu.name, options.menu);
1489
- }
1490
- }
1491
- /**
1492
- * Add global listeners to hide tooltips when needed
1493
- */
1494
- addTooltipHideListeners() {
1495
- this._tooltipScrollHandler = () => this.hideAllTooltips();
1496
- this.element.addEventListener("scroll", this._tooltipScrollHandler, { passive: true });
1497
- this._tooltipRouteHandler = () => this.hideAllTooltips();
1498
- this.getApp();
1499
- this._tooltipBlurHandler = () => this.hideAllTooltips();
1500
- window.addEventListener("blur", this._tooltipBlurHandler);
1501
- this._tooltipEscapeHandler = (e3) => {
1502
- if (e3.key === "Escape") {
1503
- this.hideAllTooltips();
1504
- }
1505
- };
1506
- document.addEventListener("keydown", this._tooltipEscapeHandler);
1507
- }
1508
- /**
1509
- * Remove global tooltip hide listeners
1510
- */
1511
- removeTooltipHideListeners() {
1512
- if (this._tooltipScrollHandler) {
1513
- this.element.removeEventListener("scroll", this._tooltipScrollHandler);
1514
- delete this._tooltipScrollHandler;
1515
- }
1516
- if (this._tooltipBlurHandler) {
1517
- window.removeEventListener("blur", this._tooltipBlurHandler);
1518
- delete this._tooltipBlurHandler;
1519
- }
1520
- if (this._tooltipEscapeHandler) {
1521
- document.removeEventListener("keydown", this._tooltipEscapeHandler);
1522
- delete this._tooltipEscapeHandler;
1523
- }
1524
- }
1525
- /**
1526
- * Force hide all visible tooltips
1527
- */
1528
- hideAllTooltips() {
1529
- const navLinks = this.element.querySelectorAll('.sidebar-nav .nav-link[data-bs-toggle="tooltip"]');
1530
- navLinks.forEach((link) => {
1531
- const tooltip = link._tooltipInstance || window.bootstrap?.Tooltip?.getInstance(link);
1532
- if (tooltip) {
1533
- tooltip.hide();
1534
- }
1535
- });
1536
- const visibleTooltips = document.querySelectorAll(".tooltip.show");
1537
- visibleTooltips.forEach((tooltip) => {
1538
- tooltip.remove();
1539
- });
1540
- }
1541
- /**
1542
- * Cleanup on destroy
1543
- */
1544
- async onBeforeDestroy() {
1545
- this.destroyTooltips();
1546
- await super.onBeforeDestroy();
1547
- }
1548
- /**
1549
- * Setup responsive behavior for mobile
1550
- */
1551
- setupResponsiveBehavior() {
1552
- const checkMobile = () => {
1553
- const isMobile = window.innerWidth <= 768;
1554
- const portalContainer = document.querySelector(".portal-container");
1555
- if (portalContainer) {
1556
- if (isMobile) {
1557
- portalContainer.classList.add("sidebar-mobile");
1558
- } else {
1559
- portalContainer.classList.remove("sidebar-mobile", "sidebar-open");
1560
- }
1561
- }
1562
- };
1563
- checkMobile();
1564
- window.addEventListener("resize", checkMobile);
1565
- }
1566
- /**
1567
- * Static method to create a sidebar with common configuration
1568
- */
1569
- static createDefault(options = {}) {
1570
- return new Sidebar({
1571
- theme: "sidebar-clean",
1572
- showToggle: true,
1573
- autoCollapseMobile: true,
1574
- ...options
1575
- });
1576
- }
1577
- /**
1578
- * Static method to create a minimal sidebar
1579
- */
1580
- static createMinimal(options = {}) {
1581
- return new Sidebar({
1582
- theme: "sidebar-clean",
1583
- showToggle: false,
1584
- autoCollapseMobile: false,
1585
- ...options
1586
- });
1587
- }
1588
- /**
1589
- * Set sidebar theme
1590
- */
1591
- setSidebarTheme(theme) {
1592
- this.removeClass("sidebar-light sidebar-dark sidebar-clean");
1593
- this.sidebarTheme = theme;
1594
- this.addClass(theme);
1595
- return this;
1596
- }
1597
- /**
1598
- * Quick method to show/hide the sidebar
1599
- */
1600
- show() {
1601
- return this.setSidebarState("normal");
1602
- }
1603
- hide() {
1604
- return this.setSidebarState("hidden");
1605
- }
1606
- collapse() {
1607
- return this.setSidebarState("collapsed");
1608
- }
1609
- expand() {
1610
- return this.setSidebarState("normal");
1611
- }
1612
- /**
1613
- * Add pulse effect to toggle button
1614
- */
1615
- pulseToggle() {
1616
- const toggleButton = this.element.querySelector(".sidebar-toggle");
1617
- if (toggleButton) {
1618
- toggleButton.classList.add("pulse");
1619
- const removePulse = () => {
1620
- toggleButton.classList.remove("pulse");
1621
- toggleButton.removeEventListener("click", removePulse);
1622
- };
1623
- toggleButton.addEventListener("click", removePulse, { once: true });
1624
- setTimeout(removePulse, 3e3);
1625
- }
1626
- return this;
1627
- }
1628
- /**
1629
- * Utility method to quickly add a simple menu item
1630
- */
1631
- addSimpleMenuItem(menuName, text, route, icon = "bi-circle") {
1632
- const menu = this.menus.get(menuName);
1633
- if (menu) {
1634
- menu.items = menu.items || [];
1635
- menu.items.push({
1636
- text,
1637
- route,
1638
- icon
1639
- });
1640
- if (this.activeMenuName === menuName) {
1641
- this.render();
1642
- }
1643
- }
1644
- return this;
1645
- }
1646
- /**
1647
- * Utility method to quickly create and set a simple menu
1648
- */
1649
- setSimpleMenu(name, header, items) {
1650
- const menu = {
1651
- name,
1652
- header,
1653
- items
1654
- };
1655
- this.addMenu(name, menu);
1656
- this.setActiveMenu(name);
1657
- return this;
1658
- }
1659
- }
1660
- class PageHeader extends View {
1661
- constructor(options = {}) {
1662
- super({
1663
- tagName: "div",
1664
- className: "page-header",
1665
- ...options
1666
- });
1667
- this.style = options.style || "default";
1668
- this.size = options.size || "md";
1669
- this.showIcon = options.showIcon !== false;
1670
- this.showDescription = options.showDescription !== false;
1671
- this.showBreadcrumbs = options.showBreadcrumbs || false;
1672
- this.currentPage = null;
1673
- }
1674
- async getTemplate() {
1675
- if (this.style === "minimal") {
1676
- return this.getMinimalTemplate();
1677
- } else if (this.style === "breadcrumb") {
1678
- return this.getBreadcrumbTemplate();
1679
- }
1680
- return this.getDefaultTemplate();
1681
- }
1682
- getDefaultTemplate() {
1683
- return `
1684
- {{#data.hasPage}}
1685
- <div class="page-header-content page-header-{{data.size}}">
1686
- <div class="page-header-main">
1687
- <div class="page-header-info">
1688
- {{#data.showIcon}}
1689
- {{#data.pageIcon}}
1690
- <div class="page-icon">
1691
- <i class="{{data.pageIcon}}"></i>
1692
- </div>
1693
- {{/data.pageIcon}}
1694
- {{/data.showIcon}}
1695
-
1696
- <div class="page-title-group">
1697
- <h1 class="page-title">{{data.pageTitle}}</h1>
1698
- {{#data.showDescription}}
1699
- {{#data.pageDescription}}
1700
- <p class="page-description text-muted">{{data.pageDescription}}</p>
1701
- {{/data.pageDescription}}
1702
- {{/data.showDescription}}
1703
- </div>
1704
- </div>
1705
-
1706
- {{#data.hasActions}}
1707
- <div class="page-actions">
1708
- {{#data.actions}}
1709
- <button class="btn {{buttonClass}}"
1710
- data-action="{{action}}"
1711
- type="button">
1712
- {{#icon}}<i class="{{icon}} me-1"></i>{{/icon}}
1713
- {{label}}
1714
- </button>
1715
- {{/data.actions}}
1716
- </div>
1717
- {{/data.hasActions}}
1718
- </div>
1719
- </div>
1720
- {{/data.hasPage}}
1721
- `;
1722
- }
1723
- getMinimalTemplate() {
1724
- return `
1725
- {{#data.hasPage}}
1726
- <div class="page-header-content page-header-minimal">
1727
- <h1 class="page-title">
1728
- {{#data.showIcon}}
1729
- {{#data.pageIcon}}<i class="{{data.pageIcon}} me-2"></i>{{/data.pageIcon}}
1730
- {{/data.showIcon}}
1731
- {{data.pageTitle}}
1732
- </h1>
1733
- </div>
1734
- {{/data.hasPage}}
1735
- `;
1736
- }
1737
- getBreadcrumbTemplate() {
1738
- return `
1739
- {{#data.hasPage}}
1740
- <div class="page-header-content page-header-breadcrumb">
1741
- {{#data.showBreadcrumbs}}
1742
- <nav aria-label="breadcrumb">
1743
- <ol class="breadcrumb mb-2">
1744
- {{#data.breadcrumbs}}
1745
- <li class="breadcrumb-item {{#active}}active{{/active}}">
1746
- {{#href}}<a href="{{href}}">{{label}}</a>{{/href}}
1747
- {{^href}}{{label}}{{/href}}
1748
- </li>
1749
- {{/data.breadcrumbs}}
1750
- </ol>
1751
- </nav>
1752
- {{/data.showBreadcrumbs}}
1753
-
1754
- <div class="d-flex justify-content-between align-items-start">
1755
- <h1 class="page-title">
1756
- {{#data.showIcon}}
1757
- {{#data.pageIcon}}<i class="{{data.pageIcon}} me-2"></i>{{/data.pageIcon}}
1758
- {{/data.showIcon}}
1759
- {{data.pageTitle}}
1760
- </h1>
1761
-
1762
- {{#data.hasActions}}
1763
- <div class="page-actions">
1764
- {{#data.actions}}
1765
- <button class="btn {{buttonClass}}"
1766
- data-action="{{action}}"
1767
- type="button">
1768
- {{#icon}}<i class="{{icon}} me-1"></i>{{/icon}}
1769
- {{label}}
1770
- </button>
1771
- {{/data.actions}}
1772
- </div>
1773
- {{/data.hasActions}}
1774
- </div>
1775
-
1776
- {{#data.showDescription}}
1777
- {{#data.pageDescription}}
1778
- <p class="page-description text-muted mt-2">{{data.pageDescription}}</p>
1779
- {{/data.pageDescription}}
1780
- {{/data.showDescription}}
1781
- </div>
1782
- {{/data.hasPage}}
1783
- `;
1784
- }
1785
- async onBeforeRender() {
1786
- await super.onBeforeRender();
1787
- const page = this.currentPage;
1788
- const hasPage = !!page;
1789
- if (page) {
1790
- console.log("PageHeader page:", {
1791
- title: page.title,
1792
- displayName: page.displayName,
1793
- name: page.name,
1794
- pageName: page.pageName,
1795
- icon: page.icon,
1796
- pageIcon: page.pageIcon,
1797
- pageDescription: page.pageDescription,
1798
- description: page.description
1799
- });
1800
- }
1801
- const headerActions = page?.options?.headerActions || page?.headerActions || page?.constructor?.prototype?.headerActions || [];
1802
- this.data = {
1803
- hasPage,
1804
- pageTitle: page?.title || page?.displayName || page?.name || page?.pageName || "",
1805
- pageIcon: page?.icon || page?.pageIcon || "",
1806
- pageDescription: page?.pageDescription || page?.description || "",
1807
- showIcon: this.showIcon,
1808
- showDescription: this.showDescription,
1809
- showBreadcrumbs: this.showBreadcrumbs,
1810
- breadcrumbs: page?.options?.breadcrumbs || page?.breadcrumbs || [],
1811
- actions: headerActions,
1812
- hasActions: headerActions.length > 0,
1813
- size: this.size
1814
- };
1815
- console.log("PageHeader data:", this.data);
1816
- }
1817
- /**
1818
- * Set the current page to display
1819
- */
1820
- async setPage(page) {
1821
- console.log("PageHeader.setPage called with:", page?.pageName || page?.name || "no page");
1822
- this.currentPage = page;
1823
- if (page) {
1824
- console.log("PageHeader.setPage calling render()");
1825
- await this.render();
1826
- console.log("PageHeader.setPage render() complete");
1827
- }
1828
- }
1829
- /**
1830
- * Get the current page
1831
- */
1832
- getPage() {
1833
- return this.currentPage;
1834
- }
1835
- /**
1836
- * Handle action clicks from page header buttons
1837
- */
1838
- async onActionDefault(action, event, element) {
1839
- if (this.currentPage && typeof this.currentPage.onHeaderAction === "function") {
1840
- await this.currentPage.onHeaderAction(action, event, element);
1841
- return true;
1842
- }
1843
- this.emit("action", {
1844
- action,
1845
- event,
1846
- element,
1847
- page: this.currentPage
1848
- });
1849
- return false;
1850
- }
1851
- }
1852
- class DeniedPage extends Page {
1853
- constructor(options = {}) {
1854
- super({
1855
- pageName: "Access Denied",
1856
- route: "/denied",
1857
- title: "Access Denied",
1858
- pageIcon: "bi bi-shield-x",
1859
- template: `
1860
- <div class="container mt-5">
1861
- <div class="row justify-content-center">
1862
- <div class="col-md-8 col-lg-6">
1863
- <div class="text-center mb-4">
1864
- <i class="bi bi-shield-x text-muted" style="font-size: 3rem;"></i>
1865
- <h2 class="mt-3 mb-2">Access Denied</h2>
1866
- <p class="text-muted">You don't have permission to access this page.</p>
1867
- </div>
1868
-
1869
- {{#deniedPage}}
1870
- <div class="card border-0 shadow-sm mb-4">
1871
- <div class="card-body">
1872
- <h6 class="card-subtitle mb-2 text-muted">Requested Page</h6>
1873
- <h5 class="card-title">
1874
- <i class="{{pageIcon}} me-2"></i>
1875
- {{displayName}}
1876
- </h5>
1877
- {{#route}}
1878
- <p class="card-text text-muted small">{{route}}</p>
1879
- {{/route}}
1880
- {{#description}}
1881
- <p class="card-text">{{description}}</p>
1882
- {{/description}}
1883
-
1884
- {{#requiredPermissions}}
1885
- <div class="mt-3">
1886
- <h6 class="mb-2">Required Permissions:</h6>
1887
- {{#permissions}}
1888
- <span class="badge bg-light text-dark me-1 mb-1">{{.}}</span>
1889
- {{/permissions}}
1890
- {{^permissions}}
1891
- <span class="text-muted small">Authentication required</span>
1892
- {{/permissions}}
1893
- </div>
1894
- {{/requiredPermissions}}
1895
- </div>
1896
- </div>
1897
- {{/deniedPage}}
1898
-
1899
- <div class="d-grid gap-2 d-md-flex justify-content-md-center">
1900
- <button type="button" class="btn btn-primary" data-action="go-back">
1901
- <i class="bi bi-arrow-left me-1"></i>
1902
- Go Back
1903
- </button>
1904
- <button type="button" class="btn btn-outline-secondary" data-action="go-home">
1905
- <i class="bi bi-house me-1"></i>
1906
- Home
1907
- </button>
1908
- {{#showLogin}}
1909
- <button type="button" class="btn btn-outline-primary" data-action="login">
1910
- <i class="bi bi-box-arrow-in-right me-1"></i>
1911
- Login
1912
- </button>
1913
- {{/showLogin}}
1914
- </div>
1915
-
1916
- {{#currentUser}}
1917
- <div class="text-center mt-4">
1918
- <small class="text-muted">
1919
- Logged in as <strong>{{username}}</strong>
1920
- </small>
1921
- </div>
1922
- {{/currentUser}}
1923
- </div>
1924
- </div>
1925
- </div>
1926
- `,
1927
- ...options
1928
- });
1929
- this.deniedPage = null;
1930
- this.deniedPageOptions = null;
1931
- }
1932
- /**
1933
- * Handle route parameters - expect denied page info
1934
- */
1935
- async onParams(params = {}, query = {}) {
1936
- await super.onParams(params, query);
1937
- if (params.page) {
1938
- this.deniedPage = params.page;
1939
- this.deniedPageOptions = params.page.options || params.page.pageOptions || {};
1940
- } else if (query.page) {
1941
- this.deniedPageName = query.page;
1942
- }
1943
- }
1944
- /**
1945
- * Set the denied page instance
1946
- */
1947
- setDeniedPage(pageInstance) {
1948
- this.deniedPage = pageInstance;
1949
- this.deniedPageOptions = pageInstance?.options || pageInstance?.pageOptions || {};
1950
- return this;
1951
- }
1952
- /**
1953
- * Get view data for template rendering
1954
- */
1955
- async getViewData() {
1956
- const app = this.getApp();
1957
- const currentUser = app?.activeUser || app?.getCurrentUser?.() || null;
1958
- let deniedPageInfo = null;
1959
- if (this.deniedPage) {
1960
- const permissions = this.deniedPageOptions?.permissions || this.deniedPage.options?.permissions || this.deniedPage.pageOptions?.permissions;
1961
- deniedPageInfo = {
1962
- displayName: this.deniedPage.displayName || this.deniedPage.pageName || this.deniedPage.title || "Unknown Page",
1963
- pageName: this.deniedPage.pageName,
1964
- route: this.deniedPage.route,
1965
- description: this.deniedPage.pageDescription || this.deniedPage.description,
1966
- pageIcon: this.deniedPage.pageIcon || "bi bi-file-text",
1967
- requiredPermissions: permissions ? {
1968
- permissions: Array.isArray(permissions) ? permissions : [permissions]
1969
- } : null
1970
- };
1971
- } else if (this.deniedPageName) {
1972
- deniedPageInfo = {
1973
- displayName: this.deniedPageName,
1974
- pageName: this.deniedPageName,
1975
- pageIcon: "bi bi-file-text"
1976
- };
1977
- }
1978
- return {
1979
- deniedPage: deniedPageInfo,
1980
- currentUser: currentUser ? {
1981
- username: currentUser.username || currentUser.name || currentUser.email || "Unknown User",
1982
- name: currentUser.name,
1983
- email: currentUser.email
1984
- } : null,
1985
- showLogin: !currentUser
1986
- // Show login button if not authenticated
1987
- };
1988
- }
1989
- /**
1990
- * Handle going back to previous page
1991
- */
1992
- async handleActionGoBack(event, element) {
1993
- event.preventDefault();
1994
- if (window.history.length > 1) {
1995
- window.history.back();
1996
- } else {
1997
- await this.handleActionGoHome(event, element);
1998
- }
1999
- }
2000
- /**
2001
- * Handle navigation to home page
2002
- */
2003
- async handleActionGoHome(event, element) {
2004
- event.preventDefault();
2005
- const app = this.getApp();
2006
- if (app) {
2007
- await app.navigateToDefault();
2008
- } else {
2009
- window.location.href = "/";
2010
- }
2011
- }
2012
- /**
2013
- * Handle login action
2014
- */
2015
- async handleActionLogin(event, element) {
2016
- event.preventDefault();
2017
- const app = this.getApp();
2018
- if (app) {
2019
- try {
2020
- await app.showPage("login");
2021
- } catch (error) {
2022
- try {
2023
- await app.navigate("/login");
2024
- } catch (navError) {
2025
- this.emit("login-required", {
2026
- returnUrl: this.deniedPage?.route || window.location.pathname
2027
- });
2028
- setTimeout(() => {
2029
- app?.showInfo?.("Please contact your administrator for access.");
2030
- }, 100);
2031
- }
2032
- }
2033
- }
2034
- }
2035
- /**
2036
- * Called when entering this page
2037
- */
2038
- async onEnter() {
2039
- await super.onEnter();
2040
- const pageName = this.deniedPage?.pageName || this.deniedPageName;
2041
- if (pageName) {
2042
- this.setMeta({
2043
- title: `Access Denied - ${pageName}`
2044
- });
2045
- }
2046
- console.warn("Access denied to page:", {
2047
- page: this.deniedPage?.pageName || this.deniedPageName,
2048
- route: this.deniedPage?.route,
2049
- permissions: this.deniedPageOptions?.permissions,
2050
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2051
- });
2052
- }
2053
- /**
2054
- * Static helper to show access denied for a specific page
2055
- */
2056
- static showForPage(app, pageInstance) {
2057
- const deniedPage = new DeniedPage();
2058
- deniedPage.setDeniedPage(pageInstance);
2059
- return app.showPage(deniedPage);
2060
- }
2061
- }
2062
- class NotFoundPage extends Page {
2063
- constructor(options = {}) {
2064
- super({
2065
- pageName: "404",
2066
- route: "/404",
2067
- title: "404 - Page Not Found",
2068
- pageIcon: "bi bi-search",
2069
- template: `
2070
- <div class="container mt-5">
2071
- <div class="row justify-content-center">
2072
- <div class="col-md-8 col-lg-6">
2073
- <div class="text-center mb-4">
2074
- <i class="bi bi-search text-muted" style="font-size: 3rem;"></i>
2075
- <h2 class="mt-3 mb-2">Page Not Found</h2>
2076
- <p class="text-muted">The page you're looking for doesn't exist.</p>
2077
- </div>
2078
-
2079
- {{#path}}
2080
- <div class="card border-0 shadow-sm mb-4">
2081
- <div class="card-body text-center">
2082
- <h6 class="card-subtitle mb-2 text-muted">Requested Path</h6>
2083
- <code class="text-primary">{{path}}</code>
2084
- </div>
2085
- </div>
2086
- {{/path}}
2087
-
2088
- <div class="d-grid gap-2 d-md-flex justify-content-md-center">
2089
- <button type="button" class="btn btn-primary" data-action="go-back">
2090
- <i class="bi bi-arrow-left me-1"></i>
2091
- Go Back
2092
- </button>
2093
- <button type="button" class="btn btn-outline-secondary" data-action="go-home">
2094
- <i class="bi bi-house me-1"></i>
2095
- Home
2096
- </button>
2097
- </div>
2098
- </div>
2099
- </div>
2100
- </div>
2101
- `,
2102
- ...options
2103
- });
2104
- this.path = null;
2105
- }
2106
- /**
2107
- * Handle route parameters
2108
- */
2109
- async onParams(params = {}, query = {}) {
2110
- await super.onParams(params, query);
2111
- if (params.path) {
2112
- this.path = params.path;
2113
- }
2114
- if (query.path) {
2115
- this.path = query.path;
2116
- }
2117
- }
2118
- /**
2119
- * Set not found path
2120
- */
2121
- setInfo(path) {
2122
- this.path = path || null;
2123
- return this;
2124
- }
2125
- /**
2126
- * Handle going back to previous page
2127
- */
2128
- async handleActionGoBack(event, _element) {
2129
- event.preventDefault();
2130
- if (window.history.length > 1) {
2131
- window.history.back();
2132
- } else {
2133
- await this.handleActionGoHome(event, _element);
2134
- }
2135
- }
2136
- /**
2137
- * Handle navigation to home page
2138
- */
2139
- async handleActionGoHome(event, _element) {
2140
- event.preventDefault();
2141
- const app = this.getApp();
2142
- if (app) {
2143
- await app.navigateToDefault();
2144
- } else {
2145
- window.location.href = "/";
2146
- }
2147
- }
2148
- /**
2149
- * Called when entering this page
2150
- */
2151
- async onEnter() {
2152
- await super.onEnter();
2153
- if (this.path) {
2154
- this.setMeta({
2155
- title: `404 - ${this.path} Not Found`
2156
- });
2157
- }
2158
- console.warn("404 Not Found:", {
2159
- path: this.path,
2160
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2161
- });
2162
- }
2163
- /**
2164
- * Static helper to show 404 for a specific path
2165
- */
2166
- static showForPath(app, path) {
2167
- const notFoundPage = new NotFoundPage();
2168
- notFoundPage.setInfo(path);
2169
- return notFoundPage.render();
2170
- }
2171
- }
2172
- class PortalApp extends WebApp {
2173
- constructor(config = {}) {
2174
- super(config);
2175
- this.sidebarConfig = config.sidebar;
2176
- this.topbarConfig = config.topbar || {};
2177
- if (config.topnav && !config.topbar) {
2178
- this.topbarConfig = config.topnav;
2179
- }
2180
- this.showPageHeader = config.showPageHeader || false;
2181
- this.pageHeaderConfig = config.pageHeader || {};
2182
- this.sidebar = null;
2183
- this.topbar = null;
2184
- this.topnav = null;
2185
- this.pageHeader = null;
2186
- this.tokenManager = new TokenManager();
2187
- this.activeGroup = null;
2188
- if (!this.isMobile()) {
2189
- this.sidebarCollapsed = this.loadSidebarState() ?? (this.sidebarConfig.defaultCollapsed || false);
2190
- } else {
2191
- this.sidebarCollapsed = this.sidebarConfig.defaultCollapsed || false;
2192
- }
2193
- this.setupPageContainer();
2194
- this.toast = new ToastService();
2195
- this.Dialog = Dialog;
2196
- this.registerPage("denied", DeniedPage);
2197
- this.registerPage("404", NotFoundPage);
2198
- }
2199
- /**
2200
- * Override WebApp start to setup portal layout
2201
- */
2202
- async start() {
2203
- await this.checkAuthStatus();
2204
- this.events.on("auth:unauthorized", () => {
2205
- this.tokenManager.clearTokens();
2206
- this.rest.clearAuth();
2207
- this.setActiveUser(null);
2208
- return;
2209
- });
2210
- this.events.on("auth:logout", () => {
2211
- this.tokenManager.clearTokens();
2212
- this.rest.clearAuth();
2213
- this.setActiveUser(null);
2214
- return;
2215
- });
2216
- this.events.on("browser:focus", () => {
2217
- if (!this.activeUser) return;
2218
- this.tokenManager.checkAndRefreshTokens(this);
2219
- });
2220
- this.events.on("portal:action", this.onPortalAction.bind(this));
2221
- if (this.activeUser) {
2222
- await this.checkActiveGroup();
2223
- }
2224
- await this.setupRouter();
2225
- this.isStarted = true;
2226
- this.events.emit("app:ready", { app: this });
2227
- }
2228
- async checkAuthStatus() {
2229
- const tokenStatus = this.tokenManager.checkTokenStatus();
2230
- if (tokenStatus.action === "logout") {
2231
- this.events.emit("auth:unauthorized", { app: this });
2232
- return false;
2233
- }
2234
- if (tokenStatus.action === "refresh") {
2235
- const refreshed = await this.tokenManager.checkAndRefreshTokens(this);
2236
- if (!refreshed) {
2237
- return false;
2238
- }
2239
- }
2240
- const token = this.tokenManager.getTokenInstance();
2241
- if (this.activeUser) {
2242
- this.tokenManager.startAutoRefresh(this);
2243
- return true;
2244
- }
2245
- this.rest.setAuthToken(token.token);
2246
- const user = new User({ id: token.getUserId() });
2247
- const resp = await user.fetch();
2248
- if (!resp.success) {
2249
- this.tokenManager.clearTokens();
2250
- this.events.emit("auth:unauthorized", { app: this, error: resp.error });
2251
- return false;
2252
- }
2253
- this.setActiveUser(user);
2254
- this.tokenManager.startAutoRefresh(this);
2255
- return true;
2256
- }
2257
- /**
2258
- * Check and load active group from storage
2259
- */
2260
- async checkActiveGroup() {
2261
- const urlParams = new URLSearchParams(window.location.search);
2262
- const urlGroupId = urlParams.get("group");
2263
- const groupId = urlGroupId || this.loadActiveGroupId();
2264
- if (groupId) {
2265
- try {
2266
- const group = new Group({ id: groupId });
2267
- const resp = await group.fetch();
2268
- if (!resp.success || !resp.data.status) {
2269
- this.clearActiveGroup();
2270
- console.warn("Failed to load active group:", resp.statusText);
2271
- return;
2272
- }
2273
- this.activeGroup = group;
2274
- if (urlGroupId) {
2275
- this.saveActiveGroupId(groupId);
2276
- }
2277
- if (this.activeUser) {
2278
- this.activeUser.member = new Member();
2279
- await this.activeUser.member.fetchForGroup(group.id);
2280
- }
2281
- this.events.emit("group:loaded", { group: this.activeGroup });
2282
- } catch (error) {
2283
- console.warn("Failed to load active group:", error);
2284
- if (urlGroupId && !this.loadActiveGroupId()) {
2285
- this.clearActiveGroupId();
2286
- } else if (urlGroupId) {
2287
- const storedGroupId = this.loadActiveGroupId();
2288
- if (storedGroupId && storedGroupId !== urlGroupId) {
2289
- try {
2290
- const fallbackGroup = new Group({ id: storedGroupId });
2291
- await fallbackGroup.fetch();
2292
- this.activeGroup = fallbackGroup;
2293
- this.events.emit("group:loaded", { group: this.activeGroup });
2294
- } catch (fallbackError) {
2295
- console.warn("Fallback to stored group also failed:", fallbackError);
2296
- this.clearActiveGroupId();
2297
- }
2298
- }
2299
- }
2300
- }
2301
- }
2302
- }
2303
- /**
2304
- * Set the active group
2305
- */
2306
- async setActiveGroup(group) {
2307
- const previousGroup = this.activeGroup;
2308
- this.activeGroup = group;
2309
- if (group && group.get("id")) {
2310
- this.saveActiveGroupId(group.get("id"));
2311
- } else {
2312
- this.clearActiveGroupId();
2313
- }
2314
- if (this.activeUser) {
2315
- this.activeUser.member = new Member();
2316
- await this.activeUser.member.fetchForGroup(group.id);
2317
- }
2318
- this.events.emit("group:changed", {
2319
- group,
2320
- previousGroup,
2321
- app: this
2322
- });
2323
- const page = this.getCurrentPage();
2324
- if (page && page.onGroupChange) {
2325
- page.onGroupChange(group);
2326
- }
2327
- this.router.updateUrl({ group: group.id }, { replace: true });
2328
- return this;
2329
- }
2330
- /**
2331
- * Get the active group
2332
- */
2333
- getActiveGroup() {
2334
- return this.activeGroup;
2335
- }
2336
- /**
2337
- * Clear the active group
2338
- */
2339
- async clearActiveGroup() {
2340
- const previousGroup = this.activeGroup;
2341
- this.activeGroup = null;
2342
- this.clearActiveGroupId();
2343
- this.events.emit("group:cleared", {
2344
- previousGroup,
2345
- app: this
2346
- });
2347
- return this;
2348
- }
2349
- /**
2350
- * Save active group ID to localStorage
2351
- */
2352
- saveActiveGroupId(groupId) {
2353
- try {
2354
- const key = this.getActiveGroupStorageKey();
2355
- localStorage.setItem(key, groupId.toString());
2356
- } catch (error) {
2357
- console.warn("Failed to save active group ID:", error);
2358
- }
2359
- }
2360
- /**
2361
- * Load active group ID from localStorage
2362
- */
2363
- loadActiveGroupId() {
2364
- try {
2365
- const key = this.getActiveGroupStorageKey();
2366
- return localStorage.getItem(key);
2367
- } catch (error) {
2368
- console.warn("Failed to load active group ID:", error);
2369
- return null;
2370
- }
2371
- }
2372
- /**
2373
- * Clear active group ID from localStorage
2374
- */
2375
- clearActiveGroupId() {
2376
- try {
2377
- const key = this.getActiveGroupStorageKey();
2378
- localStorage.removeItem(key);
2379
- } catch (error) {
2380
- console.warn("Failed to clear active group ID:", error);
2381
- }
2382
- }
2383
- /**
2384
- * Get storage key for active group ID
2385
- */
2386
- getActiveGroupStorageKey() {
2387
- return `active_group_id`;
2388
- }
2389
- /**
2390
- * Set portal profile to localStorage
2391
- */
2392
- setPortalProfile(profile) {
2393
- try {
2394
- localStorage.setItem("portal_profile", profile);
2395
- } catch (error) {
2396
- console.warn("Failed to save portal profile:", error);
2397
- }
2398
- }
2399
- /**
2400
- * Check if user needs to select a group
2401
- */
2402
- needsGroupSelection() {
2403
- return !this.activeGroup;
2404
- }
2405
- /**
2406
- * Setup layout based on configuration
2407
- */
2408
- setupPageContainer() {
2409
- const container = typeof this.container === "string" ? document.querySelector(this.container) : this.container;
2410
- if (!container) {
2411
- throw new Error(`Portal container not found: ${this.container}`);
2412
- }
2413
- const showSidebar = this.sidebarConfig && Object.keys(this.sidebarConfig).length > 0;
2414
- const showTopbar = this.topbarConfig && Object.keys(this.topbarConfig).length > 0;
2415
- const contentMarkup = this.showPageHeader ? `
2416
- <div class="portal-content" id="portal-content">
2417
- <div id="page-header"></div>
2418
- <div id="page-container">
2419
- <!-- Pages render here -->
2420
- </div>
2421
- </div>
2422
- ` : `
2423
- <div class="portal-content" id="page-container">
2424
- <!-- Pages render here -->
2425
- </div>
2426
- `;
2427
- container.innerHTML = `
2428
- <div class="portal-layout hide-sidebar">
2429
- ${showSidebar ? '<div id="portal-sidebar"></div>' : ""}
2430
- <div class="portal-body">
2431
- ${showTopbar ? '<div id="portal-topnav"></div>' : ""}
2432
- ${contentMarkup}
2433
- </div>
2434
- </div>
2435
- `;
2436
- this.pageContainer = "#page-container";
2437
- container.classList.add("portal-container");
2438
- this.setupPortalComponents();
2439
- this.applySidebarState(container);
2440
- }
2441
- /**
2442
- * Setup portal components
2443
- */
2444
- async setupPortalComponents() {
2445
- await this.setupSidebar();
2446
- await this.setupTopbar();
2447
- await this.setupPageHeader();
2448
- this.setupPortalEvents();
2449
- }
2450
- /**
2451
- * Setup sidebar component
2452
- */
2453
- async setupSidebar() {
2454
- if (!this.sidebarConfig || Object.keys(this.sidebarConfig).length === 0) return;
2455
- this.sidebar = new Sidebar({
2456
- containerId: "portal-sidebar",
2457
- ...this.sidebarConfig
2458
- });
2459
- await this.sidebar.render();
2460
- }
2461
- /**
2462
- * Setup topbar component
2463
- */
2464
- async setupTopbar() {
2465
- if (!this.topbarConfig || Object.keys(this.topbarConfig).length === 0) return;
2466
- this.topbar = new TopNav({
2467
- containerId: "portal-topnav",
2468
- brandText: this.topbarConfig.brand || this.brand || this.title,
2469
- brandRoute: this.topbarConfig.brandRoute || "/",
2470
- brandIcon: this.topbarConfig.brandIcon || this.brandIcon,
2471
- navItems: this.topbarConfig.leftItems || [],
2472
- rightItems: this.topbarConfig.rightItems || [],
2473
- displayMode: this.topbarConfig.displayMode || "both",
2474
- showSidebarToggle: this.topbarConfig.showSidebarToggle || false,
2475
- ...this.topbarConfig
2476
- });
2477
- await this.topbar.render();
2478
- this.topnav = this.topbar;
2479
- }
2480
- /**
2481
- * Setup page header component
2482
- */
2483
- async setupPageHeader() {
2484
- if (!this.showPageHeader) return;
2485
- this.pageHeader = new PageHeader({
2486
- containerId: "page-header",
2487
- style: this.pageHeaderConfig.style || "default",
2488
- showIcon: this.pageHeaderConfig.showIcon !== false,
2489
- showDescription: this.pageHeaderConfig.showDescription !== false,
2490
- showBreadcrumbs: this.pageHeaderConfig.showBreadcrumbs || false,
2491
- ...this.pageHeaderConfig
2492
- });
2493
- const headerContainer = document.getElementById("page-header");
2494
- if (headerContainer) {
2495
- await this.pageHeader.render(true, headerContainer);
2496
- }
2497
- }
2498
- /**
2499
- * Setup portal event handling
2500
- */
2501
- setupPortalEvents() {
2502
- document.addEventListener("click", (event) => {
2503
- if (event.target.closest('[data-action="toggle-sidebar"]')) {
2504
- event.preventDefault();
2505
- this.toggleSidebar();
2506
- }
2507
- });
2508
- if (window.ResizeObserver) {
2509
- const resizeObserver = new ResizeObserver(() => {
2510
- this.handleResponsive();
2511
- });
2512
- resizeObserver.observe(document.body);
2513
- this._resizeObserver = resizeObserver;
2514
- } else {
2515
- this._resizeHandler = () => this.handleResponsive();
2516
- window.addEventListener("resize", this._resizeHandler);
2517
- }
2518
- this.handleResponsive();
2519
- }
2520
- /**
2521
- * Toggle sidebar state
2522
- */
2523
- toggleSidebar() {
2524
- if (!this.sidebar) return;
2525
- const container = document.querySelector(".portal-container");
2526
- const isMobile = this.isMobile();
2527
- if (isMobile) {
2528
- container.classList.toggle("hide-sidebar");
2529
- } else {
2530
- container.classList.toggle("collapse-sidebar");
2531
- this.sidebarCollapsed = !this.sidebarCollapsed;
2532
- this.saveSidebarState(this.sidebarCollapsed);
2533
- }
2534
- this.events.emit("sidebar:toggled", {
2535
- collapsed: this.sidebarCollapsed,
2536
- mobile: isMobile
2537
- });
2538
- }
2539
- /**
2540
- * Handle responsive layout
2541
- */
2542
- handleResponsive() {
2543
- const container = document.querySelector(".portal-container");
2544
- if (!container) return;
2545
- const isMobile = this.isMobile();
2546
- if (isMobile) {
2547
- container.classList.add("mobile-layout");
2548
- if (!container.classList.contains("hide-sidebar")) {
2549
- container.classList.add("hide-sidebar");
2550
- }
2551
- } else {
2552
- container.classList.remove("mobile-layout", "hide-sidebar");
2553
- }
2554
- this.events.emit("responsive:changed", { mobile: isMobile });
2555
- }
2556
- getPortalContainer() {
2557
- return document.querySelector(".portal-container");
2558
- }
2559
- isMobile() {
2560
- return window.innerWidth < 768;
2561
- }
2562
- hasMobileLayout() {
2563
- return this.getPortalContainer().classList.contains("mobile-layout");
2564
- }
2565
- /**
2566
- * Override showPage to update navigation
2567
- */
2568
- async showPage(page, query = {}, params = {}, options = {}) {
2569
- const result = await super.showPage(page, query, params, options);
2570
- if (this.hasMobileLayout()) {
2571
- this.getPortalContainer().classList.add("hide-sidebar");
2572
- }
2573
- if (this.currentPage) {
2574
- this.updateNavigation(this.currentPage);
2575
- }
2576
- return result;
2577
- }
2578
- /**
2579
- * Update navigation active states
2580
- */
2581
- updateNavigation(page) {
2582
- if (this.sidebar && this.sidebar.setActivePage) {
2583
- this.sidebar.setActivePage(page.route);
2584
- }
2585
- if (this.topbar && this.topbar.setActivePage) {
2586
- this.topbar.setActivePage(page.route);
2587
- }
2588
- if (this.pageHeader) {
2589
- this.pageHeader.setPage(page);
2590
- }
2591
- this.events.emit("portal:page-changed", { page });
2592
- }
2593
- /**
2594
- * Set active user
2595
- */
2596
- setActiveUser(user) {
2597
- this.activeUser = user;
2598
- if (this.topbar) {
2599
- this.topbar.setUser(user);
2600
- }
2601
- this.events.emit("portal:user-changed", { user });
2602
- }
2603
- /**
2604
- * Get the active user (for backward compatibility)
2605
- */
2606
- getActiveUser() {
2607
- return this.activeUser;
2608
- }
2609
- /**
2610
- * Save sidebar state to localStorage
2611
- */
2612
- saveSidebarState(collapsed) {
2613
- try {
2614
- const key = this.getSidebarStorageKey();
2615
- localStorage.setItem(key, JSON.stringify(collapsed));
2616
- } catch (error) {
2617
- console.warn("Failed to save sidebar state:", error);
2618
- }
2619
- }
2620
- /**
2621
- * Load sidebar state from localStorage
2622
- */
2623
- loadSidebarState() {
2624
- try {
2625
- const key = this.getSidebarStorageKey();
2626
- const saved = localStorage.getItem(key);
2627
- return saved !== null ? JSON.parse(saved) : null;
2628
- } catch (error) {
2629
- console.warn("Failed to load sidebar state:", error);
2630
- return null;
2631
- }
2632
- }
2633
- /**
2634
- * Get storage key for sidebar state (allows multiple apps on same domain)
2635
- */
2636
- getSidebarStorageKey() {
2637
- const appKey = this.title ? this.title.replace(/\s+/g, "_").toLowerCase() : "portal_app";
2638
- return `${appKey}_sidebar_collapsed`;
2639
- }
2640
- /**
2641
- * Apply saved sidebar state to the UI
2642
- */
2643
- applySidebarState(container = null) {
2644
- if (!container) {
2645
- container = document.querySelector(".portal-container");
2646
- }
2647
- if (!container) return;
2648
- if (this.sidebarCollapsed) {
2649
- container.classList.add("collapse-sidebar");
2650
- } else {
2651
- container.classList.remove("collapse-sidebar");
2652
- }
2653
- }
2654
- /**
2655
- * Clear saved sidebar state
2656
- */
2657
- clearSidebarState() {
2658
- try {
2659
- const key = this.getSidebarStorageKey();
2660
- localStorage.removeItem(key);
2661
- } catch (error) {
2662
- console.warn("Failed to clear sidebar state:", error);
2663
- }
2664
- }
2665
- async changePassword() {
2666
- const data = await this.showForm({
2667
- title: "Change Password",
2668
- fields: [
2669
- {
2670
- name: "current_password",
2671
- type: "password",
2672
- label: "Current Password",
2673
- required: true,
2674
- showToggle: true,
2675
- // default, can omit
2676
- strengthMeter: true,
2677
- capsLockWarning: true
2678
- },
2679
- {
2680
- name: "new_password",
2681
- type: "password",
2682
- label: "New Password",
2683
- required: true,
2684
- showToggle: true,
2685
- passwordUsage: "new",
2686
- // sets autocomplete to 'new-password'
2687
- strengthMeter: true,
2688
- capsLockWarning: true,
2689
- attributes: {
2690
- // optional, override autocomplete if needed
2691
- autocomplete: "new-password"
2692
- }
2693
- },
2694
- {
2695
- name: "confirm_password",
2696
- type: "password",
2697
- label: "Confirm Password",
2698
- required: true,
2699
- showToggle: true,
2700
- passwordUsage: "new",
2701
- // sets autocomplete to 'new-password'
2702
- strengthMeter: true,
2703
- capsLockWarning: true,
2704
- attributes: {
2705
- // optional, override autocomplete if needed
2706
- // autocomplete: 'new-password'
2707
- }
2708
- }
2709
- ],
2710
- submitLabel: "Change Password"
2711
- });
2712
- if (data) {
2713
- if (data.new_password === data.confirm_password) {
2714
- const resp = await this.activeUser.save(data);
2715
- if (resp.status === 200) {
2716
- this.toast.success("Password changed successfully");
2717
- } else {
2718
- this.toast.error("Failed to change password");
2719
- }
2720
- } else {
2721
- this.toast.error("Passwords do not match");
2722
- }
2723
- }
2724
- }
2725
- onPortalAction(action) {
2726
- switch (action.action) {
2727
- case "logout":
2728
- this.tokenManager.clearTokens();
2729
- this.rest.clearAuth();
2730
- this.setActiveUser(null);
2731
- break;
2732
- case "profile":
2733
- this.showProfile();
2734
- break;
2735
- case "change-password":
2736
- this.changePassword();
2737
- break;
2738
- default:
2739
- console.warn(`Unknown portal action: ${action}`);
2740
- }
2741
- }
2742
- async showProfile() {
2743
- if (!this.activeUser) {
2744
- this.showError("No user is currently logged in");
2745
- return;
2746
- }
2747
- try {
2748
- const result = await Dialog.showModelForm({
2749
- title: "Edit Profile",
2750
- size: "lg",
2751
- fileHandling: "base64",
2752
- model: this.activeUser,
2753
- fields: [
2754
- // Profile Header
2755
- {
2756
- type: "header",
2757
- text: "Profile Information",
2758
- level: 4,
2759
- class: "text-primary mb-3"
2760
- },
2761
- // Avatar and Basic Info
2762
- {
2763
- type: "group",
2764
- columns: { xs: 12, md: 4 },
2765
- title: "Avatar",
2766
- fields: [
2767
- {
2768
- type: "image",
2769
- name: "avatar",
2770
- size: "lg",
2771
- imageSize: { width: 200, height: 200 },
2772
- placeholder: "Upload your avatar",
2773
- help: "Square images work best"
2774
- }
2775
- ]
2776
- },
2777
- // Profile Details
2778
- {
2779
- type: "group",
2780
- columns: { xs: 12, md: 8 },
2781
- title: "Details",
2782
- fields: [
2783
- {
2784
- type: "text",
2785
- name: "display_name",
2786
- label: "Display Name",
2787
- required: true,
2788
- columns: 12,
2789
- placeholder: "Enter first name"
2790
- },
2791
- {
2792
- type: "email",
2793
- name: "email",
2794
- label: "Email Address",
2795
- required: true,
2796
- columns: 8,
2797
- placeholder: "your.email@example.com"
2798
- },
2799
- {
2800
- type: "tel",
2801
- name: "phone_number",
2802
- label: "Phone Number",
2803
- columns: 4,
2804
- placeholder: "(555) 123-4567"
2805
- }
2806
- ]
2807
- },
2808
- // Account Settings
2809
- {
2810
- type: "group",
2811
- columns: 12,
2812
- title: "Account Settings",
2813
- class: "pt-3",
2814
- fields: [
2815
- {
2816
- type: "select",
2817
- name: "timezone",
2818
- label: "Timezone",
2819
- columns: 6,
2820
- options: [
2821
- { value: "America/New_York", text: "Eastern Time" },
2822
- { value: "America/Chicago", text: "Central Time" },
2823
- { value: "America/Denver", text: "Mountain Time" },
2824
- { value: "America/Los_Angeles", text: "Pacific Time" },
2825
- { value: "UTC", text: "UTC" }
2826
- ]
2827
- },
2828
- {
2829
- type: "select",
2830
- name: "language",
2831
- label: "Language",
2832
- columns: 6,
2833
- options: [
2834
- { value: "en", text: "English" },
2835
- { value: "es", text: "Spanish" },
2836
- { value: "fr", text: "French" },
2837
- { value: "de", text: "German" }
2838
- ]
2839
- },
2840
- {
2841
- type: "switch",
2842
- name: "email_notifications",
2843
- label: "Email Notifications",
2844
- columns: 4
2845
- },
2846
- {
2847
- type: "switch",
2848
- name: "two_factor_enabled",
2849
- label: "Two-Factor Authentication",
2850
- columns: 4
2851
- },
2852
- {
2853
- type: "switch",
2854
- name: "profile_public",
2855
- label: "Public Profile",
2856
- columns: 4
2857
- }
2858
- ]
2859
- }
2860
- ],
2861
- submitText: "Save Profile",
2862
- cancelText: "Cancel"
2863
- });
2864
- if (result && result.success) {
2865
- this.showSuccess("Profile updated successfully!");
2866
- } else if (result && !result.success) {
2867
- }
2868
- } catch (error) {
2869
- console.error("Error showing profile form:", error);
2870
- this.showError("Failed to load profile form");
2871
- }
2872
- }
2873
- /**
2874
- * Clean up portal resources
2875
- */
2876
- async destroy() {
2877
- this.activeGroup = null;
2878
- if (this._resizeObserver) {
2879
- this._resizeObserver.disconnect();
2880
- }
2881
- if (this._resizeHandler) {
2882
- window.removeEventListener("resize", this._resizeHandler);
2883
- }
2884
- if (this.topbar) {
2885
- await this.topbar.destroy();
2886
- this.topbar = null;
2887
- this.topnav = null;
2888
- }
2889
- if (this.sidebar) {
2890
- await this.sidebar.destroy();
2891
- this.sidebar = null;
2892
- }
2893
- await super.destroy();
2894
- }
2895
- /**
2896
- * Static factory method
2897
- */
2898
- static create(config = {}) {
2899
- return new PortalApp(config);
2900
- }
2901
- }
2902
- class FormPage extends Page {
2903
- constructor(options = {}) {
2904
- super({
2905
- title: "Form Page",
2906
- description: "A page for submitting forms",
2907
- icon: "form",
2908
- fields: [],
2909
- template: '<div data-container="form-view-container"></div>',
2910
- className: "form-page container-sm",
2911
- ...options
2912
- });
2913
- }
2914
- async onInit() {
2915
- await super.onInit();
2916
- this.formView = new FormView({
2917
- containerId: "form-view-container",
2918
- fields: this.options.fields,
2919
- autosaveModelField: true
2920
- });
2921
- this.addChild(this.formView);
2922
- if (this.getApp().activeGroup) {
2923
- this.formView.setModel(this.getApp().activeGroup);
2924
- }
2925
- }
2926
- async onEnter() {
2927
- await super.onEnter();
2928
- if (this.formView) {
2929
- await this.recreateFormView();
2930
- }
2931
- }
2932
- async onGroupChange(group) {
2933
- if (this.formView) {
2934
- await this.recreateFormView();
2935
- }
2936
- }
2937
- async recreateFormView() {
2938
- if (this.formView) {
2939
- await this.formView.destroy();
2940
- this.removeChild(this.formView);
2941
- }
2942
- this.formView = new FormView({
2943
- containerId: "form-view-container",
2944
- fields: this.options.fields,
2945
- autosaveModelField: true
2946
- });
2947
- this.addChild(this.formView);
2948
- if (this.getApp().activeGroup) {
2949
- this.formView.setModel(this.getApp().activeGroup);
2950
- }
2951
- }
2952
- }
2953
- class MustacheFormatter {
2954
- constructor() {
2955
- this.formatter = dataFormatter;
2956
- this.compiledTemplates = /* @__PURE__ */ new Map();
2957
- }
2958
- /**
2959
- * Render template with data
2960
- * Pipes are now handled by Model.get() and View.get() automatically
2961
- *
2962
- * @param {string} template - Mustache template
2963
- * @param {object} data - Data to render (View, Model, or plain object)
2964
- * @param {object} partials - Mustache partials
2965
- * @returns {string} Rendered template
2966
- */
2967
- render(template, data, partials = {}) {
2968
- return Mustache.render(template, data, partials);
2969
- }
2970
- /**
2971
- * Compile template for reuse
2972
- * @param {string} template - Template to compile
2973
- * @returns {object} Compiled template tokens
2974
- */
2975
- compile(template) {
2976
- const compiled = Mustache.parse(template);
2977
- this.compiledTemplates.set(template, compiled);
2978
- return compiled;
2979
- }
2980
- /**
2981
- * Render with compiled template
2982
- * @param {object} compiled - Compiled template tokens
2983
- * @param {object} data - Data to render
2984
- * @param {object} partials - Mustache partials
2985
- * @returns {string} Rendered template
2986
- */
2987
- renderCompiled(compiled, data, partials = {}) {
2988
- return Mustache.render(compiled, data, partials);
2989
- }
2990
- /**
2991
- * Clear compiled template cache
2992
- */
2993
- clearCache() {
2994
- this.compiledTemplates.clear();
2995
- Mustache.clearCache();
2996
- }
2997
- /**
2998
- * Process and cache a template
2999
- * @param {string} key - Cache key
3000
- * @param {string} template - Template to cache
3001
- * @returns {object} Cached template info
3002
- */
3003
- cache(key, template) {
3004
- const compiled = this.compile(template);
3005
- return { key, template, compiled };
3006
- }
3007
- /**
3008
- * Get cached template
3009
- * @param {string} key - Cache key
3010
- * @returns {object|null} Cached template info or null
3011
- */
3012
- getCached(key) {
3013
- for (const [template, compiled] of this.compiledTemplates) {
3014
- if (template === key || compiled === key) {
3015
- return { key, template, compiled };
3016
- }
3017
- }
3018
- return null;
3019
- }
3020
- /**
3021
- * Register a custom formatter with DataFormatter
3022
- * @param {string} name - Formatter name
3023
- * @param {function} formatter - Formatter function
3024
- * @returns {MustacheFormatter} This instance for chaining
3025
- */
3026
- registerFormatter(name, formatter) {
3027
- this.formatter.register(name, formatter);
3028
- return this;
3029
- }
3030
- /**
3031
- * Check if a string contains pipe syntax
3032
- * @param {string} template - Template string to check
3033
- * @returns {boolean} True if contains pipes
3034
- */
3035
- hasPipes(template) {
3036
- return /\{\{[{]?[^}|]+\|[^}]+\}[}]?\}/.test(template);
3037
- }
3038
- /**
3039
- * Pre-process data with pipe formatters
3040
- * This is now handled automatically by get() methods, but kept for backward compatibility
3041
- *
3042
- * @param {object} data - Data object
3043
- * @param {object} pipes - Object mapping keys to pipe strings
3044
- * @returns {object} Processed data
3045
- */
3046
- processData(data, pipes) {
3047
- const processed = { ...data };
3048
- for (const [key, pipeString] of Object.entries(pipes)) {
3049
- if (data && typeof data.get === "function") {
3050
- processed[key] = data.get(`${key}|${pipeString}`);
3051
- } else {
3052
- const value = this.getValueFromPath(data, key);
3053
- processed[key] = this.formatter.pipe(value, pipeString);
3054
- }
3055
- }
3056
- return processed;
3057
- }
3058
- /**
3059
- * Get value from object using dot notation path
3060
- * Kept for backward compatibility, but MOJOUtils.getContextData is preferred
3061
- *
3062
- * @param {object} obj - Source object
3063
- * @param {string} path - Dot notation path
3064
- * @returns {*} Value at path
3065
- */
3066
- getValueFromPath(obj, path) {
3067
- if (!obj || !path) return void 0;
3068
- if (obj && typeof obj.get === "function") {
3069
- return obj.get(path);
3070
- }
3071
- const keys = path.split(".");
3072
- let current = obj;
3073
- for (const key of keys) {
3074
- if (current === null || current === void 0) {
3075
- return void 0;
3076
- }
3077
- if (!isNaN(key) && Array.isArray(current)) {
3078
- current = current[parseInt(key)];
3079
- } else {
3080
- current = current[key];
3081
- }
3082
- }
3083
- return current;
3084
- }
3085
- /**
3086
- * Process template to handle pipe formatters
3087
- * @deprecated Pipes are now handled by get() methods automatically
3088
- * @param {string} template - Original template
3089
- * @param {object} data - Original data
3090
- * @returns {object} {template: processedTemplate, data: processedData}
3091
- */
3092
- processTemplate(template, data) {
3093
- return { template, data };
3094
- }
3095
- }
3096
- const mustacheFormatter = new MustacheFormatter();
3097
- ConsoleSilencer.install({ level: "warn" });
3098
- const FRAMEWORK_NAME = "MOJO";
3099
- const PACKAGE_NAME = "web-mojo";
3100
- const index = {
3101
- FRAMEWORK_NAME,
3102
- PACKAGE_NAME
3103
- };
3104
- export {
3105
- B as BUILD_TIME,
3106
- a1 as BundleByOptions,
3107
- e2 as ChatInputView,
3108
- d3 as ChatMessageView,
3109
- C3 as ChatView,
3110
- C2 as Collection,
3111
- a5 as CommonEventFields,
3112
- a6 as CommonScopeOptions,
3113
- a3 as ComparatorOptions,
3114
- ConsoleSilencer,
3115
- C as ContextMenu,
3116
- default2 as DataView,
3117
- D as DataWrapper,
3118
- Dialog,
3119
- D2 as DjangoLookups,
3120
- E3 as EmailDomain,
3121
- k as EmailDomainForms,
3122
- j as EmailDomainList,
3123
- s as EmailTemplate,
3124
- u as EmailTemplateForms,
3125
- t as EmailTemplateList,
3126
- E2 as EventBus,
3127
- E as EventDelegate,
3128
- FRAMEWORK_NAME,
3129
- y as File,
3130
- A as FileForms,
3131
- z as FileList,
3132
- v as FileManager,
3133
- x as FileManagerForms,
3134
- w as FileManagerList,
3135
- F as FilePreviewView,
3136
- f2 as FileUpload,
3137
- FormPage,
3138
- FormView,
3139
- az as GeoLocatedIP,
3140
- aA as GeoLocatedIPList,
3141
- Group,
3142
- b2 as GroupForms,
3143
- GroupList,
3144
- H as Incident,
3145
- I as IncidentEvent,
3146
- G as IncidentEventForms,
3147
- B2 as IncidentEventList,
3148
- K as IncidentForms,
3149
- U as IncidentHistory,
3150
- V2 as IncidentHistoryList,
3151
- J as IncidentList,
3152
- Q as IncidentRule,
3153
- R2 as IncidentRuleList,
3154
- N as IncidentRuleSet,
3155
- O as IncidentRuleSetList,
3156
- a0 as IncidentStats,
3157
- a7 as Job,
3158
- ac as JobEvent,
3159
- ad as JobEventList,
3160
- a9 as JobForms,
3161
- a8 as JobList,
3162
- aa as JobLog,
3163
- ab as JobLogList,
3164
- af as JobRunner,
3165
- ah as JobRunnerForms,
3166
- ag as JobRunnerList,
3167
- ae as JobsEngineStats,
3168
- L as LOOKUPS,
3169
- L2 as ListView,
3170
- a10 as ListViewItem,
3171
- ai as Log,
3172
- aj as LogList,
3173
- a2 as MOJOUtils,
3174
- l as Mailbox,
3175
- n as MailboxForms,
3176
- m as MailboxList,
3177
- a22 as MatchByOptions,
3178
- Member,
3179
- al as MemberForms,
3180
- ak as MemberList,
3181
- ao as MetricsForms,
3182
- am as MetricsPermission,
3183
- an as MetricsPermissionList,
3184
- M as Model,
3185
- mustacheFormatter as MustacheFormatter,
3186
- PACKAGE_NAME,
3187
- Page,
3188
- PortalApp,
3189
- P as ProgressView,
3190
- at as PushConfig,
3191
- ax as PushConfigForms,
3192
- au as PushConfigList,
3193
- av as PushDelivery,
3194
- aw as PushDeliveryList,
3195
- ap as PushDevice,
3196
- aq as PushDeviceList,
3197
- ar as PushTemplate,
3198
- ay as PushTemplateForms,
3199
- as as PushTemplateList,
3200
- r as Rest,
3201
- R as Router,
3202
- Z as Rule,
3203
- $ as RuleForms,
3204
- _ as RuleList,
3205
- W as RuleSet,
3206
- Y as RuleSetForms,
3207
- X as RuleSetList,
3208
- S as S3Bucket,
3209
- i2 as S3BucketForms,
3210
- h2 as S3BucketList,
3211
- o as SentMessage,
3212
- r2 as SentMessageForms,
3213
- q as SentMessageList,
3214
- Sidebar,
3215
- SimpleSearchView,
3216
- c3 as TabView,
3217
- b3 as TablePage,
3218
- a4 as TableRow,
3219
- T as TableView,
3220
- aB as Ticket,
3221
- aG as TicketCategories,
3222
- aF as TicketForms,
3223
- aC as TicketList,
3224
- aD as TicketNote,
3225
- aE as TicketNoteList,
3226
- ToastService,
3227
- TokenManager,
3228
- TopNav,
3229
- User,
3230
- e as UserDataView,
3231
- f as UserDevice,
3232
- g as UserDeviceList,
3233
- h as UserDeviceLocation,
3234
- i as UserDeviceLocationList,
3235
- d2 as UserForms,
3236
- c2 as UserList,
3237
- a as VERSION,
3238
- V as VERSION_INFO,
3239
- b as VERSION_MAJOR,
3240
- c as VERSION_MINOR,
3241
- d as VERSION_REVISION,
3242
- a42 as ValueTypeOptions,
3243
- View,
3244
- WebApp,
3245
- W2 as WebSocketClient,
3246
- a11 as applyFileDropMixin,
3247
- dataFormatter,
3248
- index as default,
3249
- g2 as formatFilterDisplay,
3250
- installConsoleSilencer,
3251
- p as parseFilterKey
3252
- };
1
+ import{B as e,V as t,a as s,b as i,c as a,d as n}from"./chunks/version-CU1HG1XH.js";import{V as r,d as o,M as l}from"./chunks/Rest-DHbszkuP.js";import{D as c,E as d,a as h,r as u}from"./chunks/Rest-DHbszkuP.js";import{G as p,a as g,P as m,T as v,U as b}from"./chunks/ContextMenu-BvniQz-N.js";import{C as f,b as w,c as y,d as S,e as A,f as P,g as C,h as M,i as L}from"./chunks/ContextMenu-BvniQz-N.js";import{W as I}from"./chunks/WebApp-CULZpO_0.js";import{E as T,R as k}from"./chunks/WebApp-CULZpO_0.js";import{C as D,M as x}from"./chunks/Collection-1sPoIFvQ.js";import{M as E}from"./chunks/ChatView-CqkYoMmr.js";import{B as G,C as _,a as N,b as F,c as R,d as V,e as H,D as O,E as U,f as B,g as K,h as j,i as z,j as $,F as q,k as J,l as W,m as Y,n as Q,o as X,p as Z,q as ee,G as te,r as se,I as ie,s as ae,t as ne,u as re,v as oe,w as le,x as ce,y as de,z as he,A as ue,H as pe,J as ge,K as me,L as ve,N as be,O as fe,P as we,Q as ye,R as Se,S as Ae,T as Pe,U as Ce,V as Me,W as Le,X as Ie,Y as Te,Z as ke,_ as De,$ as xe,a0 as Ee,a1 as Ge,a2 as _e,a3 as Ne,a4 as Fe,a5 as Re,a6 as Ve,a7 as He,a8 as Oe,a9 as Ue,aa as Be,ab as Ke,ac as je,ad as ze,ae as $e,af as qe,ag as Je,ah as We,ai as Ye,aj as Qe,ak as Xe,al as Ze,am as et,an as tt,ao as st,ap as it,aq as at,ar as nt,as as rt,at as ot,au as lt,av as ct,aw as dt,ax as ht,ay as ut,az as pt,aA as gt,aB as mt,aC as vt,aD as bt,aE as ft,aF as wt,aG as yt}from"./chunks/ChatView-CqkYoMmr.js";import{S as St,T as At,a as Pt}from"./chunks/TokenManager-D6SjKgPZ.js";import Ct from"./chunks/Dialog-BcgSR01Z.js";import{L as Mt,a as Lt}from"./chunks/ListView-6JQ6tRXs.js";import{default as It}from"./chunks/DataView--nUWtq6r.js";import{F as Tt}from"./chunks/FormView-OLA7t-yv.js";import{a as kt}from"./chunks/FormView-OLA7t-yv.js";import{W as Dt}from"./chunks/WebSocketClient-B-wc3mez.js";const xt={BASE_URL:"/",DEV:!1,MODE:"production",PROD:!0,SSR:!1},Et=Object.freeze({silent:0,error:1,warn:2,info:3,log:3,debug:4,trace:5,all:5}),Gt=(()=>{try{if(void 0!==import.meta&&import.meta&&xt)return!1}catch{}if("undefined"!=typeof globalThis&&void 0!==globalThis.__DEV__)try{return!!globalThis.__DEV__}catch{}return!("undefined"==typeof process||!process||"object"!=typeof process.env||"string"!=typeof process.env.NODE_ENV)&&"production"!==process.env.NODE_ENV})(),_t="undefined"!=typeof window&&"undefined"!=typeof document,Nt="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:global,Ft=Nt.console||{},Rt={};let Vt=!1,Ht=null;function Ot(e){if("number"==typeof e){const t=Et.silent,s=Et.trace;return Math.min(Math.max(e,t),s)}if("string"==typeof e){const t=e.toLowerCase();if(Object.prototype.hasOwnProperty.call(Et,t))return Et[t]}return null}function Ut(e,t){const s=Rt[e]||Ft[e]||(()=>{});return function(...e){if(Ht>=t)return s.apply(Ft,e)}}const Bt={install(e={}){if(Vt)return e&&void 0!==e.level&&this.setLevel(e.level,{persist:!!e.persist}),this;if(!Nt||!Ft)return Vt=!0,this;Ht=function(e){const t=Ot(e);if(null!==t)return t;const s=function(){if(!_t||"undefined"==typeof location||!location.search)return null;try{const e=new URLSearchParams(location.search),t=["logLevel","loglevel","mojoLog"];for(const s of t){const t=e.get(s);if(null!=t){const e=Ot(t);if(null!==e)return e}}}catch{}return null}();if(null!==s)return s;const i=function(){if(!_t||!("localStorage"in Nt))return null;try{const e=Nt.localStorage.getItem("MOJO_LOG_LEVEL");if(null!=e){const t=Ot(e);if(null!==t)return t}}catch{}return null}();return null!==i?i:Ot(Gt?"debug":"warn")}(e.level);const t=function(){const e={...Ft},t={error:Et.error,warn:Et.warn,info:Et.info,log:Et.info,dir:Et.info,table:Et.info,debug:Et.debug,group:Et.debug,groupCollapsed:Et.debug,groupEnd:Et.debug,time:Et.debug,timeEnd:Et.debug,timeLog:Et.debug,trace:Et.trace};for(const s of Object.keys(t))Rt[s]=Ft[s]||(()=>{}),e[s]=Ut(s,t[s]);return Rt.assert=Ft.assert||(()=>{}),e.assert=function(){const e=Rt.assert||Ft.assert||(()=>{});return function(t,...s){if(!t)return Ht>=Et.error?e.apply(Ft,[t,...s]):void 0}}(),e}();return Nt.console=t,Vt=!0,Nt.MOJOConsoleSilencer=this,this},uninstall(){if(!Vt)return this;try{Nt.console=Ft}catch{}return Vt=!1,this},setLevel(e,{persist:t=!1}={}){const s=Ot(e);return null===s||(Ht=s,t&&function(e){if(_t&&"localStorage"in Nt)try{const t="string"==typeof e?e:null===e?null:Object.entries(Et).find(([,t])=>t===e)?.[0]??null;t?Nt.localStorage.setItem("MOJO_LOG_LEVEL",t):Nt.localStorage.removeItem("MOJO_LOG_LEVEL")}catch{}}(e)),this},getLevel:()=>Ht,getLevelName(){const e=Object.entries(Et).find(([,e])=>e===Ht);return e?e[0]:null},criticalOnly({persist:e=!1}={}){return this.setLevel("warn",{persist:e})},errorsOnly({persist:e=!1}={}){return this.setLevel("error",{persist:e})},silent({persist:e=!1}={}){return this.setLevel("silent",{persist:e})},verbose({persist:e=!1}={}){return this.setLevel(Gt?"debug":"info",{persist:e})},allowAll({persist:e=!1}={}){return this.setLevel("trace",{persist:e})},withTemporaryLevel(e,t){const s=Ht,i=Ot(e);if(null===i||"function"!=typeof t)return t?.();Ht=i;try{return t()}finally{Ht=s}},LEVELS:Et},Kt=e=>Bt.install(e);class GroupSearchView extends St{constructor(e={}){super({...e,className:`group-search-view ${e.className||""}`.trim()}),this.showKind=void 0===e.showKind||e.showKind,this.parentField=e.parentField||"parent",this.kindField=e.kindField||"kind",this.treeData=[],this.flattenedItems=[],this.showLines=void 0===e.showLines||e.showLines}buildTreeHierarchy(e){if(!e||0===e.length)return[];const t=/* @__PURE__ */new Map;e.forEach(e=>{t.has(e.id)||t.set(e.id,{...e,children:[],level:0,hasChildren:!1});const s=e[this.parentField];s&&s.id&&!t.has(s.id)&&t.set(s.id,{...s,children:[],level:0,hasChildren:!1})});const s=[];t.forEach((i,a)=>{const n=e.find(e=>e.id===a)||i,r=n[this.parentField]?.id;if(r&&t.has(r)){const e=t.get(r);e.children.push(i),e.hasChildren=!0}else s.push(i)});const i=(e,t=0)=>{e.forEach(e=>{e.level=t,e.children.length>0&&(e.children.sort((e,t)=>(e.name||"").localeCompare(t.name||"")),i(e.children,t+1))})};return s.sort((e,t)=>(e.name||"").localeCompare(t.name||"")),i(s),s}flattenTree(e,t=[],s=[]){return e.forEach((i,a)=>{i._isLastChild=a===e.length-1,i._ancestorLastFlags=[...s];const n=s.every(e=>e);if(i._isLastDescendant=n&&i._isLastChild&&(!i.children||0===i.children.length),t.push(i),i.children&&i.children.length>0){const e=[...s,i._isLastChild];this.flattenTree(i.children,t,e)}}),t}computeVerticalLines(e){for(let t=0;t<e.length;t++){const s=e[t];s._continueVertical=[];for(let i=0;i<s.level;i++){let a=!1;for(let s=t+1;s<e.length;s++){const t=e[s];if(t.level===i+1){a=!0;break}if(t.level<=i)break}s._continueVertical[i]=a}}}updateFilteredItems(){if(!this.collection)return this.filteredItems=[],this.treeData=[],void(this.flattenedItems=[]);const e=this.collection.toJSON();this.treeData=this.buildTreeHierarchy(e),this.flattenedItems=this.flattenTree(this.treeData),this.computeVerticalLines(this.flattenedItems),this.filteredItems=this.flattenedItems,this.updateResultsView()}getDefaultItemTemplate(){return'\n <div class="tree-item-content">\n <div class="tree-item-name">{{name}}</div>\n {{#showKind}}\n <div class="tree-item-kind">{{kind}}</div>\n {{/showKind}}\n </div>\n '}processItemTemplate(e){let t=this.itemTemplate;t=t.replace(/\{\{(\w+)\}\}/g,(t,s)=>"showKind"===s?this.showKind?"true":"":this.getNestedValue(e,s)||""),t=this.showKind?t.replace(/\{\{#showKind\}\}(.*?)\{\{\/showKind\}\}/gs,"$1"):t.replace(/\{\{#showKind\}\}.*?\{\{\/showKind\}\}/gs,"");let s="";if(this.showLines&&e.level>0)for(let i=0;i<e.level;i++)i===e.level-1?s+=`<span class="${e._isLastChild?"tree-seg tree-seg-last":"tree-seg tree-seg-mid"}"></span>`:s+=e._continueVertical&&e._continueVertical[i]?'<span class="tree-seg tree-seg-vert"></span>':'<span class="tree-seg"></span>';return`\n <div class="tree-item-wrapper${e.hasChildren?" has-children":""}${e._isLastChild?" is-last-child":""}" data-tree-level="${e.level}">\n <div class="tree-lines">\n ${s}\n </div>\n <div class="tree-item-body flex-grow-1">\n ${t}\n </div>\n </div>\n `}getRootItems(){return this.treeData}getNodeChildren(e){const t=(e,s)=>{for(const i of e){if(i.id===s)return i.children;if(i.children.length>0){const e=t(i.children,s);if(e)return e}}return null};return t(this.treeData,e)||[]}}class Sidebar extends r{constructor(e={}){super({tagName:"nav",className:"sidebar",id:"sidebar",...e}),this.menus=/* @__PURE__ */new Map,this.activeMenuName=null,this.currentRoute=null,this.showToggle=e.showToggle,this.isCollapsed=!1,this.sidebarTheme=e.theme||"sidebar-light",this.customView=null,this.options.groupHeader&&(this.groupHeader=this.options.groupHeader),this.groupSelectorMode=e.groupSelectorMode||"inline",this.groupSelectorDialog=null,this.sidebarTheme&&this.addClass(this.sidebarTheme),this.initializeMenus(e),this.setupRouteListeners(),!1!==e.autoCollapseMobile&&this.setupResponsiveBehavior()}groupHeader='\n {{#group.parent}}\n <div class="sidebar-parent-bar" data-action="select-group-parent">\n <div class="parent-info">\n <span class="parent-label">{{group.parent.kind}}:</span>\n <span class="parent-name collapsed-hidden">{{group.parent.name}}</span>\n </div>\n <i class="bi bi-chevron-down parent-expand collapsed-hidden"></i>\n </div>\n {{/group.parent}}\n <div class="sidebar-selected-group-row" data-action="show-group-search">\n <div class="selected-group-info">\n <div class=\'selected-group-name collapsed-hidden\'>{{group.name}}</div>\n <div class=\'selected-group-meta collapsed-hidden\'>\n <span class="selected-group-kind">{{group.kind}}</span>\n </div>\n </div>\n <i class="bi bi-chevron-down selected-group-chevron collapsed-hidden"></i>\n </div>\n ';async onInit(){await super.onInit();const e=this.getApp(),t=e?.router;if(t){const e=t.getCurrentPath();e&&this.autoSwitchToMenuForRoute(e)}this.initializeTooltips(),this.searchView=new GroupSearchView({noAppend:!0,showExitButton:!0,headerText:"Select Group",containerId:"sidebar-search-container",Collection:p,itemTemplate:'\n <div class="p-3 border-bottom">\n <div class="fw-semibold text-dark">{{name}}</div>\n <small class="text-muted">#{{id}} {{kind}}</small>\n </div>\n '}),this.addChild(this.searchView),this.searchView.on("item:selected",e=>{this.getApp().setActiveGroup(e.model)}),this.searchView.on("exit",e=>{this.hideGroupSearch()})}showGroupSearch(){"dialog"===this.groupSelectorMode?this.showGroupSearchDialog():(this.setClass("sidebar"),this.showSearch=!0,this.render())}hideGroupSearch(){"dialog"===this.groupSelectorMode?this.groupSelectorDialog&&this.groupSelectorDialog.hide():(this.setClass("sidebar"),this.showSearch=!1,this.render())}onActionShowGroupSearch(){this.showGroupSearch()}async onActionSelectGroupParent(){const e=this.getApp().activeGroup;if(await Ct.confirm(`Are you sure you want to navigate to the '${e.get("parent.name")}'?`)){this.getApp().showLoading();let t=new g({id:e.get("parent.id")});await t.fetch(),this.getApp().setActiveGroup(t),this.getApp().hideLoading()}}async showGroupSearchDialog(){const e=new p,t=new GroupSearchView({Collection:p,collection:e,searchFields:["name"],headerText:null,searchPlaceholder:"Search groups...",headerIcon:null,maxHeight:Math.min(600,window.innerHeight-200),showExitButton:!1,showKind:!0,parentField:"parent",kindField:"kind",autoExpandRoot:!0,autoExpandAll:!1,indentSize:20,showLines:!0});this.groupSelectorDialog=new Ct({body:t,size:"md",header:null,noBodyPadding:!0,scrollable:!1,buttons:[],closeButton:!0}),t.on("item:selected",e=>{this.getApp().setActiveGroup(e.model),this.groupSelectorDialog&&this.groupSelectorDialog.hide()}),this.groupSelectorDialog.on("hidden",()=>{this.groupSelectorDialog.destroy(),this.groupSelectorDialog=null}),await this.groupSelectorDialog.render(!0,document.body),this.groupSelectorDialog.show()}autoSwitchToMenuForRoute(e){for(const[t,s]of this.menus)if((!s.groupKind||this.getApp().activeGroup)&&this.menuContainsRoute(s,e))return this._setActiveMenu(t),this.currentRoute=e,this.clearAllActiveStates(),this.setActiveItemByRoute(e),this.render(),this.emit("menu-auto-switched",{menuName:t,route:e,config:s,sidebar:this}),!0;return!1}clearAllActiveStates(){for(const[e,t]of this.menus)for(const s of t.items||[])if(s.active=!1,s.children)for(const e of s.children)e.active=!1}setActiveItemByRoute(e){const t=e=>{if(!e)return"/";const t=decodeURIComponent(e);return t.startsWith("/")?t:`/${t}`},s=t(e);for(const[i,a]of this.menus)if(!a.groupKind||this.getApp().activeGroup)for(const e of a.items||[]){if(e.route){const i=t(e.route);if(this.routesMatch(s,i))return e.active=!0,this.activeMenuItem=e,!0}if(e.children)for(const i of e.children)if(i.route){const a=t(i.route);if(this.routesMatch(s,a))return i.active=!0,e.active=!0,!0}}return!1}menuContainsRoute(e,t){const s=e=>{if(!e)return"/";const t=decodeURIComponent(e);return t.startsWith("/")?t:`/${t}`},i=s(t);for(const a of e.items||[]){if(a.route){const e=s(a.route);if(this.routesMatch(i,e))return!0}if(a.children)for(const e of a.children)if(e.route){const t=s(e.route);if(this.routesMatch(i,t))return!0}}return!1}routesMatch(e,t){return this.getApp().router.doRoutesMatch(e,t)}getTemplate(){return this.customView?'<div class="sidebar-container" id="sidebar-custom-view-container"></div>':this.showSearch?this.getSearchTemplate():this.getMenuTemplate()}getSearchTemplate(){return'\n <div class="sidebar-container" id="sidebar-search-container">\n </div>\n '}getMenuTemplate(){return'\n <div class="sidebar-container">\n {{#data.currentMenu}}\n \x3c!-- Header --\x3e\n {{#header}}\n <div class="sidebar-header">\n {{{header}}}\n {{#showToggle}}\n <button class="sidebar-toggle" data-action="toggle-sidebar"\n aria-label="Toggle Sidebar">\n <i class="bi bi-chevron-left toggle-icon"></i>\n <i class="bi bi-chevron-right toggle-icon"></i>\n </button>\n {{/showToggle}}\n </div>\n {{/header}}\n\n \x3c!-- Navigation Items --\x3e\n <div class="sidebar-body">\n <ul class="nav nav-pills flex-column sidebar-nav" id="sidebar-nav-menu">\n {{#items}}\n {{>nav-item}}\n {{/items}}\n </ul>\n </div>\n\n \x3c!-- Footer --\x3e\n {{#footer}}\n <div class="sidebar-footer">\n {{{footer}}}\n </div>\n {{/footer}}\n {{/data.currentMenu}}\n\n {{^data.currentMenu}}\n <div class="sidebar-empty">\n <p class="text-danger text-center">No menu configured</p>\n </div>\n {{/data.currentMenu}}\n </div>\n '}getPartials(){return{"nav-item":'\n {{#isDivider}}\n {{>nav-divider}}\n {{/isDivider}}\n {{#isSpacer}}\n {{>nav-spacer}}\n {{/isSpacer}}\n {{#isLabel}}\n {{>nav-label}}\n {{/isLabel}}\n\n {{^isDivider}}\n {{^isSpacer}}\n {{^isLabel}}\n <li class="nav-item">\n {{#hasChildren}}\n \x3c!-- Item with submenu --\x3e\n <a class="nav-link {{#active}}active{{/active}} has-children collapsed"\n data-bs-toggle="collapse"\n href="#collapse-{{id}}"\n role="button"\n aria-expanded="{{#active}}true{{/active}}{{^active}}false{{/active}}"\n data-action="toggle-submenu">\n {{#icon}}<i class="{{icon}} me-2"></i>{{/icon}}\n <span class="nav-text">{{text}}</span>\n {{#badge}}\n <span class="{{badge.class}} ms-auto">{{badge.text}}</span>\n {{/badge}}\n <i class="bi bi-chevron-down nav-arrow ms-auto"></i>\n </a>\n <div class="collapse {{#active}}show{{/active}}" id="collapse-{{id}}" data-bs-parent="#sidebar-nav-menu">\n <ul class="nav flex-column nav-submenu">\n {{#children}}\n <li class="nav-item">\n <a class="nav-link {{#active}}active{{/active}}"\n {{#action}}data-action="{{action}}"{{/action}}\n {{#href}}href="{{href}}"{{/href}}>\n {{#icon}}<i class="{{icon}} me-2"></i>{{/icon}}\n <span class="nav-text">{{text}}</span>\n {{#badge}}\n <span class="{{badge.class}} ms-auto">{{badge.text}}</span>\n {{/badge}}\n </a>\n </li>\n {{/children}}\n </ul>\n </div>\n {{/hasChildren}}\n {{^hasChildren}}\n \x3c!-- Simple item --\x3e\n <a class="nav-link {{#active}}active{{/active}} {{#disabled}}disabled{{/disabled}}"\n {{#action}}{{^disabled}}data-action="{{action}}"{{/disabled}}{{/action}}\n {{#href}}{{^disabled}}href="{{href}}"{{/disabled}}{{/href}}>\n {{#icon}}<i class="{{icon}} me-2"></i>{{/icon}}\n <span class="nav-text">{{text}}</span>\n {{#badge}}\n <span class="{{badge.class}} ms-auto">{{badge.text}}</span>\n {{/badge}}\n </a>\n {{/hasChildren}}\n </li>\n {{/isLabel}}\n {{/isSpacer}}\n {{/isDivider}}\n ',"nav-divider":'\n <li class="nav-divider-item">\n <hr class="nav-divider-line">\n </li>\n ',"nav-spacer":'\n <li class="nav-spacer-item"></li>\n ',"nav-label":'\n <li class="nav-item {{className}}">\n <div class="nav-text px-3">{{text}}</div>\n </li>\n '}}getGroupHeader(){return this.groupHeader}addMenu(e,t){return t.groupKind&&!t.header&&(t.header=this.getGroupHeader()),this.menus.set(e,{name:e,groupKind:t.groupKind||null,header:t.header||null,footer:t.footer||null,items:t.items||[],data:t.data||{},className:t.className||"sidebar sidebar-dark"}),this.activeMenuName||this._setActiveMenu(e),this}_setActiveMenu(e){this.showSearch=!1,this.activeMenuName=e;const t=this.getCurrentMenuConfig();t.className?this.setClass(t.className):this.setClass("sidebar")}async setActiveMenu(e){if(!this.menus.has(e))return console.warn(`Menu '${e}' not found`),this;const t=this.menus.get(e);if(!t.groupKind||(this.lastGroupMenu=t,this.getApp().activeGroup))return this._setActiveMenu(e),await this.render(),this.emit("menu-changed",{menuName:e,config:t,sidebar:this}),this;this.showGroupSearch()}getGroupMenu(e){if(!e)return console.warn("No group provided"),null;let t=this.lastGroupMenu,s=null;if(e._.kind)for(const[i,a]of this.menus){if(this._groupKindMatches(a.groupKind,e._.kind)){t=a;break}"any"===a.groupKind&&(s=a)}return t||s}_groupKindMatches(e,t){return!(!e||!t)&&(Array.isArray(e)?e.includes(t):e===t)}showMenuForGroup(e){if(!e)return void console.warn("No group provided");let t=this.getGroupMenu(e);if(t)return this._setActiveMenu(t.name),this.render(),this.emit("menu-changed",{menuName:t.name,config:t,sidebar:this}),this;console.warn(`No menu found for group kind: ${e.kind}`)}getMenuConfig(e){return this.menus.get(e)||null}getCurrentMenuConfig(){return this.activeMenuName?this.menus.get(this.activeMenuName):null}updateMenu(e,t){const s=this.menus.get(e);return s?(Object.assign(s,t),this.activeMenuName===e&&this.render(),this):(console.warn(`Menu '${e}' not found`),this)}removeMenu(e){if(this.menus.delete(e),this.activeMenuName===e){const e=Array.from(this.menus.keys());this.activeMenuName=e.length>0?e[0]:null,this.render()}return this}async onBeforeRender(){const e=this.getCurrentMenuConfig();if(!e)return{currentMenu:null};let t={version:this.getApp().version||null,group:this.getApp().activeGroup||null,user:this.getApp.activeUser||null};this.data={currentMenu:{header:this.renderTemplateString(e.header||"",t),footer:this.renderTemplateString(e.footer||"",t),items:this.processNavItems(e.items,e.groupKind),data:e.data,showToggle:this.showToggle}}}async onAfterRender(){this.isCollapsedState()?setTimeout(()=>this.initializeTooltips(),50):this.destroyTooltips()}setCustomView(e){return this.customView&&this.removeChild(this.customView.id),this.customView=e,e&&(e.containerId="sidebar-custom-view-container",this.addChild(e)),this.render(),this}clearCustomView(){return this.customView&&(this.removeChild(this.customView.id),this.customView=null),this.render(),this}processNavItems(e,t){const s=this.getApp(),i=s?.activeUser,a=s?.activeGroup,n=e=>{let s=e;if(e.startsWith("/")&&!e.includes("?")&&(s=`?page=${e.substring(1)||"home"}`),t&&a&&a.id){const e=s.includes("?")?"&":"?";return`${s}${e}group=${a.id}`}return s};return e.map((e,t)=>{if(""===e||"object"==typeof e&&e.divider)return{isDivider:!0,id:`divider-${t}`};if("object"==typeof e&&e.spacer)return{isSpacer:!0,id:`spacer-${t}`};const s={...e};if(s.permissions&&(!i||!i.hasPermission(s.permissions)))return null;if(s.requiresGroupKind){const e=a?._.kind||a?.kind;if(!e||!this._groupKindMatches(s.requiresGroupKind,e))return null}if("label"===s.kind)return s.isLabel=!0,s.id||(s.id=`nav-label-${t}`),s;if(s.id||(s.id=`nav-${t}`),s.route)s.href=n(s.route);else if(s.page){const e=s.page.startsWith("/")?s.page:`/${s.page}`;s.href=n(e),s.route=s.href}return s.children?(s.children=s.children.map(e=>{const t={...e};if(t.permissions&&i&&!i.hasPermission(t.permissions))return null;if(t.requiresGroupKind){const e=a?._.kind||a?.kind;if(!e||!this._groupKindMatches(t.requiresGroupKind,e))return null}if(t.route)t.href=n(t.route);else if(t.page){const e=t.page.startsWith("/")?t.page:`/${t.page}`;t.href=n(e),t.route=t.href}return t}).filter(e=>null!==e),s.hasChildren=!!(s.children&&s.children.length>0)):s.hasChildren=!1,s}).filter(e=>null!==e)}isItemActive(e){if(!e.route||!this.currentRoute)return!1;const t=e=>{if(!e)return"/";const t=decodeURIComponent(e);return t.startsWith("/")?t:`/${t}`},s=t(e.route),i=t(this.currentRoute);return"/"===s&&"/"===i||"/"!==s&&"/"!==i&&(i.startsWith(s)||i===s)}async updateActiveItem(e){return this.currentRoute=e,this.clearAllActiveStates(),this.setActiveItemByRoute(e),await this.render(),this}async handleActionToggleSubmenu(e,t){const s=t.querySelector(".nav-arrow");s&&s.classList.toggle("rotated")}async handleActionToggleSidebar(e,t){this.toggleSidebar()}onActionShowGroupMenu(e,t,s){return this.setActiveMenu("group_default"),!1}async onActionDefault(e,t,s){const i=this.getCurrentMenuConfig();if(!i)return;const a=i=>{for(const n of i){if(n.action==e&&n.handler)return n.handler(e,t,s,this.getApp()),!0;if(n.children&&n.children.length>0&&a(n.children))return!0}return!1};return a(i.items)}getMenuNames(){return Array.from(this.menus.keys())}hasMenu(e){return this.menus.has(e)}clearMenus(){return this.menus.clear(),this.activeMenuName=null,this.render(),this}setMenuData(e){const t=this.getCurrentMenuConfig();return t&&(t.data={...t.data,...e},this.render()),this}getMenuData(){const e=this.getCurrentMenuConfig();return e?e.data:{}}setupRouteListeners(){const e=this.getApp();e&&e.events&&(e.events.on(["page:showing"],e=>{this.onRouteChanged(e)}),e.events.on("group:changed",e=>{this.showMenuForGroup(e.group)}),e.events.on("portal:user-changed",e=>{this.render()}))}onRouteChanged(e){if(e.page&&e.page.route){const t=e.page.route;if(this.activeMenuItem&&this.routesMatch(t,this.activeMenuItem.route))return;this.autoSwitchToMenuForRoute(t)||(this.clearAllActiveStates(),this.setActiveItemByRoute(t),this.updateActiveItem(t))}}toggleSidebar(){const e=document.querySelector(".portal-container");if(!e)return;this.hideAllTooltips();const t=e.classList.contains("collapse-sidebar");return e.classList.contains("hide-sidebar")?(e.classList.remove("hide-sidebar"),this.isCollapsed=!1,this.destroyTooltips()):t?(e.classList.remove("collapse-sidebar"),this.isCollapsed=!1,this.destroyTooltips()):(e.classList.add("collapse-sidebar"),this.isCollapsed=!0,setTimeout(()=>this.initializeTooltips(),150)),this}setSidebarState(e){const t=document.querySelector(".portal-container");if(!t)return this;switch(t.classList.remove("collapse-sidebar","hide-sidebar"),e){case"collapsed":t.classList.add("collapse-sidebar"),this.isCollapsed=!0;break;case"hidden":t.classList.add("hide-sidebar"),this.isCollapsed=!1;break;default:this.isCollapsed=!1}return this.isCollapsed?(this.hideAllTooltips(),setTimeout(()=>this.initializeTooltips(),100)):this.destroyTooltips(),this}initializeTooltips(){return this.destroyTooltips(),this.isCollapsedState()?(this.element.querySelectorAll(".sidebar-nav .nav-link").forEach(e=>{const t=e.querySelector(".nav-text");if(t&&t.textContent.trim()){const s=t.textContent.trim();if(e.setAttribute("data-bs-toggle","tooltip"),e.setAttribute("data-bs-placement","right"),e.setAttribute("data-bs-title",s),e.setAttribute("data-bs-container","body"),window.bootstrap&&window.bootstrap.Tooltip){const t=e.getAttribute("data-tooltip-theme"),s=e.getAttribute("data-tooltip-size");let i="";t&&(i+=`tooltip-${t} `),s&&(i+=`tooltip-${s}`);const a={placement:"right",container:"body",trigger:"hover",delay:{show:500,hide:100},fallbackPlacements:["top","bottom","left"]},n=i.trim();n&&(a.customClass=n);const r=new window.bootstrap.Tooltip(e,a);e._tooltipInstance=r,e.addEventListener("click",()=>{r.hide()}),e.addEventListener("blur",()=>{r.hide()})}}}),this.addTooltipHideListeners(),this):this}destroyTooltips(){return this.removeTooltipHideListeners(),this.element.querySelectorAll('.sidebar-nav .nav-link[data-bs-toggle="tooltip"]').forEach(e=>{const t=e._tooltipInstance||window.bootstrap?.Tooltip?.getInstance(e);t&&(t.hide(),t.dispose()),delete e._tooltipInstance,e.removeAttribute("data-bs-toggle"),e.removeAttribute("data-bs-placement"),e.removeAttribute("data-bs-title"),e.removeAttribute("data-bs-container")}),this}getSidebarState(){const e=document.querySelector(".portal-container");return e?e.classList.contains("hide-sidebar")?"hidden":e.classList.contains("collapse-sidebar")?"collapsed":"normal":"normal"}isCollapsedState(){return"collapsed"===this.getSidebarState()}setToggleEnabled(e){return this.showToggle=e,this.render(),this}initializeMenus(e){if(e.menus)for(const t of e.menus)this.addMenu(t.name,t);else e.menu&&(e.menu.name=e.menu.name||"default",this.addMenu(e.menu.name,e.menu))}addTooltipHideListeners(){this._tooltipScrollHandler=()=>this.hideAllTooltips(),this.element.addEventListener("scroll",this._tooltipScrollHandler,{passive:!0}),this._tooltipRouteHandler=()=>this.hideAllTooltips(),this.getApp(),this._tooltipBlurHandler=()=>this.hideAllTooltips(),window.addEventListener("blur",this._tooltipBlurHandler),this._tooltipEscapeHandler=e=>{"Escape"===e.key&&this.hideAllTooltips()},document.addEventListener("keydown",this._tooltipEscapeHandler)}removeTooltipHideListeners(){this._tooltipScrollHandler&&(this.element.removeEventListener("scroll",this._tooltipScrollHandler),delete this._tooltipScrollHandler),this._tooltipBlurHandler&&(window.removeEventListener("blur",this._tooltipBlurHandler),delete this._tooltipBlurHandler),this._tooltipEscapeHandler&&(document.removeEventListener("keydown",this._tooltipEscapeHandler),delete this._tooltipEscapeHandler)}hideAllTooltips(){this.element.querySelectorAll('.sidebar-nav .nav-link[data-bs-toggle="tooltip"]').forEach(e=>{const t=e._tooltipInstance||window.bootstrap?.Tooltip?.getInstance(e);t&&t.hide()}),document.querySelectorAll(".tooltip.show").forEach(e=>{e.remove()})}async onBeforeDestroy(){this.destroyTooltips(),await super.onBeforeDestroy()}setupResponsiveBehavior(){const e=()=>{const e=window.innerWidth<=768,t=document.querySelector(".portal-container");t&&(e?t.classList.add("sidebar-mobile"):t.classList.remove("sidebar-mobile","sidebar-open"))};e(),window.addEventListener("resize",e)}static createDefault(e={}){return new Sidebar({theme:"sidebar-clean",showToggle:!0,autoCollapseMobile:!0,...e})}static createMinimal(e={}){return new Sidebar({theme:"sidebar-clean",showToggle:!1,autoCollapseMobile:!1,...e})}setSidebarTheme(e){return this.removeClass("sidebar-light sidebar-dark sidebar-clean"),this.sidebarTheme=e,this.addClass(e),this}show(){return this.setSidebarState("normal")}hide(){return this.setSidebarState("hidden")}collapse(){return this.setSidebarState("collapsed")}expand(){return this.setSidebarState("normal")}pulseToggle(){const e=this.element.querySelector(".sidebar-toggle");if(e){e.classList.add("pulse");const t=()=>{e.classList.remove("pulse"),e.removeEventListener("click",t)};e.addEventListener("click",t,{once:!0}),setTimeout(t,3e3)}return this}addSimpleMenuItem(e,t,s,i="bi-circle"){const a=this.menus.get(e);return a&&(a.items=a.items||[],a.items.push({text:t,route:s,icon:i}),this.activeMenuName===e&&this.render()),this}setSimpleMenu(e,t,s){const i={name:e,header:t,items:s};return this.addMenu(e,i),this.setActiveMenu(e),this}}class PageHeader extends r{constructor(e={}){super({tagName:"div",className:"page-header",...e}),this.style=e.style||"default",this.size=e.size||"md",this.showIcon=!1!==e.showIcon,this.showDescription=!1!==e.showDescription,this.showBreadcrumbs=e.showBreadcrumbs||!1,this.currentPage=null}async getTemplate(){return"minimal"===this.style?this.getMinimalTemplate():"breadcrumb"===this.style?this.getBreadcrumbTemplate():this.getDefaultTemplate()}getDefaultTemplate(){return'\n {{#data.hasPage}}\n <div class="page-header-content page-header-{{data.size}}">\n <div class="page-header-main">\n <div class="page-header-info">\n {{#data.showIcon}}\n {{#data.pageIcon}}\n <div class="page-icon">\n <i class="{{data.pageIcon}}"></i>\n </div>\n {{/data.pageIcon}}\n {{/data.showIcon}}\n \n <div class="page-title-group">\n <h1 class="page-title">{{data.pageTitle}}</h1>\n {{#data.showDescription}}\n {{#data.pageDescription}}\n <p class="page-description text-muted">{{data.pageDescription}}</p>\n {{/data.pageDescription}}\n {{/data.showDescription}}\n </div>\n </div>\n\n {{#data.hasActions}}\n <div class="page-actions">\n {{#data.actions}}\n <button class="btn {{buttonClass}}" \n data-action="{{action}}"\n type="button">\n {{#icon}}<i class="{{icon}} me-1"></i>{{/icon}}\n {{label}}\n </button>\n {{/data.actions}}\n </div>\n {{/data.hasActions}}\n </div>\n </div>\n {{/data.hasPage}}\n '}getMinimalTemplate(){return'\n {{#data.hasPage}}\n <div class="page-header-content page-header-minimal">\n <h1 class="page-title">\n {{#data.showIcon}}\n {{#data.pageIcon}}<i class="{{data.pageIcon}} me-2"></i>{{/data.pageIcon}}\n {{/data.showIcon}}\n {{data.pageTitle}}\n </h1>\n </div>\n {{/data.hasPage}}\n '}getBreadcrumbTemplate(){return'\n {{#data.hasPage}}\n <div class="page-header-content page-header-breadcrumb">\n {{#data.showBreadcrumbs}}\n <nav aria-label="breadcrumb">\n <ol class="breadcrumb mb-2">\n {{#data.breadcrumbs}}\n <li class="breadcrumb-item {{#active}}active{{/active}}">\n {{#href}}<a href="{{href}}">{{label}}</a>{{/href}}\n {{^href}}{{label}}{{/href}}\n </li>\n {{/data.breadcrumbs}}\n </ol>\n </nav>\n {{/data.showBreadcrumbs}}\n \n <div class="d-flex justify-content-between align-items-start">\n <h1 class="page-title">\n {{#data.showIcon}}\n {{#data.pageIcon}}<i class="{{data.pageIcon}} me-2"></i>{{/data.pageIcon}}\n {{/data.showIcon}}\n {{data.pageTitle}}\n </h1>\n \n {{#data.hasActions}}\n <div class="page-actions">\n {{#data.actions}}\n <button class="btn {{buttonClass}}" \n data-action="{{action}}"\n type="button">\n {{#icon}}<i class="{{icon}} me-1"></i>{{/icon}}\n {{label}}\n </button>\n {{/data.actions}}\n </div>\n {{/data.hasActions}}\n </div>\n \n {{#data.showDescription}}\n {{#data.pageDescription}}\n <p class="page-description text-muted mt-2">{{data.pageDescription}}</p>\n {{/data.pageDescription}}\n {{/data.showDescription}}\n </div>\n {{/data.hasPage}}\n '}async onBeforeRender(){await super.onBeforeRender();const e=this.currentPage,t=!!e;e&&(e.title,e.displayName,e.name,e.pageName,e.icon,e.pageIcon,e.pageDescription,e.description);const s=e?.options?.headerActions||e?.headerActions||e?.constructor?.prototype?.headerActions||[];this.data={hasPage:t,pageTitle:e?.title||e?.displayName||e?.name||e?.pageName||"",pageIcon:e?.icon||e?.pageIcon||"",pageDescription:e?.pageDescription||e?.description||"",showIcon:this.showIcon,showDescription:this.showDescription,showBreadcrumbs:this.showBreadcrumbs,breadcrumbs:e?.options?.breadcrumbs||e?.breadcrumbs||[],actions:s,hasActions:s.length>0,size:this.size},this.data}async setPage(e){this.currentPage=e,e&&await this.render()}getPage(){return this.currentPage}async onActionDefault(e,t,s){return this.currentPage&&"function"==typeof this.currentPage.onHeaderAction?(await this.currentPage.onHeaderAction(e,t,s),!0):(this.emit("action",{action:e,event:t,element:s,page:this.currentPage}),!1)}}class DeniedPage extends m{constructor(e={}){super({pageName:"Access Denied",route:"/denied",title:"Access Denied",pageIcon:"bi bi-shield-x",template:'\n <div class="container mt-5">\n <div class="row justify-content-center">\n <div class="col-md-8 col-lg-6">\n <div class="text-center mb-4">\n <i class="bi bi-shield-x text-muted" style="font-size: 3rem;"></i>\n <h2 class="mt-3 mb-2">Access Denied</h2>\n <p class="text-muted">You don\'t have permission to access this page.</p>\n </div>\n\n {{#deniedPage}}\n <div class="card border-0 shadow-sm mb-4">\n <div class="card-body">\n <h6 class="card-subtitle mb-2 text-muted">Requested Page</h6>\n <h5 class="card-title">\n <i class="{{pageIcon}} me-2"></i>\n {{displayName}}\n </h5>\n {{#route}}\n <p class="card-text text-muted small">{{route}}</p>\n {{/route}}\n {{#description}}\n <p class="card-text">{{description}}</p>\n {{/description}}\n\n {{#requiredPermissions}}\n <div class="mt-3">\n <h6 class="mb-2">Required Permissions:</h6>\n {{#permissions}}\n <span class="badge bg-light text-dark me-1 mb-1">{{.}}</span>\n {{/permissions}}\n {{^permissions}}\n <span class="text-muted small">Authentication required</span>\n {{/permissions}}\n </div>\n {{/requiredPermissions}}\n </div>\n </div>\n {{/deniedPage}}\n\n <div class="d-grid gap-2 d-md-flex justify-content-md-center">\n <button type="button" class="btn btn-primary" data-action="go-back">\n <i class="bi bi-arrow-left me-1"></i>\n Go Back\n </button>\n <button type="button" class="btn btn-outline-secondary" data-action="go-home">\n <i class="bi bi-house me-1"></i>\n Home\n </button>\n {{#showLogin}}\n <button type="button" class="btn btn-outline-primary" data-action="login">\n <i class="bi bi-box-arrow-in-right me-1"></i>\n Login\n </button>\n {{/showLogin}}\n </div>\n\n {{#currentUser}}\n <div class="text-center mt-4">\n <small class="text-muted">\n Logged in as <strong>{{username}}</strong>\n </small>\n </div>\n {{/currentUser}}\n </div>\n </div>\n </div>\n ',...e}),this.deniedPage=null,this.deniedPageOptions=null}async onParams(e={},t={}){await super.onParams(e,t),e.page?(this.deniedPage=e.page,this.deniedPageOptions=e.page.options||e.page.pageOptions||{}):t.page&&(this.deniedPageName=t.page)}setDeniedPage(e){return this.deniedPage=e,this.deniedPageOptions=e?.options||e?.pageOptions||{},this}async getViewData(){const e=this.getApp(),t=e?.activeUser||e?.getCurrentUser?.()||null;let s=null;if(this.deniedPage){const e=this.deniedPageOptions?.permissions||this.deniedPage.options?.permissions||this.deniedPage.pageOptions?.permissions;s={displayName:this.deniedPage.displayName||this.deniedPage.pageName||this.deniedPage.title||"Unknown Page",pageName:this.deniedPage.pageName,route:this.deniedPage.route,description:this.deniedPage.pageDescription||this.deniedPage.description,pageIcon:this.deniedPage.pageIcon||"bi bi-file-text",requiredPermissions:e?{permissions:Array.isArray(e)?e:[e]}:null}}else this.deniedPageName&&(s={displayName:this.deniedPageName,pageName:this.deniedPageName,pageIcon:"bi bi-file-text"});return{deniedPage:s,currentUser:t?{username:t.username||t.name||t.email||"Unknown User",name:t.name,email:t.email}:null,showLogin:!t}}async handleActionGoBack(e,t){e.preventDefault(),window.history.length>1?window.history.back():await this.handleActionGoHome(e,t)}async handleActionGoHome(e,t){e.preventDefault();const s=this.getApp();s?await s.navigateToDefault():window.location.href="/"}async handleActionLogin(e,t){e.preventDefault();const s=this.getApp();if(s)try{await s.showPage("login")}catch(i){try{await s.navigate("/login")}catch(a){this.emit("login-required",{returnUrl:this.deniedPage?.route||window.location.pathname}),setTimeout(()=>{s?.showInfo?.("Please contact your administrator for access.")},100)}}}async onEnter(){await super.onEnter();const e=this.deniedPage?.pageName||this.deniedPageName;e&&this.setMeta({title:`Access Denied - ${e}`}),console.warn("Access denied to page:",{page:this.deniedPage?.pageName||this.deniedPageName,route:this.deniedPage?.route,permissions:this.deniedPageOptions?.permissions,timestamp:/* @__PURE__ */(new Date).toISOString()})}static showForPage(e,t){const s=new DeniedPage;return s.setDeniedPage(t),e.showPage(s)}}class NotFoundPage extends m{constructor(e={}){super({pageName:"404",route:"/404",title:"404 - Page Not Found",pageIcon:"bi bi-search",template:'\n <div class="container mt-5">\n <div class="row justify-content-center">\n <div class="col-md-8 col-lg-6">\n <div class="text-center mb-4">\n <i class="bi bi-search text-muted" style="font-size: 3rem;"></i>\n <h2 class="mt-3 mb-2">Page Not Found</h2>\n <p class="text-muted">The page you\'re looking for doesn\'t exist.</p>\n </div>\n\n {{#path}}\n <div class="card border-0 shadow-sm mb-4">\n <div class="card-body text-center">\n <h6 class="card-subtitle mb-2 text-muted">Requested Path</h6>\n <code class="text-primary">{{path}}</code>\n </div>\n </div>\n {{/path}}\n\n <div class="d-grid gap-2 d-md-flex justify-content-md-center">\n <button type="button" class="btn btn-primary" data-action="go-back">\n <i class="bi bi-arrow-left me-1"></i>\n Go Back\n </button>\n <button type="button" class="btn btn-outline-secondary" data-action="go-home">\n <i class="bi bi-house me-1"></i>\n Home\n </button>\n </div>\n </div>\n </div>\n </div>\n ',...e}),this.path=null}async onParams(e={},t={}){await super.onParams(e,t),e.path&&(this.path=e.path),t.path&&(this.path=t.path)}setInfo(e){return this.path=e||null,this}async handleActionGoBack(e,t){e.preventDefault(),window.history.length>1?window.history.back():await this.handleActionGoHome(e,t)}async handleActionGoHome(e,t){e.preventDefault();const s=this.getApp();s?await s.navigateToDefault():window.location.href="/"}async onEnter(){await super.onEnter(),this.path&&this.setMeta({title:`404 - ${this.path} Not Found`}),console.warn("404 Not Found:",{path:this.path,timestamp:/* @__PURE__ */(new Date).toISOString()})}static showForPath(e,t){const s=new NotFoundPage;return s.setInfo(t),s.render()}}class PortalApp extends I{constructor(e={}){super(e),this.sidebarConfig=e.sidebar,this.topbarConfig=e.topbar||{},e.topnav&&!e.topbar&&(this.topbarConfig=e.topnav),this.showPageHeader=e.showPageHeader||!1,this.pageHeaderConfig=e.pageHeader||{},this.sidebar=null,this.topbar=null,this.topnav=null,this.pageHeader=null,this.tokenManager=new At,this.activeGroup=null,this.isMobile()?this.sidebarCollapsed=this.sidebarConfig.defaultCollapsed||!1:this.sidebarCollapsed=this.loadSidebarState()??(this.sidebarConfig.defaultCollapsed||!1),this.setupPageContainer(),this.toast=new v,this.Dialog=Ct,this.registerPage("denied",DeniedPage),this.registerPage("404",NotFoundPage)}async start(){await this.checkAuthStatus(),this.events.on("auth:unauthorized",()=>{this.tokenManager.clearTokens(),this.rest.clearAuth(),this.setActiveUser(null)}),this.events.on("auth:logout",()=>{this.tokenManager.clearTokens(),this.rest.clearAuth(),this.setActiveUser(null)}),this.events.on("browser:focus",()=>{this.activeUser&&this.tokenManager.checkAndRefreshTokens(this)}),this.events.on("portal:action",this.onPortalAction.bind(this)),this.activeUser&&await this.checkActiveGroup(),await this.setupRouter(),this.isStarted=!0,this.events.emit("app:ready",{app:this})}async checkAuthStatus(){const e=this.tokenManager.checkTokenStatus();if("logout"===e.action)return this.events.emit("auth:unauthorized",{app:this}),!1;if("refresh"===e.action&&!(await this.tokenManager.checkAndRefreshTokens(this)))return!1;const t=this.tokenManager.getTokenInstance();if(this.activeUser)return this.tokenManager.startAutoRefresh(this),!0;this.rest.setAuthToken(t.token);const s=new b({id:t.getUserId()}),i=await s.fetch();return i.success?(this.setActiveUser(s),this.tokenManager.startAutoRefresh(this),!0):(this.tokenManager.clearTokens(),this.events.emit("auth:unauthorized",{app:this,error:i.error}),!1)}async checkActiveGroup(){const e=new URLSearchParams(window.location.search).get("group"),t=e||this.loadActiveGroupId();if(t)try{const s=new g({id:t}),i=await s.fetch();if(!i.success||!i.data.status)return this.clearActiveGroup(),void console.warn("Failed to load active group:",i.statusText);this.activeGroup=s,e&&this.saveActiveGroupId(t),this.activeUser&&(this.activeUser.member=new E,await this.activeUser.member.fetchForGroup(s.id)),this.events.emit("group:loaded",{group:this.activeGroup})}catch(s){if(console.warn("Failed to load active group:",s),e&&!this.loadActiveGroupId())this.clearActiveGroupId();else if(e){const t=this.loadActiveGroupId();if(t&&t!==e)try{const e=new g({id:t});await e.fetch(),this.activeGroup=e,this.events.emit("group:loaded",{group:this.activeGroup})}catch(i){console.warn("Fallback to stored group also failed:",i),this.clearActiveGroupId()}}}}async setActiveGroup(e){const t=this.activeGroup;this.activeGroup=e,e&&e.get("id")?this.saveActiveGroupId(e.get("id")):this.clearActiveGroupId(),this.activeUser&&(this.activeUser.member=new E,await this.activeUser.member.fetchForGroup(e.id)),this.events.emit("group:changed",{group:e,previousGroup:t,app:this});const s=this.getCurrentPage();return s&&s.onGroupChange&&s.onGroupChange(e),this.router.updateUrl({group:e.id},{replace:!0}),this}getActiveGroup(){return this.activeGroup}async clearActiveGroup(){const e=this.activeGroup;return this.activeGroup=null,this.clearActiveGroupId(),this.events.emit("group:cleared",{previousGroup:e,app:this}),this}saveActiveGroupId(e){try{const t=this.getActiveGroupStorageKey();localStorage.setItem(t,e.toString())}catch(t){console.warn("Failed to save active group ID:",t)}}loadActiveGroupId(){try{const e=this.getActiveGroupStorageKey();return localStorage.getItem(e)}catch(e){return console.warn("Failed to load active group ID:",e),null}}clearActiveGroupId(){try{const e=this.getActiveGroupStorageKey();localStorage.removeItem(e)}catch(e){console.warn("Failed to clear active group ID:",e)}}getActiveGroupStorageKey(){return"active_group_id"}setPortalProfile(e){try{localStorage.setItem("portal_profile",e)}catch(t){console.warn("Failed to save portal profile:",t)}}needsGroupSelection(){return!this.activeGroup}setupPageContainer(){const e="string"==typeof this.container?document.querySelector(this.container):this.container;if(!e)throw new Error(`Portal container not found: ${this.container}`);const t=this.sidebarConfig&&Object.keys(this.sidebarConfig).length>0,s=this.topbarConfig&&Object.keys(this.topbarConfig).length>0,i=this.showPageHeader?'\n <div class="portal-content" id="portal-content">\n <div id="page-header"></div>\n <div id="page-container">\n \x3c!-- Pages render here --\x3e\n </div>\n </div>\n ':'\n <div class="portal-content" id="page-container">\n \x3c!-- Pages render here --\x3e\n </div>\n ';e.innerHTML=`\n <div class="portal-layout hide-sidebar">\n ${t?'<div id="portal-sidebar"></div>':""}\n <div class="portal-body">\n ${s?'<div id="portal-topnav"></div>':""}\n ${i}\n </div>\n </div>\n `,this.pageContainer="#page-container",e.classList.add("portal-container"),this.setupPortalComponents(),this.applySidebarState(e)}async setupPortalComponents(){await this.setupSidebar(),await this.setupTopbar(),await this.setupPageHeader(),this.setupPortalEvents()}async setupSidebar(){this.sidebarConfig&&0!==Object.keys(this.sidebarConfig).length&&(this.sidebar=new Sidebar({containerId:"portal-sidebar",...this.sidebarConfig}),await this.sidebar.render())}async setupTopbar(){this.topbarConfig&&0!==Object.keys(this.topbarConfig).length&&(this.topbar=new Pt({containerId:"portal-topnav",brandText:this.topbarConfig.brand||this.brand||this.title,brandRoute:this.topbarConfig.brandRoute||"/",brandIcon:this.topbarConfig.brandIcon||this.brandIcon,navItems:this.topbarConfig.leftItems||[],rightItems:this.topbarConfig.rightItems||[],displayMode:this.topbarConfig.displayMode||"both",showSidebarToggle:this.topbarConfig.showSidebarToggle||!1,...this.topbarConfig}),await this.topbar.render(),this.topnav=this.topbar)}async setupPageHeader(){if(!this.showPageHeader)return;this.pageHeader=new PageHeader({containerId:"page-header",style:this.pageHeaderConfig.style||"default",showIcon:!1!==this.pageHeaderConfig.showIcon,showDescription:!1!==this.pageHeaderConfig.showDescription,showBreadcrumbs:this.pageHeaderConfig.showBreadcrumbs||!1,...this.pageHeaderConfig});const e=document.getElementById("page-header");e&&await this.pageHeader.render(!0,e)}setupPortalEvents(){if(document.addEventListener("click",e=>{e.target.closest('[data-action="toggle-sidebar"]')&&(e.preventDefault(),this.toggleSidebar())}),window.ResizeObserver){const e=new ResizeObserver(()=>{this.handleResponsive()});e.observe(document.body),this._resizeObserver=e}else this._resizeHandler=()=>this.handleResponsive(),window.addEventListener("resize",this._resizeHandler);this.handleResponsive()}toggleSidebar(){if(!this.sidebar)return;const e=document.querySelector(".portal-container"),t=this.isMobile();t?e.classList.toggle("hide-sidebar"):(e.classList.toggle("collapse-sidebar"),this.sidebarCollapsed=!this.sidebarCollapsed,this.saveSidebarState(this.sidebarCollapsed)),this.events.emit("sidebar:toggled",{collapsed:this.sidebarCollapsed,mobile:t})}handleResponsive(){const e=document.querySelector(".portal-container");if(!e)return;const t=this.isMobile();t?(e.classList.add("mobile-layout"),e.classList.contains("hide-sidebar")||e.classList.add("hide-sidebar")):e.classList.remove("mobile-layout","hide-sidebar"),this.events.emit("responsive:changed",{mobile:t})}getPortalContainer(){return document.querySelector(".portal-container")}isMobile(){return window.innerWidth<768}hasMobileLayout(){return this.getPortalContainer().classList.contains("mobile-layout")}async showPage(e,t={},s={},i={}){const a=await super.showPage(e,t,s,i);return this.hasMobileLayout()&&this.getPortalContainer().classList.add("hide-sidebar"),this.currentPage&&this.updateNavigation(this.currentPage),a}updateNavigation(e){this.sidebar&&this.sidebar.setActivePage&&this.sidebar.setActivePage(e.route),this.topbar&&this.topbar.setActivePage&&this.topbar.setActivePage(e.route),this.pageHeader&&this.pageHeader.setPage(e),this.events.emit("portal:page-changed",{page:e})}setActiveUser(e){this.activeUser=e,this.topbar&&this.topbar.setUser(e),this.events.emit("portal:user-changed",{user:e})}getActiveUser(){return this.activeUser}saveSidebarState(e){try{const t=this.getSidebarStorageKey();localStorage.setItem(t,JSON.stringify(e))}catch(t){console.warn("Failed to save sidebar state:",t)}}loadSidebarState(){try{const e=this.getSidebarStorageKey(),t=localStorage.getItem(e);return null!==t?JSON.parse(t):null}catch(e){return console.warn("Failed to load sidebar state:",e),null}}getSidebarStorageKey(){return`${this.title?this.title.replace(/\s+/g,"_").toLowerCase():"portal_app"}_sidebar_collapsed`}applySidebarState(e=null){e||(e=document.querySelector(".portal-container")),e&&(this.sidebarCollapsed?e.classList.add("collapse-sidebar"):e.classList.remove("collapse-sidebar"))}clearSidebarState(){try{const e=this.getSidebarStorageKey();localStorage.removeItem(e)}catch(e){console.warn("Failed to clear sidebar state:",e)}}async changePassword(){const e=await this.showForm({title:"Change Password",fields:[{name:"current_password",type:"password",label:"Current Password",required:!0,showToggle:!0,strengthMeter:!0,capsLockWarning:!0},{name:"new_password",type:"password",label:"New Password",required:!0,showToggle:!0,passwordUsage:"new",strengthMeter:!0,capsLockWarning:!0,attributes:{autocomplete:"new-password"}},{name:"confirm_password",type:"password",label:"Confirm Password",required:!0,showToggle:!0,passwordUsage:"new",strengthMeter:!0,capsLockWarning:!0,attributes:{}}],submitLabel:"Change Password"});e&&(e.new_password===e.confirm_password?200===(await this.activeUser.save(e)).status?this.toast.success("Password changed successfully"):this.toast.error("Failed to change password"):this.toast.error("Passwords do not match"))}onPortalAction(e){switch(e.action){case"logout":this.tokenManager.clearTokens(),this.rest.clearAuth(),this.setActiveUser(null);break;case"profile":this.showProfile();break;case"change-password":this.changePassword();break;default:console.warn(`Unknown portal action: ${e}`)}}async showProfile(){if(this.activeUser)try{const e=await Ct.showModelForm({title:"Edit Profile",size:"lg",fileHandling:"base64",model:this.activeUser,fields:[{type:"header",text:"Profile Information",level:4,class:"text-primary mb-3"},{type:"group",columns:{xs:12,md:4},title:"Avatar",fields:[{type:"image",name:"avatar",size:"lg",imageSize:{width:200,height:200},placeholder:"Upload your avatar",help:"Square images work best"}]},{type:"group",columns:{xs:12,md:8},title:"Details",fields:[{type:"text",name:"display_name",label:"Display Name",required:!0,columns:12,placeholder:"Enter first name"},{type:"email",name:"email",label:"Email Address",required:!0,columns:8,placeholder:"your.email@example.com"},{type:"tel",name:"phone_number",label:"Phone Number",columns:4,placeholder:"(555) 123-4567"}]},{type:"group",columns:12,title:"Account Settings",class:"pt-3",fields:[{type:"select",name:"timezone",label:"Timezone",columns:6,options:[{value:"America/New_York",text:"Eastern Time"},{value:"America/Chicago",text:"Central Time"},{value:"America/Denver",text:"Mountain Time"},{value:"America/Los_Angeles",text:"Pacific Time"},{value:"UTC",text:"UTC"}]},{type:"select",name:"language",label:"Language",columns:6,options:[{value:"en",text:"English"},{value:"es",text:"Spanish"},{value:"fr",text:"French"},{value:"de",text:"German"}]},{type:"switch",name:"email_notifications",label:"Email Notifications",columns:4},{type:"switch",name:"two_factor_enabled",label:"Two-Factor Authentication",columns:4},{type:"switch",name:"profile_public",label:"Public Profile",columns:4}]}],submitText:"Save Profile",cancelText:"Cancel"});e&&e.success?this.showSuccess("Profile updated successfully!"):e&&e.success}catch(e){console.error("Error showing profile form:",e),this.showError("Failed to load profile form")}else this.showError("No user is currently logged in")}async destroy(){this.activeGroup=null,this._resizeObserver&&this._resizeObserver.disconnect(),this._resizeHandler&&window.removeEventListener("resize",this._resizeHandler),this.topbar&&(await this.topbar.destroy(),this.topbar=null,this.topnav=null),this.sidebar&&(await this.sidebar.destroy(),this.sidebar=null),await super.destroy()}static create(e={}){return new PortalApp(e)}}class FormPage extends m{constructor(e={}){super({title:"Form Page",description:"A page for submitting forms",icon:"form",fields:[],template:'<div data-container="form-view-container"></div>',className:"form-page container-sm",...e})}async onInit(){await super.onInit(),this.formView=new Tt({containerId:"form-view-container",fields:this.options.fields,autosaveModelField:!0}),this.addChild(this.formView),this.getApp().activeGroup&&this.formView.setModel(this.getApp().activeGroup)}async onEnter(){await super.onEnter(),this.formView&&await this.recreateFormView()}async onGroupChange(e){this.formView&&await this.recreateFormView()}async recreateFormView(){this.formView&&(await this.formView.destroy(),this.removeChild(this.formView)),this.formView=new Tt({containerId:"form-view-container",fields:this.options.fields,autosaveModelField:!0}),this.addChild(this.formView),this.getApp().activeGroup&&this.formView.setModel(this.getApp().activeGroup)}}const jt=new class{constructor(){this.formatter=o,this.compiledTemplates=/* @__PURE__ */new Map}render(e,t,s={}){return l.render(e,t,s)}compile(e){const t=l.parse(e);return this.compiledTemplates.set(e,t),t}renderCompiled(e,t,s={}){return l.render(e,t,s)}clearCache(){this.compiledTemplates.clear(),l.clearCache()}cache(e,t){return{key:e,template:t,compiled:this.compile(t)}}getCached(e){for(const[t,s]of this.compiledTemplates)if(t===e||s===e)return{key:e,template:t,compiled:s};return null}registerFormatter(e,t){return this.formatter.register(e,t),this}hasPipes(e){return/\{\{[{]?[^}|]+\|[^}]+\}[}]?\}/.test(e)}processData(e,t){const s={...e};for(const[i,a]of Object.entries(t))if(e&&"function"==typeof e.get)s[i]=e.get(`${i}|${a}`);else{const t=this.getValueFromPath(e,i);s[i]=this.formatter.pipe(t,a)}return s}getValueFromPath(e,t){if(!e||!t)return;if(e&&"function"==typeof e.get)return e.get(t);const s=t.split(".");let i=e;for(const a of s){if(null==i)return;i=!isNaN(a)&&Array.isArray(i)?i[parseInt(a)]:i[a]}return i}processTemplate(e,t){return{template:e,data:t}}};Bt.install({level:"warn"});const zt="MOJO",$t="web-mojo",qt={FRAMEWORK_NAME:zt,PACKAGE_NAME:$t};export{e as BUILD_TIME,G as BundleByOptions,_ as ChatInputView,N as ChatMessageView,F as ChatView,D as Collection,R as CommonEventFields,V as CommonScopeOptions,H as ComparatorOptions,Bt as ConsoleSilencer,f as ContextMenu,It as DataView,c as DataWrapper,Ct as Dialog,O as DjangoLookups,U as EmailDomain,B as EmailDomainForms,K as EmailDomainList,j as EmailTemplate,z as EmailTemplateForms,$ as EmailTemplateList,T as EventBus,d as EventDelegate,zt as FRAMEWORK_NAME,q as File,J as FileForms,W as FileList,Y as FileManager,Q as FileManagerForms,X as FileManagerList,Z as FilePreviewView,ee as FileUpload,FormPage,Tt as FormView,te as GeoLocatedIP,se as GeoLocatedIPList,g as Group,w as GroupForms,p as GroupList,ie as Incident,ae as IncidentEvent,ne as IncidentEventForms,re as IncidentEventList,oe as IncidentForms,le as IncidentHistory,ce as IncidentHistoryList,de as IncidentList,he as IncidentRule,ue as IncidentRuleList,pe as IncidentRuleSet,ge as IncidentRuleSetList,me as IncidentStats,ve as Job,be as JobEvent,fe as JobEventList,we as JobForms,ye as JobList,Se as JobLog,Ae as JobLogList,Pe as JobRunner,Ce as JobRunnerForms,Me as JobRunnerList,Le as JobsEngineStats,Ie as LOOKUPS,Mt as ListView,Lt as ListViewItem,Te as Log,ke as LogList,h as MOJOUtils,De as Mailbox,xe as MailboxForms,Ee as MailboxList,Ge as MatchByOptions,E as Member,_e as MemberForms,Ne as MemberList,Fe as MetricsForms,Re as MetricsPermission,Ve as MetricsPermissionList,x as Model,jt as MustacheFormatter,$t as PACKAGE_NAME,m as Page,PortalApp,He as ProgressView,Oe as PushConfig,Ue as PushConfigForms,Be as PushConfigList,Ke as PushDelivery,je as PushDeliveryList,ze as PushDevice,$e as PushDeviceList,qe as PushTemplate,Je as PushTemplateForms,We as PushTemplateList,u as Rest,k as Router,Ye as Rule,Qe as RuleForms,Xe as RuleList,Ze as RuleSet,et as RuleSetForms,tt as RuleSetList,st as S3Bucket,it as S3BucketForms,at as S3BucketList,nt as SentMessage,rt as SentMessageForms,ot as SentMessageList,Sidebar,St as SimpleSearchView,lt as TabView,ct as TablePage,dt as TableRow,ht as TableView,ut as Ticket,pt as TicketCategories,gt as TicketForms,mt as TicketList,vt as TicketNote,bt as TicketNoteList,v as ToastService,At as TokenManager,Pt as TopNav,b as User,y as UserDataView,S as UserDevice,A as UserDeviceList,P as UserDeviceLocation,C as UserDeviceLocationList,M as UserForms,L as UserList,t as VERSION,s as VERSION_INFO,i as VERSION_MAJOR,a as VERSION_MINOR,n as VERSION_REVISION,ft as ValueTypeOptions,r as View,I as WebApp,Dt as WebSocketClient,kt as applyFileDropMixin,o as dataFormatter,qt as default,wt as formatFilterDisplay,Kt as installConsoleSilencer,yt as parseFilterKey};
3253
2
  //# sourceMappingURL=index.es.js.map