web-mojo 2.2.57 → 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.
- package/dist/admin.cjs.js +1 -1
- package/dist/admin.cjs.js.map +1 -1
- package/dist/admin.es.js +1 -10105
- package/dist/admin.es.js.map +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.es.js +1 -588
- package/dist/auth.es.js.map +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -571
- package/dist/charts.es.js.map +1 -1
- package/dist/chunks/ChatView-CqkYoMmr.js +2 -0
- package/dist/chunks/{ChatView-9k6xBWXk.js.map → ChatView-CqkYoMmr.js.map} +1 -1
- package/dist/chunks/{ChatView-CdtuCDYm.js → ChatView-DFN9xt0c.js} +2 -2
- package/dist/chunks/{ChatView-CdtuCDYm.js.map → ChatView-DFN9xt0c.js.map} +1 -1
- package/dist/chunks/Collection-1sPoIFvQ.js +2 -0
- package/dist/chunks/{Collection-DaiL0uGl.js.map → Collection-1sPoIFvQ.js.map} +1 -1
- package/dist/chunks/{Collection-CxbNKOas.js → Collection-DSBRXpwK.js} +2 -2
- package/dist/chunks/{Collection-CxbNKOas.js.map → Collection-DSBRXpwK.js.map} +1 -1
- package/dist/chunks/{ContextMenu-ClwHEbbD.js → ContextMenu-BWy7WqF4.js} +2 -2
- package/dist/chunks/{ContextMenu-ClwHEbbD.js.map → ContextMenu-BWy7WqF4.js.map} +1 -1
- package/dist/chunks/ContextMenu-BvniQz-N.js +3 -0
- package/dist/chunks/{ContextMenu-sgvgSACY.js.map → ContextMenu-BvniQz-N.js.map} +1 -1
- package/dist/chunks/DataView--nUWtq6r.js +2 -0
- package/dist/chunks/{DataView-Dzo0jbs2.js.map → DataView--nUWtq6r.js.map} +1 -1
- package/dist/chunks/{DataView-1xh3GFeC.js → DataView-CK3Z0TJH.js} +2 -2
- package/dist/chunks/{DataView-1xh3GFeC.js.map → DataView-CK3Z0TJH.js.map} +1 -1
- package/dist/chunks/Dialog-BcgSR01Z.js +2 -0
- package/dist/chunks/{Dialog-DOGDalUq.js.map → Dialog-BcgSR01Z.js.map} +1 -1
- package/dist/chunks/{Dialog-CQlTDhZS.js → Dialog-DwCTFV6O.js} +2 -2
- package/dist/chunks/{Dialog-CQlTDhZS.js.map → Dialog-DwCTFV6O.js.map} +1 -1
- package/dist/chunks/FormPlugins-DvQ-G5J5.js +2 -0
- package/dist/chunks/{FormPlugins-DY6e88YT.js.map → FormPlugins-DvQ-G5J5.js.map} +1 -1
- package/dist/chunks/{FormView-DaKA4Sys.js → FormView-CRmEReTC.js} +3 -3
- package/dist/chunks/{FormView-DaKA4Sys.js.map → FormView-CRmEReTC.js.map} +1 -1
- package/dist/chunks/FormView-OLA7t-yv.js +3 -0
- package/dist/chunks/{FormView-Dz3mYasQ.js.map → FormView-OLA7t-yv.js.map} +1 -1
- package/dist/chunks/ListView-6JQ6tRXs.js +2 -0
- package/dist/chunks/{ListView-X5w5jf51.js.map → ListView-6JQ6tRXs.js.map} +1 -1
- package/dist/chunks/{ListView-CDzKIpd8.js → ListView-DVStKiMi.js} +2 -2
- package/dist/chunks/{ListView-CDzKIpd8.js.map → ListView-DVStKiMi.js.map} +1 -1
- package/dist/chunks/{MetricsCountryMapView-Dx2cw7ya.js → MetricsCountryMapView-CnAEbUw_.js} +2 -2
- package/dist/chunks/{MetricsCountryMapView-Dx2cw7ya.js.map → MetricsCountryMapView-CnAEbUw_.js.map} +1 -1
- package/dist/chunks/MetricsCountryMapView-J067qrrt.js +2 -0
- package/dist/chunks/{MetricsCountryMapView-B2xz6zUw.js.map → MetricsCountryMapView-J067qrrt.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-CBuso0OE.js → MetricsMiniChartWidget-BeD1slGs.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-CBuso0OE.js.map → MetricsMiniChartWidget-BeD1slGs.js.map} +1 -1
- package/dist/chunks/MetricsMiniChartWidget-x2gFjHOU.js +2 -0
- package/dist/chunks/{MetricsMiniChartWidget-DvKd7Qrk.js.map → MetricsMiniChartWidget-x2gFjHOU.js.map} +1 -1
- package/dist/chunks/PDFViewer-CsyKn-gh.js +2 -0
- package/dist/chunks/{PDFViewer-EJ9cOfPF.js.map → PDFViewer-CsyKn-gh.js.map} +1 -1
- package/dist/chunks/{PDFViewer-ofMGdSaj.js → PDFViewer-DSa4BZCm.js} +2 -2
- package/dist/chunks/{PDFViewer-ofMGdSaj.js.map → PDFViewer-DSa4BZCm.js.map} +1 -1
- package/dist/chunks/Rest-DHbszkuP.js +2 -0
- package/dist/chunks/Rest-DHbszkuP.js.map +1 -0
- package/dist/chunks/Rest-Ds9e8tN8.js +2 -0
- package/dist/chunks/Rest-Ds9e8tN8.js.map +1 -0
- package/dist/chunks/TokenManager-D6SjKgPZ.js +2 -0
- package/dist/chunks/{TokenManager-DoN9e6q6.js.map → TokenManager-D6SjKgPZ.js.map} +1 -1
- package/dist/chunks/{TokenManager-Gqvj7SDX.js → TokenManager-REbha1Le.js} +2 -2
- package/dist/chunks/{TokenManager-Gqvj7SDX.js.map → TokenManager-REbha1Le.js.map} +1 -1
- package/dist/chunks/WebApp-CULZpO_0.js +2 -0
- package/dist/chunks/{WebApp-6qvqmOts.js.map → WebApp-CULZpO_0.js.map} +1 -1
- package/dist/chunks/{WebApp-_dgpwtFw.js → WebApp-DovLtA60.js} +2 -2
- package/dist/chunks/{WebApp-_dgpwtFw.js.map → WebApp-DovLtA60.js.map} +1 -1
- package/dist/chunks/WebSocketClient-B-wc3mez.js +2 -0
- package/dist/chunks/{WebSocketClient-DG2olXpH.js.map → WebSocketClient-B-wc3mez.js.map} +1 -1
- package/dist/chunks/{WebSocketClient-MFkFlSue.js → WebSocketClient-BdZ9QYll.js} +2 -2
- package/dist/chunks/{WebSocketClient-MFkFlSue.js.map → WebSocketClient-BdZ9QYll.js.map} +1 -1
- package/dist/chunks/version-CU1HG1XH.js +2 -0
- package/dist/chunks/version-CU1HG1XH.js.map +1 -0
- package/dist/chunks/{version-BVADfTA5.js → version-DaB1uXvO.js} +2 -2
- package/dist/chunks/{version-BVADfTA5.js.map → version-DaB1uXvO.js.map} +1 -1
- package/dist/css/web-mojo.css +1 -1
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.es.js +1 -957
- package/dist/docit.es.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -3252
- package/dist/index.es.js.map +1 -1
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.es.js +1 -3737
- package/dist/lightbox.es.js.map +1 -1
- package/dist/loader.umd.js +2 -2
- package/dist/map.cjs.js +1 -1
- package/dist/map.es.js +1 -1032
- package/dist/map.es.js.map +1 -1
- package/dist/mojo-auth.es.js +338 -0
- package/dist/mojo-auth.umd.js +1 -0
- package/dist/timeline.cjs.js +1 -1
- package/dist/timeline.es.js +1 -224
- package/dist/timeline.es.js.map +1 -1
- package/dist/web-mojo.lite.iife.js +14 -3
- package/dist/web-mojo.lite.iife.js.map +1 -1
- package/dist/web-mojo.lite.iife.min.js +6 -6
- package/dist/web-mojo.lite.iife.min.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunks/ChatView-9k6xBWXk.js +0 -7632
- package/dist/chunks/Collection-DaiL0uGl.js +0 -1014
- package/dist/chunks/ContextMenu-sgvgSACY.js +0 -1535
- package/dist/chunks/DataView-Dzo0jbs2.js +0 -862
- package/dist/chunks/Dialog-DOGDalUq.js +0 -1579
- package/dist/chunks/FormPlugins-DY6e88YT.js +0 -124
- package/dist/chunks/FormView-Dz3mYasQ.js +0 -8636
- package/dist/chunks/ListView-X5w5jf51.js +0 -495
- package/dist/chunks/MetricsCountryMapView-B2xz6zUw.js +0 -1054
- package/dist/chunks/MetricsMiniChartWidget-DvKd7Qrk.js +0 -3283
- package/dist/chunks/PDFViewer-EJ9cOfPF.js +0 -946
- package/dist/chunks/Rest-CgSjfMaU.js +0 -2
- package/dist/chunks/Rest-CgSjfMaU.js.map +0 -1
- package/dist/chunks/Rest-W-sPfGh9.js +0 -4375
- package/dist/chunks/Rest-W-sPfGh9.js.map +0 -1
- package/dist/chunks/TokenManager-DoN9e6q6.js +0 -1423
- package/dist/chunks/WebApp-6qvqmOts.js +0 -1386
- package/dist/chunks/WebSocketClient-DG2olXpH.js +0 -209
- package/dist/chunks/version-OyPGnx30.js +0 -38
- package/dist/chunks/version-OyPGnx30.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -1,3253 +1,2 @@
|
|
|
1
|
-
import { B, a, V, b, c, d } from "./chunks/version-OyPGnx30.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
|