web-mojo 2.1.936 → 2.1.955
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 +122 -229
- package/dist/admin.es.js.map +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.cjs.js.map +1 -1
- package/dist/auth.es.js +7 -7
- package/dist/auth.es.js.map +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +7 -7
- package/dist/chunks/ChatView-CTtQHvRP.js +2 -0
- package/dist/chunks/ChatView-CTtQHvRP.js.map +1 -0
- package/dist/chunks/{ChatView-DlSxjxah.js → ChatView-DLEStri1.js} +89 -574
- package/dist/chunks/ChatView-DLEStri1.js.map +1 -0
- package/dist/chunks/Collection-DD1_31eh.js +2 -0
- package/dist/chunks/Collection-DD1_31eh.js.map +1 -0
- package/dist/chunks/Collection-YRfGoT73.js +990 -0
- package/dist/chunks/Collection-YRfGoT73.js.map +1 -0
- package/dist/chunks/ContextMenu-By2g3KYY.js +1171 -0
- package/dist/chunks/ContextMenu-By2g3KYY.js.map +1 -0
- package/dist/chunks/ContextMenu-Cl0TRsIa.js +3 -0
- package/dist/chunks/ContextMenu-Cl0TRsIa.js.map +1 -0
- package/dist/chunks/DataView-CdDY9ijM.js +2 -0
- package/dist/chunks/{DataView-XJbTQ5q0.js.map → DataView-CdDY9ijM.js.map} +1 -1
- package/dist/chunks/{DataView-Vmjx4eCr.js → DataView-OUqaLmGB.js} +2 -2
- package/dist/chunks/{DataView-Vmjx4eCr.js.map → DataView-OUqaLmGB.js.map} +1 -1
- package/dist/chunks/{Dialog-D_rAf4gQ.js → Dialog-C2mRUxga.js} +8 -6
- package/dist/chunks/{Dialog-D_rAf4gQ.js.map → Dialog-C2mRUxga.js.map} +1 -1
- package/dist/chunks/Dialog-Cl6MN8if.js +2 -0
- package/dist/chunks/{Dialog-BinTQTfO.js.map → Dialog-Cl6MN8if.js.map} +1 -1
- package/dist/chunks/FormView-BSWaXDav.js +3 -0
- package/dist/chunks/{FormView-TPFsq8ZX.js.map → FormView-BSWaXDav.js.map} +1 -1
- package/dist/chunks/{FormView-CIriLDZY.js → FormView-HWvIdFkB.js} +10 -6
- package/dist/chunks/FormView-HWvIdFkB.js.map +1 -0
- package/dist/chunks/ListView-BMNhd5-B.js +492 -0
- package/dist/chunks/ListView-BMNhd5-B.js.map +1 -0
- package/dist/chunks/ListView-BRGiITfD.js +2 -0
- package/dist/chunks/ListView-BRGiITfD.js.map +1 -0
- package/dist/chunks/MetricsMiniChartWidget-BkTEO87S.js +2 -0
- package/dist/chunks/{MetricsMiniChartWidget-sONcM0pG.js.map → MetricsMiniChartWidget-BkTEO87S.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-BolRZ-Ja.js → MetricsMiniChartWidget-Y70IHFIe.js} +3 -3
- package/dist/chunks/{MetricsMiniChartWidget-BolRZ-Ja.js.map → MetricsMiniChartWidget-Y70IHFIe.js.map} +1 -1
- package/dist/chunks/PDFViewer-C0aMqGJL.js +2 -0
- package/dist/chunks/{PDFViewer-UBhinN8A.js.map → PDFViewer-C0aMqGJL.js.map} +1 -1
- package/dist/chunks/{PDFViewer-D6SKOl85.js → PDFViewer-DkbYnnoV.js} +3 -3
- package/dist/chunks/{PDFViewer-D6SKOl85.js.map → PDFViewer-DkbYnnoV.js.map} +1 -1
- package/dist/chunks/Page-CvbwEoLv.js +2 -0
- package/dist/chunks/{Page-CnvHhwLZ.js.map → Page-CvbwEoLv.js.map} +1 -1
- package/dist/chunks/{Page-B7L25Omb.js → Page-Deq4y2Kq.js} +2 -2
- package/dist/chunks/{Page-B7L25Omb.js.map → Page-Deq4y2Kq.js.map} +1 -1
- package/dist/chunks/Rest-BNYqGlnP.js +2 -0
- package/dist/chunks/Rest-BNYqGlnP.js.map +1 -0
- package/dist/chunks/{WebApp-El07OMHH.js → Rest-CS4jRCAs.js} +5 -1389
- package/dist/chunks/Rest-CS4jRCAs.js.map +1 -0
- package/dist/chunks/TopNav-A7NQ4viq.js +2 -0
- package/dist/chunks/{TopNav-Dcmcic-i.js.map → TopNav-A7NQ4viq.js.map} +1 -1
- package/dist/chunks/{TopNav-CPA884W7.js → TopNav-Dch6cZFa.js} +5 -5
- package/dist/chunks/{TopNav-CPA884W7.js.map → TopNav-Dch6cZFa.js.map} +1 -1
- package/dist/chunks/WebApp-CaOPY_k7.js +2 -0
- package/dist/chunks/WebApp-CaOPY_k7.js.map +1 -0
- package/dist/chunks/WebApp-RHtJ4hFZ.js +1388 -0
- package/dist/chunks/WebApp-RHtJ4hFZ.js.map +1 -0
- package/dist/css/web-mojo.css +2 -2
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.cjs.js.map +1 -1
- package/dist/docit.es.js +12 -10
- package/dist/docit.es.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +120 -116
- package/dist/index.es.js.map +1 -1
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.cjs.js.map +1 -1
- package/dist/lightbox.es.js +121 -121
- package/dist/lightbox.es.js.map +1 -1
- package/dist/map.cjs.js +2 -0
- package/dist/map.cjs.js.map +1 -0
- package/dist/map.es.js +188 -0
- package/dist/map.es.js.map +1 -0
- package/dist/timeline.cjs.js +2 -0
- package/dist/timeline.cjs.js.map +1 -0
- package/dist/timeline.es.js +225 -0
- package/dist/timeline.es.js.map +1 -0
- package/package.json +9 -1
- package/dist/chunks/ChatView-DlSxjxah.js.map +0 -1
- package/dist/chunks/ChatView-DnqrGXMC.js +0 -2
- package/dist/chunks/ChatView-DnqrGXMC.js.map +0 -1
- package/dist/chunks/ContextMenu-CE77rUmn.js +0 -2155
- package/dist/chunks/ContextMenu-CE77rUmn.js.map +0 -1
- package/dist/chunks/ContextMenu-KVxd0Kgd.js +0 -3
- package/dist/chunks/ContextMenu-KVxd0Kgd.js.map +0 -1
- package/dist/chunks/DataView-XJbTQ5q0.js +0 -2
- package/dist/chunks/Dialog-BinTQTfO.js +0 -2
- package/dist/chunks/FormView-CIriLDZY.js.map +0 -1
- package/dist/chunks/FormView-TPFsq8ZX.js +0 -3
- package/dist/chunks/MetricsMiniChartWidget-sONcM0pG.js +0 -2
- package/dist/chunks/PDFViewer-UBhinN8A.js +0 -2
- package/dist/chunks/Page-CnvHhwLZ.js +0 -2
- package/dist/chunks/TopNav-Dcmcic-i.js +0 -2
- package/dist/chunks/WebApp-El07OMHH.js.map +0 -1
- package/dist/chunks/WebApp-b9DQWz1d.js +0 -2
- package/dist/chunks/WebApp-b9DQWz1d.js.map +0 -1
|
@@ -1,2155 +0,0 @@
|
|
|
1
|
-
import { i as EventEmitter, r as rest, h as MOJOUtils, V as View } from "./WebApp-El07OMHH.js";
|
|
2
|
-
class Model {
|
|
3
|
-
constructor(data = {}, options = {}) {
|
|
4
|
-
this.endpoint = options.endpoint || this.constructor.endpoint || "";
|
|
5
|
-
this.id = data.id || null;
|
|
6
|
-
this.attributes = { ...data };
|
|
7
|
-
this._ = this.attributes;
|
|
8
|
-
this.originalAttributes = { ...data };
|
|
9
|
-
this.errors = {};
|
|
10
|
-
this.loading = false;
|
|
11
|
-
this.rest = rest;
|
|
12
|
-
this.options = {
|
|
13
|
-
idAttribute: "id",
|
|
14
|
-
timestamps: true,
|
|
15
|
-
...options
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
getContextValue(key) {
|
|
19
|
-
return this.get(key);
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Get attribute value with support for dot notation and pipe formatting
|
|
23
|
-
* @param {string} key - Attribute key with optional pipes (e.g., "name|uppercase")
|
|
24
|
-
* @returns {*} Attribute value, possibly formatted
|
|
25
|
-
*/
|
|
26
|
-
get(key) {
|
|
27
|
-
if (!key.includes(".") && !key.includes("|") && this[key] !== void 0) {
|
|
28
|
-
if (typeof this[key] === "function") {
|
|
29
|
-
return this[key]();
|
|
30
|
-
}
|
|
31
|
-
return this[key];
|
|
32
|
-
}
|
|
33
|
-
return MOJOUtils.getContextData(this.attributes, key);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Set attribute value(s)
|
|
37
|
-
* @param {string|object} key - Attribute key or object of key-value pairs
|
|
38
|
-
* @param {*} value - Attribute value (if key is string)
|
|
39
|
-
* @param {object} options - Options (silent: true to not trigger change event)
|
|
40
|
-
*/
|
|
41
|
-
set(key, value, options = {}) {
|
|
42
|
-
const previousAttributes = JSON.parse(JSON.stringify(this.attributes));
|
|
43
|
-
let hasChanged = false;
|
|
44
|
-
if (typeof key === "object") {
|
|
45
|
-
for (const [attrKey, attrValue] of Object.entries(key)) {
|
|
46
|
-
hasChanged = this._setNestedAttribute(attrKey, attrValue) || hasChanged;
|
|
47
|
-
}
|
|
48
|
-
if (key.id !== void 0) {
|
|
49
|
-
this.id = key.id;
|
|
50
|
-
}
|
|
51
|
-
} else {
|
|
52
|
-
if (key === "id") {
|
|
53
|
-
this.id = value;
|
|
54
|
-
hasChanged = true;
|
|
55
|
-
} else {
|
|
56
|
-
hasChanged = this._setNestedAttribute(key, value);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (hasChanged && !options.silent) {
|
|
60
|
-
this.emit("change", this);
|
|
61
|
-
if (typeof key === "string") {
|
|
62
|
-
this.emit(`change:${key}`, value, this);
|
|
63
|
-
} else {
|
|
64
|
-
for (const [attr, val] of Object.entries(key)) {
|
|
65
|
-
const finalValue = this._getNestedValue(attr);
|
|
66
|
-
if (JSON.stringify(this._getNestedValue(attr, previousAttributes)) !== JSON.stringify(finalValue)) {
|
|
67
|
-
this.emit(`change:${attr}`, finalValue, this);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Set a nested attribute using dot notation
|
|
75
|
-
* @param {string} key - Attribute key (may contain dots)
|
|
76
|
-
* @param {*} value - Value to set
|
|
77
|
-
* @returns {boolean} - Whether the value changed
|
|
78
|
-
*/
|
|
79
|
-
_setNestedAttribute(key, value) {
|
|
80
|
-
if (!key.includes(".")) {
|
|
81
|
-
const oldValue2 = this.attributes[key];
|
|
82
|
-
this.attributes[key] = value;
|
|
83
|
-
this[key] = value;
|
|
84
|
-
return oldValue2 !== value;
|
|
85
|
-
}
|
|
86
|
-
const keys = key.split(".");
|
|
87
|
-
const topLevelKey = keys[0];
|
|
88
|
-
if (!this.attributes[topLevelKey] || typeof this.attributes[topLevelKey] !== "object") {
|
|
89
|
-
this.attributes[topLevelKey] = {};
|
|
90
|
-
}
|
|
91
|
-
if (!this[topLevelKey] || typeof this[topLevelKey] !== "object") {
|
|
92
|
-
this[topLevelKey] = {};
|
|
93
|
-
}
|
|
94
|
-
const oldValue = this._getNestedValue(key);
|
|
95
|
-
let attrTarget = this.attributes[topLevelKey];
|
|
96
|
-
let instanceTarget = this[topLevelKey];
|
|
97
|
-
for (let i = 1; i < keys.length - 1; i++) {
|
|
98
|
-
const currentKey = keys[i];
|
|
99
|
-
if (!attrTarget[currentKey] || typeof attrTarget[currentKey] !== "object") {
|
|
100
|
-
attrTarget[currentKey] = {};
|
|
101
|
-
}
|
|
102
|
-
if (!instanceTarget[currentKey] || typeof instanceTarget[currentKey] !== "object") {
|
|
103
|
-
instanceTarget[currentKey] = {};
|
|
104
|
-
}
|
|
105
|
-
attrTarget = attrTarget[currentKey];
|
|
106
|
-
instanceTarget = instanceTarget[currentKey];
|
|
107
|
-
}
|
|
108
|
-
const finalKey = keys[keys.length - 1];
|
|
109
|
-
attrTarget[finalKey] = value;
|
|
110
|
-
instanceTarget[finalKey] = value;
|
|
111
|
-
return JSON.stringify(oldValue) !== JSON.stringify(value);
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Get a nested value using dot notation
|
|
115
|
-
* @param {string} key - Attribute key (may contain dots)
|
|
116
|
-
* @param {object} source - Source object (defaults to this.attributes)
|
|
117
|
-
* @returns {*} - The nested value
|
|
118
|
-
*/
|
|
119
|
-
_getNestedValue(key, source = this.attributes) {
|
|
120
|
-
if (!key.includes(".")) {
|
|
121
|
-
return source[key];
|
|
122
|
-
}
|
|
123
|
-
const keys = key.split(".");
|
|
124
|
-
let current = source;
|
|
125
|
-
for (const k of keys) {
|
|
126
|
-
if (current == null || typeof current !== "object") {
|
|
127
|
-
return void 0;
|
|
128
|
-
}
|
|
129
|
-
current = current[k];
|
|
130
|
-
}
|
|
131
|
-
return current;
|
|
132
|
-
}
|
|
133
|
-
getData() {
|
|
134
|
-
return this.attributes;
|
|
135
|
-
}
|
|
136
|
-
getId() {
|
|
137
|
-
return this.id;
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Fetch model data from API with request deduplication and cancellation
|
|
141
|
-
* @param {object} options - Request options
|
|
142
|
-
* @param {number} options.debounceMs - Optional debounce delay in milliseconds
|
|
143
|
-
* @returns {Promise} Promise that resolves with REST response
|
|
144
|
-
*/
|
|
145
|
-
async fetch(options = {}) {
|
|
146
|
-
let url = options.url;
|
|
147
|
-
if (!url) {
|
|
148
|
-
const id = options.id || this.getId();
|
|
149
|
-
if (!id && this.options.requiresId !== false) {
|
|
150
|
-
throw new Error("Model: ID is required for fetching");
|
|
151
|
-
}
|
|
152
|
-
url = this.buildUrl(id);
|
|
153
|
-
}
|
|
154
|
-
const requestKey = JSON.stringify({ url, params: options.params });
|
|
155
|
-
if (options.debounceMs && options.debounceMs > 0) {
|
|
156
|
-
return this._debouncedFetch(requestKey, options);
|
|
157
|
-
}
|
|
158
|
-
if (this.currentRequest && this.currentRequestKey !== requestKey) {
|
|
159
|
-
console.info("Model: Cancelling previous request for new parameters");
|
|
160
|
-
this.abortController?.abort();
|
|
161
|
-
this.currentRequest = null;
|
|
162
|
-
}
|
|
163
|
-
if (this.currentRequest && this.currentRequestKey === requestKey) {
|
|
164
|
-
console.info("Model: Duplicate request in progress, returning existing promise");
|
|
165
|
-
return this.currentRequest;
|
|
166
|
-
}
|
|
167
|
-
const now = Date.now();
|
|
168
|
-
const minInterval = 100;
|
|
169
|
-
if (this.lastFetchTime && now - this.lastFetchTime < minInterval) {
|
|
170
|
-
console.info("Model: Rate limited, skipping fetch");
|
|
171
|
-
return this;
|
|
172
|
-
}
|
|
173
|
-
this.loading = true;
|
|
174
|
-
this.errors = {};
|
|
175
|
-
this.lastFetchTime = now;
|
|
176
|
-
this.currentRequestKey = requestKey;
|
|
177
|
-
this.abortController = new AbortController();
|
|
178
|
-
this.currentRequest = this._performFetch(url, options, this.abortController);
|
|
179
|
-
try {
|
|
180
|
-
const result = await this.currentRequest;
|
|
181
|
-
return result;
|
|
182
|
-
} catch (error) {
|
|
183
|
-
if (error.name === "AbortError") {
|
|
184
|
-
console.info("Model: Request was cancelled");
|
|
185
|
-
return this;
|
|
186
|
-
}
|
|
187
|
-
throw error;
|
|
188
|
-
} finally {
|
|
189
|
-
this.currentRequest = null;
|
|
190
|
-
this.currentRequestKey = null;
|
|
191
|
-
this.abortController = null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Handle debounced fetch requests
|
|
196
|
-
* @param {string} requestKey - Unique key for this request
|
|
197
|
-
* @param {object} options - Fetch options
|
|
198
|
-
* @returns {Promise} Promise that resolves with REST response
|
|
199
|
-
*/
|
|
200
|
-
async _debouncedFetch(requestKey, options) {
|
|
201
|
-
if (this.debouncedFetchTimeout) {
|
|
202
|
-
clearTimeout(this.debouncedFetchTimeout);
|
|
203
|
-
}
|
|
204
|
-
this.cancel();
|
|
205
|
-
return new Promise((resolve, reject) => {
|
|
206
|
-
this.debouncedFetchTimeout = setTimeout(async () => {
|
|
207
|
-
try {
|
|
208
|
-
const result = await this.fetch({ ...options, debounceMs: 0 });
|
|
209
|
-
resolve(result);
|
|
210
|
-
} catch (error) {
|
|
211
|
-
reject(error);
|
|
212
|
-
}
|
|
213
|
-
}, options.debounceMs);
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Internal method to perform the actual fetch
|
|
218
|
-
* @param {string} url - API endpoint URL
|
|
219
|
-
* @param {object} options - Request options
|
|
220
|
-
* @param {AbortController} abortController - Controller for request cancellation
|
|
221
|
-
* @returns {Promise} Promise that resolves with REST response
|
|
222
|
-
*/
|
|
223
|
-
async _performFetch(url, options, abortController) {
|
|
224
|
-
try {
|
|
225
|
-
if (options.graph && (!options.params || !options.params.graph)) {
|
|
226
|
-
if (!options.params) options.params = {};
|
|
227
|
-
options.params.graph = options.graph;
|
|
228
|
-
}
|
|
229
|
-
const response = await this.rest.GET(url, options.params, {
|
|
230
|
-
signal: abortController.signal
|
|
231
|
-
});
|
|
232
|
-
if (response.success) {
|
|
233
|
-
if (response.data.status) {
|
|
234
|
-
this.originalAttributes = { ...this.attributes };
|
|
235
|
-
this.set(response.data.data);
|
|
236
|
-
this.errors = {};
|
|
237
|
-
} else {
|
|
238
|
-
this.errors = response.data;
|
|
239
|
-
}
|
|
240
|
-
} else {
|
|
241
|
-
this.errors = response.errors || {};
|
|
242
|
-
}
|
|
243
|
-
return response;
|
|
244
|
-
} catch (error) {
|
|
245
|
-
if (error.name === "AbortError") {
|
|
246
|
-
console.info("Model: Fetch was cancelled");
|
|
247
|
-
throw error;
|
|
248
|
-
}
|
|
249
|
-
this.errors = { fetch: error.message };
|
|
250
|
-
return {
|
|
251
|
-
success: false,
|
|
252
|
-
error: error.message,
|
|
253
|
-
status: error.status || 500
|
|
254
|
-
};
|
|
255
|
-
} finally {
|
|
256
|
-
this.loading = false;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Save model to API (create or update)
|
|
261
|
-
* @param {object} data - Data to save to the model
|
|
262
|
-
* @param {object} options - Request options
|
|
263
|
-
* @returns {Promise} Promise that resolves with REST response
|
|
264
|
-
*/
|
|
265
|
-
async save(data, options = {}) {
|
|
266
|
-
const isNew = !this.id;
|
|
267
|
-
const method = isNew ? "POST" : "PUT";
|
|
268
|
-
const url = isNew ? this.buildUrl() : this.buildUrl(this.id);
|
|
269
|
-
this.loading = true;
|
|
270
|
-
this.errors = {};
|
|
271
|
-
try {
|
|
272
|
-
const response = await this.rest[method](url, data, options.params);
|
|
273
|
-
if (response.success) {
|
|
274
|
-
if (response.data.status) {
|
|
275
|
-
this.originalAttributes = { ...this.attributes };
|
|
276
|
-
this.set(response.data.data);
|
|
277
|
-
this.errors = {};
|
|
278
|
-
} else {
|
|
279
|
-
this.errors = response.data;
|
|
280
|
-
}
|
|
281
|
-
} else {
|
|
282
|
-
this.errors = response.errors || {};
|
|
283
|
-
}
|
|
284
|
-
return response;
|
|
285
|
-
} catch (error) {
|
|
286
|
-
return {
|
|
287
|
-
success: false,
|
|
288
|
-
error: error.message,
|
|
289
|
-
status: error.status || 500
|
|
290
|
-
};
|
|
291
|
-
} finally {
|
|
292
|
-
this.loading = false;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
/**
|
|
296
|
-
* Delete model from API
|
|
297
|
-
* @param {object} options - Request options
|
|
298
|
-
* @returns {Promise} Promise that resolves with REST response
|
|
299
|
-
*/
|
|
300
|
-
async destroy(options = {}) {
|
|
301
|
-
if (!this.id) {
|
|
302
|
-
this.errors = { destroy: "Cannot destroy model without ID" };
|
|
303
|
-
return {
|
|
304
|
-
success: false,
|
|
305
|
-
error: "Cannot destroy model without ID",
|
|
306
|
-
status: 400
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
const url = this.buildUrl(this.id);
|
|
310
|
-
this.loading = true;
|
|
311
|
-
this.errors = {};
|
|
312
|
-
try {
|
|
313
|
-
const response = await this.rest.DELETE(url, options.params);
|
|
314
|
-
if (response.success) {
|
|
315
|
-
this.attributes = {};
|
|
316
|
-
this.originalAttributes = {};
|
|
317
|
-
this.id = null;
|
|
318
|
-
this.errors = {};
|
|
319
|
-
} else {
|
|
320
|
-
this.errors = response.errors || {};
|
|
321
|
-
}
|
|
322
|
-
return response;
|
|
323
|
-
} catch (error) {
|
|
324
|
-
this.errors = { destroy: error.message };
|
|
325
|
-
return {
|
|
326
|
-
success: false,
|
|
327
|
-
error: error.message,
|
|
328
|
-
status: error.status || 500
|
|
329
|
-
};
|
|
330
|
-
} finally {
|
|
331
|
-
this.loading = false;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Check if model has been modified
|
|
336
|
-
* @returns {boolean} True if model has unsaved changes
|
|
337
|
-
*/
|
|
338
|
-
isDirty() {
|
|
339
|
-
return JSON.stringify(this.attributes) !== JSON.stringify(this.originalAttributes);
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Get attributes that have changed since last save
|
|
343
|
-
* @returns {object} Object containing only changed attributes
|
|
344
|
-
*/
|
|
345
|
-
getChangedAttributes() {
|
|
346
|
-
const changed = {};
|
|
347
|
-
for (const [key, value] of Object.entries(this.attributes)) {
|
|
348
|
-
if (this.originalAttributes[key] !== value) {
|
|
349
|
-
changed[key] = value;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
return changed;
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Reset model to original state
|
|
356
|
-
*/
|
|
357
|
-
reset() {
|
|
358
|
-
this.attributes = { ...this.originalAttributes };
|
|
359
|
-
this._ = this.attributes;
|
|
360
|
-
this.errors = {};
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Build URL for API requests
|
|
364
|
-
* @param {string|number} id - Optional ID to append to URL
|
|
365
|
-
* @returns {string} Complete API URL
|
|
366
|
-
*/
|
|
367
|
-
buildUrl(id = null) {
|
|
368
|
-
let url = this.endpoint;
|
|
369
|
-
if (id) {
|
|
370
|
-
url = url.endsWith("/") ? `${url}${id}` : `${url}/${id}`;
|
|
371
|
-
}
|
|
372
|
-
return url;
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Convert model to JSON
|
|
376
|
-
* @returns {object} Model attributes as plain object
|
|
377
|
-
*/
|
|
378
|
-
toJSON() {
|
|
379
|
-
return {
|
|
380
|
-
id: this.id,
|
|
381
|
-
...this.attributes
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Validate model attributes
|
|
386
|
-
* @returns {boolean} True if valid, false if validation errors exist
|
|
387
|
-
*/
|
|
388
|
-
validate() {
|
|
389
|
-
this.errors = {};
|
|
390
|
-
if (this.constructor.validations) {
|
|
391
|
-
for (const [field, rules] of Object.entries(this.constructor.validations)) {
|
|
392
|
-
this.validateField(field, rules);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return Object.keys(this.errors).length === 0;
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Validate a single field
|
|
399
|
-
* @param {string} field - Field name
|
|
400
|
-
* @param {object|array} rules - Validation rules
|
|
401
|
-
*/
|
|
402
|
-
validateField(field, rules) {
|
|
403
|
-
const value = this.get(field);
|
|
404
|
-
const rulesArray = Array.isArray(rules) ? rules : [rules];
|
|
405
|
-
for (const rule of rulesArray) {
|
|
406
|
-
if (typeof rule === "function") {
|
|
407
|
-
const result = rule(value, this);
|
|
408
|
-
if (result !== true) {
|
|
409
|
-
this.errors[field] = result || `${field} is invalid`;
|
|
410
|
-
break;
|
|
411
|
-
}
|
|
412
|
-
} else if (typeof rule === "object") {
|
|
413
|
-
if (rule.required && (value === void 0 || value === null || value === "")) {
|
|
414
|
-
this.errors[field] = rule.message || `${field} is required`;
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
if (rule.minLength && value && value.length < rule.minLength) {
|
|
418
|
-
this.errors[field] = rule.message || `${field} must be at least ${rule.minLength} characters`;
|
|
419
|
-
break;
|
|
420
|
-
}
|
|
421
|
-
if (rule.maxLength && value && value.length > rule.maxLength) {
|
|
422
|
-
this.errors[field] = rule.message || `${field} must be no more than ${rule.maxLength} characters`;
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
if (rule.pattern && value && !rule.pattern.test(value)) {
|
|
426
|
-
this.errors[field] = rule.message || `${field} format is invalid`;
|
|
427
|
-
break;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
// EventEmitter API: on, off, once, emit (from mixin).
|
|
433
|
-
/**
|
|
434
|
-
* Static method to create and fetch a model by ID
|
|
435
|
-
* @param {string|number} id - Model ID
|
|
436
|
-
* @param {object} options - Options
|
|
437
|
-
* @returns {Promise<RestModel>} Promise that resolves with fetched model
|
|
438
|
-
*/
|
|
439
|
-
static async find(id, options = {}) {
|
|
440
|
-
const model = new this({}, options);
|
|
441
|
-
await model.fetch({ id, ...options });
|
|
442
|
-
return model;
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Static method to create a new model with data
|
|
446
|
-
* @param {object} data - Model data
|
|
447
|
-
* @param {object} options - Options
|
|
448
|
-
* @returns {RestModel} New model instance
|
|
449
|
-
*/
|
|
450
|
-
static create(data = {}, options = {}) {
|
|
451
|
-
return new this(data, options);
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Cancel any active fetch request
|
|
455
|
-
* @returns {boolean} True if a request was cancelled, false if no active request
|
|
456
|
-
*/
|
|
457
|
-
cancel() {
|
|
458
|
-
if (this.currentRequest && this.abortController) {
|
|
459
|
-
console.info("Model: Manually cancelling active request");
|
|
460
|
-
this.abortController.abort();
|
|
461
|
-
return true;
|
|
462
|
-
}
|
|
463
|
-
if (this.debouncedFetchTimeout) {
|
|
464
|
-
clearTimeout(this.debouncedFetchTimeout);
|
|
465
|
-
this.debouncedFetchTimeout = null;
|
|
466
|
-
return true;
|
|
467
|
-
}
|
|
468
|
-
return false;
|
|
469
|
-
}
|
|
470
|
-
/**
|
|
471
|
-
* Check if model has an active fetch request
|
|
472
|
-
* @returns {boolean} True if fetch is in progress
|
|
473
|
-
*/
|
|
474
|
-
isFetching() {
|
|
475
|
-
return !!this.currentRequest;
|
|
476
|
-
}
|
|
477
|
-
async showError(message) {
|
|
478
|
-
await Dialog.alert(message, "Error", {
|
|
479
|
-
size: "md",
|
|
480
|
-
class: "text-danger"
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
Object.assign(Model.prototype, EventEmitter);
|
|
485
|
-
class Collection {
|
|
486
|
-
constructor(options = {}, data = null) {
|
|
487
|
-
if (Array.isArray(options)) {
|
|
488
|
-
data = options;
|
|
489
|
-
options = data || {};
|
|
490
|
-
} else {
|
|
491
|
-
data = data || options.data || [];
|
|
492
|
-
}
|
|
493
|
-
this.ModelClass = options.ModelClass || Model;
|
|
494
|
-
this.models = [];
|
|
495
|
-
this.loading = false;
|
|
496
|
-
this.errors = {};
|
|
497
|
-
this.meta = {};
|
|
498
|
-
this.rest = rest;
|
|
499
|
-
if (data) {
|
|
500
|
-
this.add(data);
|
|
501
|
-
}
|
|
502
|
-
this.params = {
|
|
503
|
-
start: 0,
|
|
504
|
-
size: options.size || 10,
|
|
505
|
-
...options.params
|
|
506
|
-
};
|
|
507
|
-
this.endpoint = options.endpoint || this.ModelClass.endpoint || "";
|
|
508
|
-
if (!this.endpoint) {
|
|
509
|
-
let tmp = new this.ModelClass();
|
|
510
|
-
this.endpoint = tmp.endpoint;
|
|
511
|
-
}
|
|
512
|
-
this.restEnabled = this.endpoint ? true : false;
|
|
513
|
-
if (options.restEnabled !== void 0) {
|
|
514
|
-
this.restEnabled = options.restEnabled;
|
|
515
|
-
}
|
|
516
|
-
this.options = {
|
|
517
|
-
parse: true,
|
|
518
|
-
reset: true,
|
|
519
|
-
preloaded: false,
|
|
520
|
-
...options
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
getModelName() {
|
|
524
|
-
return this.ModelClass.name;
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* Fetch collection data from API
|
|
528
|
-
* @param {object} additionalParams - Additional parameters to merge for this fetch only
|
|
529
|
-
* @returns {Promise} Promise that resolves with REST response
|
|
530
|
-
*/
|
|
531
|
-
async fetch(additionalParams = {}) {
|
|
532
|
-
const requestKey = JSON.stringify({ ...this.params, ...additionalParams });
|
|
533
|
-
if (this.currentRequest && this.currentRequestKey !== requestKey) {
|
|
534
|
-
console.info("Collection: Cancelling previous request for new parameters");
|
|
535
|
-
this.abortController?.abort();
|
|
536
|
-
this.currentRequest = null;
|
|
537
|
-
}
|
|
538
|
-
if (this.currentRequest && this.currentRequestKey === requestKey) {
|
|
539
|
-
console.info("Collection: Duplicate request in progress, returning existing promise");
|
|
540
|
-
return this.currentRequest;
|
|
541
|
-
}
|
|
542
|
-
const now = Date.now();
|
|
543
|
-
const minInterval = 100;
|
|
544
|
-
if (this.options.rateLimiting && this.lastFetchTime && now - this.lastFetchTime < minInterval) {
|
|
545
|
-
console.info("Collection: Rate limited, skipping fetch");
|
|
546
|
-
return { success: true, message: "Rate limited, skipping fetch", data: { data: this.toJSON() } };
|
|
547
|
-
}
|
|
548
|
-
if (!this.restEnabled) {
|
|
549
|
-
console.info("Collection: REST disabled, skipping fetch");
|
|
550
|
-
return { success: true, message: "REST disabled, skipping fetch", data: { data: this.toJSON() } };
|
|
551
|
-
}
|
|
552
|
-
if (this.options.preloaded && this.models.length > 0) {
|
|
553
|
-
console.info("Collection: Using preloaded data, skipping fetch");
|
|
554
|
-
return { success: true, message: "Using preloaded data, skipping fetch", data: { data: this.toJSON() } };
|
|
555
|
-
}
|
|
556
|
-
const url = this.buildUrl();
|
|
557
|
-
this.loading = true;
|
|
558
|
-
this.errors = {};
|
|
559
|
-
this.lastFetchTime = now;
|
|
560
|
-
this.currentRequestKey = requestKey;
|
|
561
|
-
this.abortController = new AbortController();
|
|
562
|
-
this.currentRequest = this._performFetch(url, additionalParams, this.abortController);
|
|
563
|
-
try {
|
|
564
|
-
const result = await this.currentRequest;
|
|
565
|
-
return result;
|
|
566
|
-
} catch (error) {
|
|
567
|
-
if (error.name === "AbortError") {
|
|
568
|
-
console.info("Collection: Request was cancelled");
|
|
569
|
-
return { success: false, error: "Request cancelled", status: 0 };
|
|
570
|
-
}
|
|
571
|
-
return {
|
|
572
|
-
success: false,
|
|
573
|
-
error: error.message,
|
|
574
|
-
status: error.status || 500
|
|
575
|
-
};
|
|
576
|
-
} finally {
|
|
577
|
-
this.currentRequest = null;
|
|
578
|
-
this.currentRequestKey = null;
|
|
579
|
-
this.abortController = null;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
/**
|
|
583
|
-
* Internal method to perform the actual fetch
|
|
584
|
-
* @param {string} url - API endpoint URL
|
|
585
|
-
* @param {object} additionalParams - Additional parameters
|
|
586
|
-
* @param {AbortController} abortController - Controller for request cancellation
|
|
587
|
-
* @returns {Promise} Promise that resolves with REST response
|
|
588
|
-
*/
|
|
589
|
-
async _performFetch(url, additionalParams, abortController) {
|
|
590
|
-
const fetchParams = { ...this.params, ...additionalParams };
|
|
591
|
-
console.log("Fetching collection data from", url, fetchParams);
|
|
592
|
-
try {
|
|
593
|
-
this.emit("fetch:start");
|
|
594
|
-
const response = await this.rest.GET(url, fetchParams, {
|
|
595
|
-
signal: abortController.signal
|
|
596
|
-
});
|
|
597
|
-
if (response.success && response.data.status) {
|
|
598
|
-
const data = this.options.parse ? this.parse(response) : response.data;
|
|
599
|
-
if (this.options.reset || additionalParams.reset !== false) {
|
|
600
|
-
this.reset();
|
|
601
|
-
}
|
|
602
|
-
this.add(data, { silent: additionalParams.silent });
|
|
603
|
-
this.errors = {};
|
|
604
|
-
this.emit("fetch:success");
|
|
605
|
-
} else {
|
|
606
|
-
if (response.data && response.data.error) {
|
|
607
|
-
this.errors = response.data;
|
|
608
|
-
this.emit("fetch:error", { message: response.data.error, error: response.data });
|
|
609
|
-
} else {
|
|
610
|
-
this.errors = response.errors || {};
|
|
611
|
-
this.emit("fetch:error", { error: response.errors });
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
return response;
|
|
615
|
-
} catch (error) {
|
|
616
|
-
if (error.name === "AbortError") {
|
|
617
|
-
console.info("Collection: Fetch was cancelled");
|
|
618
|
-
return { success: false, error: "Request cancelled", status: 0 };
|
|
619
|
-
}
|
|
620
|
-
this.errors = { fetch: error.message };
|
|
621
|
-
this.emit("fetch:error", { message: error.message, error });
|
|
622
|
-
return {
|
|
623
|
-
success: false,
|
|
624
|
-
error: error.message,
|
|
625
|
-
status: error.status || 500
|
|
626
|
-
};
|
|
627
|
-
} finally {
|
|
628
|
-
this.loading = false;
|
|
629
|
-
this.emit("fetch:end");
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Update collection parameters and optionally fetch new data
|
|
634
|
-
* @param {object} newParams - Parameters to update
|
|
635
|
-
* @param {boolean} autoFetch - Whether to automatically fetch after updating params
|
|
636
|
-
* @param {number} debounceMs - Optional debounce delay in milliseconds
|
|
637
|
-
* @returns {Promise} Promise that resolves with REST response if autoFetch=true, or collection if autoFetch=false
|
|
638
|
-
*/
|
|
639
|
-
async updateParams(newParams, autoFetch = false, debounceMs = 0) {
|
|
640
|
-
return await this.setParams({ ...this.params, ...newParams }, autoFetch, debounceMs);
|
|
641
|
-
}
|
|
642
|
-
async setParams(newParams, autoFetch = false, debounceMs = 0) {
|
|
643
|
-
this.params = newParams;
|
|
644
|
-
if (autoFetch && this.restEnabled) {
|
|
645
|
-
if (debounceMs > 0) {
|
|
646
|
-
if (this.debouncedFetchTimeout) {
|
|
647
|
-
clearTimeout(this.debouncedFetchTimeout);
|
|
648
|
-
}
|
|
649
|
-
this.cancel();
|
|
650
|
-
return new Promise((resolve, reject) => {
|
|
651
|
-
this.debouncedFetchTimeout = setTimeout(async () => {
|
|
652
|
-
try {
|
|
653
|
-
const result = await this.fetch();
|
|
654
|
-
resolve(result);
|
|
655
|
-
} catch (error) {
|
|
656
|
-
reject(error);
|
|
657
|
-
}
|
|
658
|
-
}, debounceMs);
|
|
659
|
-
});
|
|
660
|
-
} else {
|
|
661
|
-
return this.fetch();
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
return Promise.resolve(this);
|
|
665
|
-
}
|
|
666
|
-
/**
|
|
667
|
-
* Fetch a single model by ID
|
|
668
|
-
* @param {string|number} id - Model ID to fetch
|
|
669
|
-
* @param {object} options - Additional fetch options
|
|
670
|
-
* @returns {Promise<Model|null>} Promise that resolves with model instance or null if not found
|
|
671
|
-
*/
|
|
672
|
-
async fetchOne(id, options = {}) {
|
|
673
|
-
if (!id) {
|
|
674
|
-
console.warn("Collection: fetchOne requires an ID");
|
|
675
|
-
return null;
|
|
676
|
-
}
|
|
677
|
-
if (!this.restEnabled) {
|
|
678
|
-
console.info("Collection: REST disabled, cannot fetch single item");
|
|
679
|
-
return null;
|
|
680
|
-
}
|
|
681
|
-
try {
|
|
682
|
-
const model = new this.ModelClass({ id }, {
|
|
683
|
-
endpoint: this.endpoint,
|
|
684
|
-
collection: this
|
|
685
|
-
});
|
|
686
|
-
const response = await model.fetch(options);
|
|
687
|
-
if (response.success) {
|
|
688
|
-
if (options.addToCollection === true) {
|
|
689
|
-
const existingModel = this.get(model.id);
|
|
690
|
-
if (!existingModel) {
|
|
691
|
-
this.add(model, { silent: options.silent });
|
|
692
|
-
} else if (options.merge !== false) {
|
|
693
|
-
existingModel.set(model.attributes);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
return model;
|
|
697
|
-
} else {
|
|
698
|
-
console.warn("Collection: fetchOne failed -", response.error || "Unknown error");
|
|
699
|
-
return null;
|
|
700
|
-
}
|
|
701
|
-
} catch (error) {
|
|
702
|
-
console.error("Collection: fetchOne error -", error.message);
|
|
703
|
-
return null;
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* Download collection data in a specified format
|
|
708
|
-
* @param {string} format - The format for the download (e.g., 'csv', 'json')
|
|
709
|
-
* @param {object} options - Download options
|
|
710
|
-
* @returns {Promise} Promise that resolves with the download result
|
|
711
|
-
*/
|
|
712
|
-
async download(format = "json", options = {}) {
|
|
713
|
-
if (!this.restEnabled) {
|
|
714
|
-
console.warn("Collection: REST is not enabled, cannot download from remote.");
|
|
715
|
-
return { success: false, message: "Remote downloads are not enabled for this collection." };
|
|
716
|
-
}
|
|
717
|
-
const url = this.buildUrl();
|
|
718
|
-
const downloadParams = { ...this.params };
|
|
719
|
-
delete downloadParams.start;
|
|
720
|
-
delete downloadParams.size;
|
|
721
|
-
downloadParams.download_format = format;
|
|
722
|
-
const filename = `export-${this.getModelName().toLowerCase()}.${format}`;
|
|
723
|
-
const contentTypes = {
|
|
724
|
-
json: "application/json",
|
|
725
|
-
csv: "text/csv"
|
|
726
|
-
};
|
|
727
|
-
const acceptHeader = contentTypes[format] || "*/*";
|
|
728
|
-
return this.rest.download(url, downloadParams, {
|
|
729
|
-
...options,
|
|
730
|
-
filename,
|
|
731
|
-
headers: { "Accept": acceptHeader }
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
/**
|
|
735
|
-
* Parse response data - override in subclasses for custom parsing
|
|
736
|
-
* @param {object} response - API response
|
|
737
|
-
* @returns {array} Array of model data objects
|
|
738
|
-
*/
|
|
739
|
-
parse(response) {
|
|
740
|
-
if (response.data && Array.isArray(response.data.data)) {
|
|
741
|
-
this.meta = {
|
|
742
|
-
size: response.data.size || 10,
|
|
743
|
-
start: response.data.start || 0,
|
|
744
|
-
count: response.data.count || 0,
|
|
745
|
-
status: response.data.status,
|
|
746
|
-
graph: response.data.graph,
|
|
747
|
-
...response.meta
|
|
748
|
-
};
|
|
749
|
-
return response.data.data;
|
|
750
|
-
}
|
|
751
|
-
if (Array.isArray(response.data)) {
|
|
752
|
-
return response.data;
|
|
753
|
-
}
|
|
754
|
-
return Array.isArray(response) ? response : [response];
|
|
755
|
-
}
|
|
756
|
-
/**
|
|
757
|
-
* Add model(s) to the collection
|
|
758
|
-
* @param {object|array} data - Model data or array of model data
|
|
759
|
-
* @param {object} options - Options for adding models
|
|
760
|
-
*/
|
|
761
|
-
add(data, options = {}) {
|
|
762
|
-
const modelsData = Array.isArray(data) ? data : [data];
|
|
763
|
-
const addedModels = [];
|
|
764
|
-
for (const modelData of modelsData) {
|
|
765
|
-
let model;
|
|
766
|
-
if (modelData instanceof this.ModelClass) {
|
|
767
|
-
model = modelData;
|
|
768
|
-
} else {
|
|
769
|
-
model = new this.ModelClass(modelData, {
|
|
770
|
-
endpoint: this.endpoint,
|
|
771
|
-
collection: this
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
const existingIndex = this.models.findIndex((m) => m.id === model.id);
|
|
775
|
-
if (existingIndex !== -1) {
|
|
776
|
-
if (options.merge !== false) {
|
|
777
|
-
this.models[existingIndex].set(model.attributes);
|
|
778
|
-
}
|
|
779
|
-
} else {
|
|
780
|
-
this.models.push(model);
|
|
781
|
-
addedModels.push(model);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
if (!options.silent && addedModels.length > 0) {
|
|
785
|
-
this.emit("add", { models: addedModels, collection: this });
|
|
786
|
-
this.emit("update", { collection: this });
|
|
787
|
-
}
|
|
788
|
-
return addedModels;
|
|
789
|
-
}
|
|
790
|
-
/**
|
|
791
|
-
* Remove model(s) from the collection
|
|
792
|
-
* @param {Model|array|string|number} models - Model(s) to remove or ID(s)
|
|
793
|
-
* @param {object} options - Options
|
|
794
|
-
*/
|
|
795
|
-
remove(models, options = {}) {
|
|
796
|
-
const modelsToRemove = Array.isArray(models) ? models : [models];
|
|
797
|
-
const removedModels = [];
|
|
798
|
-
for (const model of modelsToRemove) {
|
|
799
|
-
let index = -1;
|
|
800
|
-
if (typeof model === "string" || typeof model === "number") {
|
|
801
|
-
index = this.models.findIndex((m) => m.id == model);
|
|
802
|
-
} else {
|
|
803
|
-
index = this.models.indexOf(model);
|
|
804
|
-
}
|
|
805
|
-
if (index !== -1) {
|
|
806
|
-
const removedModel = this.models.splice(index, 1)[0];
|
|
807
|
-
removedModels.push(removedModel);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
if (!options.silent && removedModels.length > 0) {
|
|
811
|
-
this.emit("remove", { models: removedModels, collection: this });
|
|
812
|
-
this.emit("update", { collection: this });
|
|
813
|
-
}
|
|
814
|
-
return removedModels;
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* Reset the collection (remove all models)
|
|
818
|
-
* @param {array} models - Optional new models to set
|
|
819
|
-
* @param {object} options - Options
|
|
820
|
-
*/
|
|
821
|
-
reset(models = null, options = {}) {
|
|
822
|
-
const previousModels = [...this.models];
|
|
823
|
-
this.models = [];
|
|
824
|
-
if (models) {
|
|
825
|
-
this.add(models, { silent: true, ...options });
|
|
826
|
-
}
|
|
827
|
-
if (!options.silent) {
|
|
828
|
-
this.emit("reset", {
|
|
829
|
-
collection: this,
|
|
830
|
-
previousModels
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
return this;
|
|
834
|
-
}
|
|
835
|
-
/**
|
|
836
|
-
* Get model by ID
|
|
837
|
-
* @param {string|number} id - Model ID
|
|
838
|
-
* @returns {Model|undefined} Model instance or undefined
|
|
839
|
-
*/
|
|
840
|
-
get(id) {
|
|
841
|
-
return this.models.find((model) => model.id == id);
|
|
842
|
-
}
|
|
843
|
-
/**
|
|
844
|
-
* Get model by index
|
|
845
|
-
* @param {number} index - Model index
|
|
846
|
-
* @returns {Model|undefined} Model instance or undefined
|
|
847
|
-
*/
|
|
848
|
-
at(index) {
|
|
849
|
-
return this.models[index];
|
|
850
|
-
}
|
|
851
|
-
/**
|
|
852
|
-
* Get collection length
|
|
853
|
-
* @returns {number} Number of models in collection
|
|
854
|
-
*/
|
|
855
|
-
length() {
|
|
856
|
-
return this.models.length;
|
|
857
|
-
}
|
|
858
|
-
/**
|
|
859
|
-
* Check if collection is empty
|
|
860
|
-
* @returns {boolean} True if collection has no models
|
|
861
|
-
*/
|
|
862
|
-
isEmpty() {
|
|
863
|
-
return this.models.length === 0;
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* Find models matching criteria
|
|
867
|
-
* @param {function|object} criteria - Filter function or object with key-value pairs
|
|
868
|
-
* @returns {array} Array of matching models
|
|
869
|
-
*/
|
|
870
|
-
where(criteria) {
|
|
871
|
-
if (typeof criteria === "function") {
|
|
872
|
-
return this.models.filter(criteria);
|
|
873
|
-
}
|
|
874
|
-
if (typeof criteria === "object") {
|
|
875
|
-
return this.models.filter((model) => {
|
|
876
|
-
return Object.entries(criteria).every(([key, value]) => {
|
|
877
|
-
return model.get(key) === value;
|
|
878
|
-
});
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
return [];
|
|
882
|
-
}
|
|
883
|
-
/**
|
|
884
|
-
* Find first model matching criteria
|
|
885
|
-
* @param {function|object} criteria - Filter function or object with key-value pairs
|
|
886
|
-
* @returns {Model|undefined} First matching model or undefined
|
|
887
|
-
*/
|
|
888
|
-
findWhere(criteria) {
|
|
889
|
-
const results = this.where(criteria);
|
|
890
|
-
return results.length > 0 ? results[0] : void 0;
|
|
891
|
-
}
|
|
892
|
-
/**
|
|
893
|
-
* Iterate over each model in the collection
|
|
894
|
-
* @param {function} callback - Function to execute for each model (model, index, collection)
|
|
895
|
-
* @param {object} thisArg - Optional value to use as this when executing callback
|
|
896
|
-
* @returns {Collection} Returns the collection for chaining
|
|
897
|
-
*/
|
|
898
|
-
forEach(callback, thisArg) {
|
|
899
|
-
if (typeof callback !== "function") {
|
|
900
|
-
throw new TypeError("Callback must be a function");
|
|
901
|
-
}
|
|
902
|
-
this.models.forEach((model, index) => {
|
|
903
|
-
callback.call(thisArg, model, index, this);
|
|
904
|
-
});
|
|
905
|
-
return this;
|
|
906
|
-
}
|
|
907
|
-
/**
|
|
908
|
-
* Sort collection by comparator function
|
|
909
|
-
* @param {function|string} comparator - Comparison function or attribute name
|
|
910
|
-
* @param {object} options - Sort options
|
|
911
|
-
*/
|
|
912
|
-
sort(comparator, options = {}) {
|
|
913
|
-
if (typeof comparator === "string") {
|
|
914
|
-
const attr = comparator;
|
|
915
|
-
comparator = (a, b) => {
|
|
916
|
-
const aVal = a.get(attr);
|
|
917
|
-
const bVal = b.get(attr);
|
|
918
|
-
if (aVal < bVal) return -1;
|
|
919
|
-
if (aVal > bVal) return 1;
|
|
920
|
-
return 0;
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
this.models.sort(comparator);
|
|
924
|
-
if (!options.silent) {
|
|
925
|
-
this.trigger("sort", { collection: this });
|
|
926
|
-
}
|
|
927
|
-
return this;
|
|
928
|
-
}
|
|
929
|
-
/**
|
|
930
|
-
* Convert collection to JSON array
|
|
931
|
-
* @returns {array} Array of model JSON representations
|
|
932
|
-
*/
|
|
933
|
-
toJSON() {
|
|
934
|
-
return this.models.map((model) => model.toJSON());
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* Cancel any active fetch request
|
|
938
|
-
* @returns {boolean} True if a request was cancelled, false if no active request
|
|
939
|
-
*/
|
|
940
|
-
cancel() {
|
|
941
|
-
if (this.currentRequest && this.abortController) {
|
|
942
|
-
console.info("Collection: Manually cancelling active request");
|
|
943
|
-
this.abortController.abort();
|
|
944
|
-
return true;
|
|
945
|
-
}
|
|
946
|
-
return false;
|
|
947
|
-
}
|
|
948
|
-
/**
|
|
949
|
-
* Check if collection has an active fetch request
|
|
950
|
-
* @returns {boolean} True if fetch is in progress
|
|
951
|
-
*/
|
|
952
|
-
isFetching() {
|
|
953
|
-
return !!this.currentRequest;
|
|
954
|
-
}
|
|
955
|
-
/**
|
|
956
|
-
* Build URL for collection endpoint
|
|
957
|
-
* @returns {string} Collection API URL
|
|
958
|
-
*/
|
|
959
|
-
buildUrl() {
|
|
960
|
-
return this.endpoint;
|
|
961
|
-
}
|
|
962
|
-
// EventEmitter API: on, off, once, emit (from mixin).
|
|
963
|
-
/**
|
|
964
|
-
* Iterator support for for...of loops
|
|
965
|
-
*/
|
|
966
|
-
*[Symbol.iterator]() {
|
|
967
|
-
for (const model of this.models) {
|
|
968
|
-
yield model;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
/**
|
|
972
|
-
* Static method to create collection from array data
|
|
973
|
-
* @param {function} ModelClass - Model class constructor
|
|
974
|
-
* @param {array} data - Array of model data
|
|
975
|
-
* @param {object} options - Collection options
|
|
976
|
-
* @returns {Collection} New collection instance
|
|
977
|
-
*/
|
|
978
|
-
static fromArray(ModelClass, data = [], options = {}) {
|
|
979
|
-
const collection = new this(ModelClass, options);
|
|
980
|
-
collection.add(data, { silent: true });
|
|
981
|
-
return collection;
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
Object.assign(Collection.prototype, EventEmitter);
|
|
985
|
-
class ToastService {
|
|
986
|
-
constructor(options = {}) {
|
|
987
|
-
this.options = {
|
|
988
|
-
containerId: "toast-container",
|
|
989
|
-
position: "top-end",
|
|
990
|
-
// top-start, top-center, top-end, middle-start, etc.
|
|
991
|
-
autohide: true,
|
|
992
|
-
defaultDelay: 5e3,
|
|
993
|
-
// 5 seconds
|
|
994
|
-
maxToasts: 5,
|
|
995
|
-
// Maximum number of toasts to show at once
|
|
996
|
-
...options
|
|
997
|
-
};
|
|
998
|
-
this.toasts = /* @__PURE__ */ new Map();
|
|
999
|
-
this.toastCounter = 0;
|
|
1000
|
-
this.init();
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* Initialize the toast service
|
|
1004
|
-
*/
|
|
1005
|
-
init() {
|
|
1006
|
-
this.createContainer();
|
|
1007
|
-
}
|
|
1008
|
-
/**
|
|
1009
|
-
* Create the toast container if it doesn't exist
|
|
1010
|
-
*/
|
|
1011
|
-
createContainer() {
|
|
1012
|
-
let container = document.getElementById(this.options.containerId);
|
|
1013
|
-
if (!container) {
|
|
1014
|
-
container = document.createElement("div");
|
|
1015
|
-
container.id = this.options.containerId;
|
|
1016
|
-
container.className = `toast-container position-fixed ${this.getPositionClasses()}`;
|
|
1017
|
-
container.style.zIndex = "1070";
|
|
1018
|
-
container.setAttribute("aria-live", "polite");
|
|
1019
|
-
container.setAttribute("aria-atomic", "true");
|
|
1020
|
-
document.body.appendChild(container);
|
|
1021
|
-
}
|
|
1022
|
-
this.container = container;
|
|
1023
|
-
}
|
|
1024
|
-
/**
|
|
1025
|
-
* Get CSS classes for toast positioning
|
|
1026
|
-
*/
|
|
1027
|
-
getPositionClasses() {
|
|
1028
|
-
const positionMap = {
|
|
1029
|
-
"top-start": "top-0 start-0 p-3",
|
|
1030
|
-
"top-center": "top-0 start-50 translate-middle-x p-3",
|
|
1031
|
-
"top-end": "top-0 end-0 p-3",
|
|
1032
|
-
"middle-start": "top-50 start-0 translate-middle-y p-3",
|
|
1033
|
-
"middle-center": "top-50 start-50 translate-middle p-3",
|
|
1034
|
-
"middle-end": "top-50 end-0 translate-middle-y p-3",
|
|
1035
|
-
"bottom-start": "bottom-0 start-0 p-3",
|
|
1036
|
-
"bottom-center": "bottom-0 start-50 translate-middle-x p-3",
|
|
1037
|
-
"bottom-end": "bottom-0 end-0 p-3"
|
|
1038
|
-
};
|
|
1039
|
-
return positionMap[this.options.position] || positionMap["top-end"];
|
|
1040
|
-
}
|
|
1041
|
-
/**
|
|
1042
|
-
* Show a success toast
|
|
1043
|
-
* @param {string} message - The message to display
|
|
1044
|
-
* @param {object} options - Additional options
|
|
1045
|
-
*/
|
|
1046
|
-
success(message, options = {}) {
|
|
1047
|
-
return this.show(message, "success", {
|
|
1048
|
-
icon: "bi-check-circle-fill",
|
|
1049
|
-
...options
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
/**
|
|
1053
|
-
* Show an error toast
|
|
1054
|
-
* @param {string} message - The message to display
|
|
1055
|
-
* @param {object} options - Additional options
|
|
1056
|
-
*/
|
|
1057
|
-
error(message, options = {}) {
|
|
1058
|
-
return this.show(message, "error", {
|
|
1059
|
-
icon: "bi-exclamation-triangle-fill",
|
|
1060
|
-
autohide: false,
|
|
1061
|
-
// Keep error toasts visible until manually dismissed
|
|
1062
|
-
...options
|
|
1063
|
-
});
|
|
1064
|
-
}
|
|
1065
|
-
/**
|
|
1066
|
-
* Show an info toast
|
|
1067
|
-
* @param {string} message - The message to display
|
|
1068
|
-
* @param {object} options - Additional options
|
|
1069
|
-
*/
|
|
1070
|
-
info(message, options = {}) {
|
|
1071
|
-
return this.show(message, "info", {
|
|
1072
|
-
icon: "bi-info-circle-fill",
|
|
1073
|
-
...options
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
/**
|
|
1077
|
-
* Show a warning toast
|
|
1078
|
-
* @param {string} message - The message to display
|
|
1079
|
-
* @param {object} options - Additional options
|
|
1080
|
-
*/
|
|
1081
|
-
warning(message, options = {}) {
|
|
1082
|
-
return this.show(message, "warning", {
|
|
1083
|
-
icon: "bi-exclamation-triangle-fill",
|
|
1084
|
-
...options
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
/**
|
|
1088
|
-
* Show a plain toast without specific styling
|
|
1089
|
-
* @param {string} message - The message to display
|
|
1090
|
-
* @param {object} options - Additional options
|
|
1091
|
-
*/
|
|
1092
|
-
plain(message, options = {}) {
|
|
1093
|
-
return this.show(message, "plain", {
|
|
1094
|
-
...options
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
/**
|
|
1098
|
-
* Show a toast with specified type and options
|
|
1099
|
-
* @param {string} message - The message to display
|
|
1100
|
-
* @param {string} type - Toast type (success, error, info, warning)
|
|
1101
|
-
* @param {object} options - Additional options
|
|
1102
|
-
*/
|
|
1103
|
-
show(message, type = "info", options = {}) {
|
|
1104
|
-
this.enforceMaxToasts();
|
|
1105
|
-
const toastId = `toast-${++this.toastCounter}`;
|
|
1106
|
-
const config = {
|
|
1107
|
-
title: this.getDefaultTitle(type),
|
|
1108
|
-
icon: this.getDefaultIcon(type),
|
|
1109
|
-
autohide: this.options.autohide,
|
|
1110
|
-
delay: this.options.defaultDelay,
|
|
1111
|
-
dismissible: true,
|
|
1112
|
-
...options
|
|
1113
|
-
};
|
|
1114
|
-
const toastElement = this.createToastElement(toastId, message, type, config);
|
|
1115
|
-
this.container.appendChild(toastElement);
|
|
1116
|
-
if (typeof bootstrap === "undefined") {
|
|
1117
|
-
throw new Error("Bootstrap is required for ToastService. Make sure Bootstrap 5 is loaded.");
|
|
1118
|
-
}
|
|
1119
|
-
const bsToast = new bootstrap.Toast(toastElement, {
|
|
1120
|
-
autohide: config.autohide,
|
|
1121
|
-
delay: config.delay
|
|
1122
|
-
});
|
|
1123
|
-
this.toasts.set(toastId, {
|
|
1124
|
-
element: toastElement,
|
|
1125
|
-
bootstrap: bsToast,
|
|
1126
|
-
type,
|
|
1127
|
-
message
|
|
1128
|
-
});
|
|
1129
|
-
toastElement.addEventListener("hidden.bs.toast", () => {
|
|
1130
|
-
this.cleanup(toastId);
|
|
1131
|
-
});
|
|
1132
|
-
bsToast.show();
|
|
1133
|
-
return {
|
|
1134
|
-
id: toastId,
|
|
1135
|
-
hide: () => {
|
|
1136
|
-
try {
|
|
1137
|
-
bsToast.hide();
|
|
1138
|
-
} catch (error) {
|
|
1139
|
-
console.warn("Error hiding toast:", error);
|
|
1140
|
-
}
|
|
1141
|
-
},
|
|
1142
|
-
dispose: () => this.cleanup(toastId),
|
|
1143
|
-
updateProgress: options.updateProgress || null
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
/**
|
|
1147
|
-
* Show a toast with a View component in the body
|
|
1148
|
-
* @param {View} view - The View component to display
|
|
1149
|
-
* @param {string} type - Toast type (success, error, info, warning, plain)
|
|
1150
|
-
* @param {object} options - Additional options
|
|
1151
|
-
*/
|
|
1152
|
-
showView(view, type = "info", options = {}) {
|
|
1153
|
-
this.enforceMaxToasts();
|
|
1154
|
-
const toastId = `toast-${++this.toastCounter}`;
|
|
1155
|
-
const config = {
|
|
1156
|
-
title: options.title || this.getDefaultTitle(type),
|
|
1157
|
-
icon: options.icon || this.getDefaultIcon(type),
|
|
1158
|
-
autohide: this.options.autohide,
|
|
1159
|
-
delay: this.options.defaultDelay,
|
|
1160
|
-
dismissible: true,
|
|
1161
|
-
...options
|
|
1162
|
-
};
|
|
1163
|
-
const toastElement = this.createViewToastElement(toastId, view, type, config);
|
|
1164
|
-
this.container.appendChild(toastElement);
|
|
1165
|
-
if (typeof bootstrap === "undefined") {
|
|
1166
|
-
throw new Error("Bootstrap is required for ToastService. Make sure Bootstrap 5 is loaded.");
|
|
1167
|
-
}
|
|
1168
|
-
const bsToast = new bootstrap.Toast(toastElement, {
|
|
1169
|
-
autohide: config.autohide,
|
|
1170
|
-
delay: config.delay
|
|
1171
|
-
});
|
|
1172
|
-
this.toasts.set(toastId, {
|
|
1173
|
-
element: toastElement,
|
|
1174
|
-
bootstrap: bsToast,
|
|
1175
|
-
type,
|
|
1176
|
-
view,
|
|
1177
|
-
message: "View toast"
|
|
1178
|
-
});
|
|
1179
|
-
toastElement.addEventListener("hidden.bs.toast", () => {
|
|
1180
|
-
this.cleanupView(toastId);
|
|
1181
|
-
});
|
|
1182
|
-
const bodyContainer = toastElement.querySelector(".toast-view-body");
|
|
1183
|
-
if (bodyContainer && view) {
|
|
1184
|
-
view.render(true, bodyContainer);
|
|
1185
|
-
}
|
|
1186
|
-
bsToast.show();
|
|
1187
|
-
return {
|
|
1188
|
-
id: toastId,
|
|
1189
|
-
view,
|
|
1190
|
-
hide: () => {
|
|
1191
|
-
try {
|
|
1192
|
-
bsToast.hide();
|
|
1193
|
-
} catch (error) {
|
|
1194
|
-
console.warn("Error hiding view toast:", error);
|
|
1195
|
-
}
|
|
1196
|
-
},
|
|
1197
|
-
dispose: () => this.cleanupView(toastId),
|
|
1198
|
-
updateProgress: (progressInfo) => {
|
|
1199
|
-
if (view && typeof view.updateProgress === "function") {
|
|
1200
|
-
view.updateProgress(progressInfo);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
};
|
|
1204
|
-
}
|
|
1205
|
-
/**
|
|
1206
|
-
* Create toast DOM element
|
|
1207
|
-
*/
|
|
1208
|
-
createToastElement(id, message, type, config) {
|
|
1209
|
-
const toast = document.createElement("div");
|
|
1210
|
-
toast.id = id;
|
|
1211
|
-
toast.className = `toast toast-service-${type}`;
|
|
1212
|
-
toast.setAttribute("role", "alert");
|
|
1213
|
-
toast.setAttribute("aria-live", "assertive");
|
|
1214
|
-
toast.setAttribute("aria-atomic", "true");
|
|
1215
|
-
const header = config.title || config.icon ? this.createToastHeader(config, type) : "";
|
|
1216
|
-
const body = this.createToastBody(message, config.icon && !config.title);
|
|
1217
|
-
toast.innerHTML = `
|
|
1218
|
-
${header}
|
|
1219
|
-
${body}
|
|
1220
|
-
`;
|
|
1221
|
-
return toast;
|
|
1222
|
-
}
|
|
1223
|
-
/**
|
|
1224
|
-
* Create toast DOM element for View component
|
|
1225
|
-
*/
|
|
1226
|
-
createViewToastElement(id, view, type, config) {
|
|
1227
|
-
const toast = document.createElement("div");
|
|
1228
|
-
toast.id = id;
|
|
1229
|
-
toast.className = `toast toast-service-${type}`;
|
|
1230
|
-
toast.setAttribute("role", "alert");
|
|
1231
|
-
toast.setAttribute("aria-live", "assertive");
|
|
1232
|
-
toast.setAttribute("aria-atomic", "true");
|
|
1233
|
-
const header = config.title || config.icon ? this.createToastHeader(config, type) : "";
|
|
1234
|
-
const body = this.createViewToastBody();
|
|
1235
|
-
toast.innerHTML = `
|
|
1236
|
-
${header}
|
|
1237
|
-
${body}
|
|
1238
|
-
`;
|
|
1239
|
-
return toast;
|
|
1240
|
-
}
|
|
1241
|
-
/**
|
|
1242
|
-
* Create toast body for View component
|
|
1243
|
-
*/
|
|
1244
|
-
createViewToastBody() {
|
|
1245
|
-
return `
|
|
1246
|
-
<div class="toast-body p-0">
|
|
1247
|
-
<div class="toast-view-body p-3"></div>
|
|
1248
|
-
</div>
|
|
1249
|
-
`;
|
|
1250
|
-
}
|
|
1251
|
-
/**
|
|
1252
|
-
* Create toast header with title and icon
|
|
1253
|
-
*/
|
|
1254
|
-
createToastHeader(config, _type) {
|
|
1255
|
-
const iconHtml = config.icon ? `<i class="${config.icon} toast-service-icon me-2"></i>` : "";
|
|
1256
|
-
const titleHtml = config.title ? `<strong class="me-auto">${iconHtml}${this.escapeHtml(config.title)}</strong>` : "";
|
|
1257
|
-
const timeHtml = config.showTime ? `<small class="text-muted">${this.getTimeString()}</small>` : "";
|
|
1258
|
-
const closeButton = config.dismissible ? `<button type="button" class="btn-close toast-service-close" data-bs-dismiss="toast" aria-label="Close"></button>` : "";
|
|
1259
|
-
if (!titleHtml && !timeHtml && !closeButton) {
|
|
1260
|
-
return "";
|
|
1261
|
-
}
|
|
1262
|
-
return `
|
|
1263
|
-
<div class="toast-header">
|
|
1264
|
-
${titleHtml}
|
|
1265
|
-
${timeHtml}
|
|
1266
|
-
${closeButton}
|
|
1267
|
-
</div>
|
|
1268
|
-
`;
|
|
1269
|
-
}
|
|
1270
|
-
/**
|
|
1271
|
-
* Create toast body with message
|
|
1272
|
-
*/
|
|
1273
|
-
createToastBody(message, showIcon = false) {
|
|
1274
|
-
const iconHtml = showIcon ? `<i class="${this.getDefaultIcon("info")} toast-service-icon me-2"></i>` : "";
|
|
1275
|
-
return `
|
|
1276
|
-
<div class="toast-body d-flex align-items-center">
|
|
1277
|
-
${iconHtml}
|
|
1278
|
-
<span>${this.escapeHtml(message)}</span>
|
|
1279
|
-
</div>
|
|
1280
|
-
`;
|
|
1281
|
-
}
|
|
1282
|
-
/**
|
|
1283
|
-
* Get default title for toast type
|
|
1284
|
-
*/
|
|
1285
|
-
getDefaultTitle(type) {
|
|
1286
|
-
const titles = {
|
|
1287
|
-
success: "Success",
|
|
1288
|
-
error: "Error",
|
|
1289
|
-
warning: "Warning",
|
|
1290
|
-
info: "Information",
|
|
1291
|
-
plain: ""
|
|
1292
|
-
};
|
|
1293
|
-
return titles[type] || "Notification";
|
|
1294
|
-
}
|
|
1295
|
-
/**
|
|
1296
|
-
* Get default icon for toast type
|
|
1297
|
-
*/
|
|
1298
|
-
getDefaultIcon(type) {
|
|
1299
|
-
const icons = {
|
|
1300
|
-
success: "bi-check-circle-fill",
|
|
1301
|
-
error: "bi-exclamation-triangle-fill",
|
|
1302
|
-
warning: "bi-exclamation-triangle-fill",
|
|
1303
|
-
info: "bi-info-circle-fill",
|
|
1304
|
-
plain: ""
|
|
1305
|
-
};
|
|
1306
|
-
return icons[type] || "bi-info-circle-fill";
|
|
1307
|
-
}
|
|
1308
|
-
/**
|
|
1309
|
-
* Enforce maximum number of toasts
|
|
1310
|
-
*/
|
|
1311
|
-
enforceMaxToasts() {
|
|
1312
|
-
const activeToasts = Array.from(this.toasts.values());
|
|
1313
|
-
if (activeToasts.length >= this.options.maxToasts) {
|
|
1314
|
-
const oldestId = this.toasts.keys().next().value;
|
|
1315
|
-
const oldest = this.toasts.get(oldestId);
|
|
1316
|
-
if (oldest) {
|
|
1317
|
-
oldest.bootstrap.hide();
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
/**
|
|
1322
|
-
* Clean up toast resources
|
|
1323
|
-
*/
|
|
1324
|
-
cleanup(toastId) {
|
|
1325
|
-
const toast = this.toasts.get(toastId);
|
|
1326
|
-
if (toast) {
|
|
1327
|
-
try {
|
|
1328
|
-
toast.bootstrap.dispose();
|
|
1329
|
-
} catch (e) {
|
|
1330
|
-
console.warn("Error disposing toast:", e);
|
|
1331
|
-
}
|
|
1332
|
-
if (toast.element && toast.element.parentNode) {
|
|
1333
|
-
toast.element.parentNode.removeChild(toast.element);
|
|
1334
|
-
}
|
|
1335
|
-
this.toasts.delete(toastId);
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
/**
|
|
1339
|
-
* Clean up view toast resources with proper view disposal
|
|
1340
|
-
*/
|
|
1341
|
-
cleanupView(toastId) {
|
|
1342
|
-
const toast = this.toasts.get(toastId);
|
|
1343
|
-
if (toast) {
|
|
1344
|
-
if (toast.view && typeof toast.view.dispose === "function") {
|
|
1345
|
-
try {
|
|
1346
|
-
toast.view.dispose();
|
|
1347
|
-
} catch (e) {
|
|
1348
|
-
console.warn("Error disposing view in toast:", e);
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
try {
|
|
1352
|
-
toast.bootstrap.dispose();
|
|
1353
|
-
} catch (e) {
|
|
1354
|
-
console.warn("Error disposing toast:", e);
|
|
1355
|
-
}
|
|
1356
|
-
if (toast.element && toast.element.parentNode) {
|
|
1357
|
-
toast.element.parentNode.removeChild(toast.element);
|
|
1358
|
-
}
|
|
1359
|
-
this.toasts.delete(toastId);
|
|
1360
|
-
}
|
|
1361
|
-
}
|
|
1362
|
-
/**
|
|
1363
|
-
* Hide all active toasts
|
|
1364
|
-
*/
|
|
1365
|
-
hideAll() {
|
|
1366
|
-
this.toasts.forEach((toast, _id) => {
|
|
1367
|
-
toast.bootstrap.hide();
|
|
1368
|
-
});
|
|
1369
|
-
}
|
|
1370
|
-
/**
|
|
1371
|
-
* Clear all toasts immediately
|
|
1372
|
-
*/
|
|
1373
|
-
clearAll() {
|
|
1374
|
-
this.toasts.forEach((toast, id) => {
|
|
1375
|
-
this.cleanup(id);
|
|
1376
|
-
});
|
|
1377
|
-
}
|
|
1378
|
-
/**
|
|
1379
|
-
* Get current time string
|
|
1380
|
-
*/
|
|
1381
|
-
getTimeString() {
|
|
1382
|
-
return (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
|
|
1383
|
-
hour: "2-digit",
|
|
1384
|
-
minute: "2-digit"
|
|
1385
|
-
});
|
|
1386
|
-
}
|
|
1387
|
-
/**
|
|
1388
|
-
* Escape HTML to prevent XSS
|
|
1389
|
-
*/
|
|
1390
|
-
escapeHtml(str) {
|
|
1391
|
-
const div = document.createElement("div");
|
|
1392
|
-
div.textContent = str;
|
|
1393
|
-
return div.innerHTML;
|
|
1394
|
-
}
|
|
1395
|
-
/**
|
|
1396
|
-
* Dispose of the entire toast service
|
|
1397
|
-
*/
|
|
1398
|
-
dispose() {
|
|
1399
|
-
this.clearAll();
|
|
1400
|
-
if (this.container && this.container.parentNode) {
|
|
1401
|
-
this.container.parentNode.removeChild(this.container);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
/**
|
|
1405
|
-
* Get statistics about active toasts
|
|
1406
|
-
*/
|
|
1407
|
-
getStats() {
|
|
1408
|
-
const stats = {
|
|
1409
|
-
total: this.toasts.size,
|
|
1410
|
-
byType: {}
|
|
1411
|
-
};
|
|
1412
|
-
this.toasts.forEach((toast) => {
|
|
1413
|
-
stats.byType[toast.type] = (stats.byType[toast.type] || 0) + 1;
|
|
1414
|
-
});
|
|
1415
|
-
return stats;
|
|
1416
|
-
}
|
|
1417
|
-
/**
|
|
1418
|
-
* Set global options
|
|
1419
|
-
*/
|
|
1420
|
-
setOptions(newOptions) {
|
|
1421
|
-
this.options = { ...this.options, ...newOptions };
|
|
1422
|
-
if (newOptions.position) {
|
|
1423
|
-
if (this.container) {
|
|
1424
|
-
this.container.className = `toast-container position-fixed ${this.getPositionClasses()}`;
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
class Group extends Model {
|
|
1430
|
-
constructor(data = {}) {
|
|
1431
|
-
super(data, {
|
|
1432
|
-
endpoint: "/api/group"
|
|
1433
|
-
});
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
class GroupList extends Collection {
|
|
1437
|
-
constructor(options = {}) {
|
|
1438
|
-
super({
|
|
1439
|
-
ModelClass: Group,
|
|
1440
|
-
endpoint: "/api/group",
|
|
1441
|
-
size: 10,
|
|
1442
|
-
...options
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
const GroupKinds = {
|
|
1447
|
-
"org": "Organization",
|
|
1448
|
-
"division": "Division",
|
|
1449
|
-
"department": "Department",
|
|
1450
|
-
"team": "Team",
|
|
1451
|
-
"merchant": "Merchant",
|
|
1452
|
-
"partner": "Partner",
|
|
1453
|
-
"client": "Client",
|
|
1454
|
-
"iso": "ISO",
|
|
1455
|
-
"sales": "Sales",
|
|
1456
|
-
"reseller": "Reseller",
|
|
1457
|
-
"location": "Location",
|
|
1458
|
-
"region": "Region",
|
|
1459
|
-
"route": "Route",
|
|
1460
|
-
"project": "Project",
|
|
1461
|
-
"inventory": "Inventory",
|
|
1462
|
-
"test": "Testing",
|
|
1463
|
-
"misc": "Miscellaneous",
|
|
1464
|
-
"qa": "Quality Assurance"
|
|
1465
|
-
};
|
|
1466
|
-
const GroupKindOptions = Object.entries(GroupKinds).map(([key, label]) => ({
|
|
1467
|
-
value: key,
|
|
1468
|
-
label
|
|
1469
|
-
}));
|
|
1470
|
-
const GroupForms = {
|
|
1471
|
-
create: {
|
|
1472
|
-
title: "Create Group",
|
|
1473
|
-
fields: [
|
|
1474
|
-
{
|
|
1475
|
-
name: "name",
|
|
1476
|
-
type: "text",
|
|
1477
|
-
label: "Group Name",
|
|
1478
|
-
required: true,
|
|
1479
|
-
placeholder: "Enter group name"
|
|
1480
|
-
},
|
|
1481
|
-
{
|
|
1482
|
-
name: "kind",
|
|
1483
|
-
type: "select",
|
|
1484
|
-
label: "Group Kind",
|
|
1485
|
-
required: true,
|
|
1486
|
-
options: GroupKindOptions
|
|
1487
|
-
},
|
|
1488
|
-
{
|
|
1489
|
-
type: "collection",
|
|
1490
|
-
name: "parent",
|
|
1491
|
-
label: "Parent Group",
|
|
1492
|
-
Collection: GroupList,
|
|
1493
|
-
// Collection class
|
|
1494
|
-
labelField: "name",
|
|
1495
|
-
// Field to display in dropdown
|
|
1496
|
-
valueField: "id",
|
|
1497
|
-
// Field to use as value
|
|
1498
|
-
maxItems: 10,
|
|
1499
|
-
// Max items to show in dropdown
|
|
1500
|
-
placeholder: "Search groups...",
|
|
1501
|
-
emptyFetch: false,
|
|
1502
|
-
debounceMs: 300
|
|
1503
|
-
// Search debounce delay
|
|
1504
|
-
}
|
|
1505
|
-
]
|
|
1506
|
-
},
|
|
1507
|
-
edit: {
|
|
1508
|
-
title: "Edit Group",
|
|
1509
|
-
fields: [
|
|
1510
|
-
{
|
|
1511
|
-
name: "name",
|
|
1512
|
-
type: "text",
|
|
1513
|
-
label: "Group Name",
|
|
1514
|
-
required: true,
|
|
1515
|
-
placeholder: "Enter group name"
|
|
1516
|
-
},
|
|
1517
|
-
{
|
|
1518
|
-
name: "kind",
|
|
1519
|
-
type: "select",
|
|
1520
|
-
label: "Group Kind",
|
|
1521
|
-
required: true,
|
|
1522
|
-
options: GroupKindOptions
|
|
1523
|
-
},
|
|
1524
|
-
{
|
|
1525
|
-
type: "collection",
|
|
1526
|
-
name: "parent",
|
|
1527
|
-
label: "Parent Group",
|
|
1528
|
-
Collection: GroupList,
|
|
1529
|
-
// Collection class
|
|
1530
|
-
labelField: "name",
|
|
1531
|
-
// Field to display in dropdown
|
|
1532
|
-
valueField: "id",
|
|
1533
|
-
// Field to use as value
|
|
1534
|
-
maxItems: 10,
|
|
1535
|
-
// Max items to show in dropdown
|
|
1536
|
-
placeholder: "Search groups...",
|
|
1537
|
-
emptyFetch: false,
|
|
1538
|
-
debounceMs: 300
|
|
1539
|
-
// Search debounce delay
|
|
1540
|
-
},
|
|
1541
|
-
{
|
|
1542
|
-
name: "metadata.domain",
|
|
1543
|
-
type: "text",
|
|
1544
|
-
label: "Default Domain",
|
|
1545
|
-
placeholder: "Enter Domain"
|
|
1546
|
-
},
|
|
1547
|
-
{
|
|
1548
|
-
name: "metadata.portal",
|
|
1549
|
-
type: "text",
|
|
1550
|
-
label: "Default Portal",
|
|
1551
|
-
placeholder: "Enter Portal URL"
|
|
1552
|
-
},
|
|
1553
|
-
{
|
|
1554
|
-
name: "is_active",
|
|
1555
|
-
type: "switch",
|
|
1556
|
-
label: "Is Active",
|
|
1557
|
-
cols: 4
|
|
1558
|
-
}
|
|
1559
|
-
]
|
|
1560
|
-
},
|
|
1561
|
-
detailed: {
|
|
1562
|
-
title: "Group Details",
|
|
1563
|
-
fields: [
|
|
1564
|
-
// Profile Header
|
|
1565
|
-
{
|
|
1566
|
-
type: "header",
|
|
1567
|
-
text: "Profile Information",
|
|
1568
|
-
level: 4,
|
|
1569
|
-
class: "text-primary mb-3"
|
|
1570
|
-
},
|
|
1571
|
-
// Avatar and Basic Info
|
|
1572
|
-
{
|
|
1573
|
-
type: "group",
|
|
1574
|
-
columns: { xs: 12, md: 4 },
|
|
1575
|
-
fields: [
|
|
1576
|
-
{
|
|
1577
|
-
type: "image",
|
|
1578
|
-
name: "avatar",
|
|
1579
|
-
size: "lg",
|
|
1580
|
-
imageSize: { width: 200, height: 200 },
|
|
1581
|
-
placeholder: "Upload your avatar",
|
|
1582
|
-
help: "Square images work best",
|
|
1583
|
-
columns: 12
|
|
1584
|
-
},
|
|
1585
|
-
{
|
|
1586
|
-
name: "is_active",
|
|
1587
|
-
type: "switch",
|
|
1588
|
-
label: "Is Active",
|
|
1589
|
-
columns: 12
|
|
1590
|
-
}
|
|
1591
|
-
]
|
|
1592
|
-
},
|
|
1593
|
-
// Profile Details
|
|
1594
|
-
{
|
|
1595
|
-
type: "group",
|
|
1596
|
-
columns: { xs: 12, md: 8 },
|
|
1597
|
-
title: "Details",
|
|
1598
|
-
fields: [
|
|
1599
|
-
{
|
|
1600
|
-
name: "name",
|
|
1601
|
-
type: "text",
|
|
1602
|
-
label: "Group Name",
|
|
1603
|
-
required: true,
|
|
1604
|
-
placeholder: "Enter group name",
|
|
1605
|
-
columns: 12
|
|
1606
|
-
},
|
|
1607
|
-
{
|
|
1608
|
-
name: "kind",
|
|
1609
|
-
type: "select",
|
|
1610
|
-
label: "Group Kind",
|
|
1611
|
-
required: true,
|
|
1612
|
-
columns: 12,
|
|
1613
|
-
options: [
|
|
1614
|
-
{ value: "org", label: "Organization" },
|
|
1615
|
-
{ value: "team", label: "Team" },
|
|
1616
|
-
{ value: "department", label: "Department" },
|
|
1617
|
-
{ value: "merchant", label: "Merchant" },
|
|
1618
|
-
{ value: "iso", label: "ISO" },
|
|
1619
|
-
{ value: "group", label: "Group" }
|
|
1620
|
-
]
|
|
1621
|
-
},
|
|
1622
|
-
{
|
|
1623
|
-
type: "collection",
|
|
1624
|
-
name: "parent",
|
|
1625
|
-
label: "Parent Group",
|
|
1626
|
-
Collection: GroupList,
|
|
1627
|
-
// Collection class
|
|
1628
|
-
labelField: "name",
|
|
1629
|
-
// Field to display in dropdown
|
|
1630
|
-
valueField: "id",
|
|
1631
|
-
// Field to use as value
|
|
1632
|
-
maxItems: 10,
|
|
1633
|
-
// Max items to show in dropdown
|
|
1634
|
-
placeholder: "Search groups...",
|
|
1635
|
-
emptyFetch: false,
|
|
1636
|
-
debounceMs: 300,
|
|
1637
|
-
// Search debounce delay
|
|
1638
|
-
columns: 12
|
|
1639
|
-
}
|
|
1640
|
-
]
|
|
1641
|
-
},
|
|
1642
|
-
// Account Settings
|
|
1643
|
-
{
|
|
1644
|
-
type: "group",
|
|
1645
|
-
columns: 12,
|
|
1646
|
-
title: "Account Settings",
|
|
1647
|
-
class: "pt-3",
|
|
1648
|
-
fields: [
|
|
1649
|
-
{
|
|
1650
|
-
type: "select",
|
|
1651
|
-
name: "metadata.timezone",
|
|
1652
|
-
label: "Timezone",
|
|
1653
|
-
columns: 6,
|
|
1654
|
-
options: [
|
|
1655
|
-
{ value: "America/New_York", text: "Eastern Time" },
|
|
1656
|
-
{ value: "America/Chicago", text: "Central Time" },
|
|
1657
|
-
{ value: "America/Denver", text: "Mountain Time" },
|
|
1658
|
-
{ value: "America/Los_Angeles", text: "Pacific Time" },
|
|
1659
|
-
{ value: "UTC", text: "UTC" }
|
|
1660
|
-
]
|
|
1661
|
-
},
|
|
1662
|
-
{
|
|
1663
|
-
type: "select",
|
|
1664
|
-
name: "metadata.language",
|
|
1665
|
-
label: "Language",
|
|
1666
|
-
columns: 6,
|
|
1667
|
-
options: [
|
|
1668
|
-
{ value: "en", text: "English" },
|
|
1669
|
-
{ value: "es", text: "Spanish" },
|
|
1670
|
-
{ value: "fr", text: "French" },
|
|
1671
|
-
{ value: "de", text: "German" }
|
|
1672
|
-
]
|
|
1673
|
-
},
|
|
1674
|
-
{
|
|
1675
|
-
type: "switch",
|
|
1676
|
-
name: "metadata.notify.email",
|
|
1677
|
-
label: "Email Notifications",
|
|
1678
|
-
columns: 4
|
|
1679
|
-
},
|
|
1680
|
-
{
|
|
1681
|
-
type: "switch",
|
|
1682
|
-
name: "metadata.profile_public",
|
|
1683
|
-
label: "Public Profile",
|
|
1684
|
-
columns: 4
|
|
1685
|
-
}
|
|
1686
|
-
]
|
|
1687
|
-
}
|
|
1688
|
-
]
|
|
1689
|
-
}
|
|
1690
|
-
};
|
|
1691
|
-
Group.EDIT_FORM = GroupForms.edit;
|
|
1692
|
-
Group.ADD_FORM = GroupForms.create;
|
|
1693
|
-
Group.CREATE_FORM = GroupForms.create;
|
|
1694
|
-
Group.GroupKindOptions = GroupKindOptions;
|
|
1695
|
-
Group.GroupKinds = GroupKinds;
|
|
1696
|
-
const Group$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
1697
|
-
__proto__: null,
|
|
1698
|
-
Group,
|
|
1699
|
-
GroupForms,
|
|
1700
|
-
GroupList
|
|
1701
|
-
}, Symbol.toStringTag, { value: "Module" }));
|
|
1702
|
-
class User extends Model {
|
|
1703
|
-
constructor(data = {}) {
|
|
1704
|
-
super(data, {
|
|
1705
|
-
endpoint: "/api/user"
|
|
1706
|
-
});
|
|
1707
|
-
}
|
|
1708
|
-
hasPermission(permission) {
|
|
1709
|
-
if (Array.isArray(permission)) {
|
|
1710
|
-
return permission.some((p) => this.hasPermission(p));
|
|
1711
|
-
}
|
|
1712
|
-
const isSysPermission = permission.startsWith("sys.");
|
|
1713
|
-
const permissionToCheck = isSysPermission ? permission.substring(4) : permission;
|
|
1714
|
-
if (this._hasPermission(permissionToCheck)) {
|
|
1715
|
-
return true;
|
|
1716
|
-
}
|
|
1717
|
-
if (!isSysPermission && this.member && this.member.hasPermission(permission)) {
|
|
1718
|
-
return true;
|
|
1719
|
-
}
|
|
1720
|
-
return false;
|
|
1721
|
-
}
|
|
1722
|
-
_hasPermission(permission) {
|
|
1723
|
-
const permissions = this.get("permissions");
|
|
1724
|
-
if (!permissions) {
|
|
1725
|
-
return false;
|
|
1726
|
-
}
|
|
1727
|
-
return permissions[permission] == true;
|
|
1728
|
-
}
|
|
1729
|
-
hasPerm(p) {
|
|
1730
|
-
return this.hasPermission(p);
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
class UserList extends Collection {
|
|
1734
|
-
constructor(options = {}) {
|
|
1735
|
-
super({
|
|
1736
|
-
ModelClass: User,
|
|
1737
|
-
endpoint: "/api/user",
|
|
1738
|
-
...options
|
|
1739
|
-
});
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
User.PERMISSIONS = [
|
|
1743
|
-
{ name: "manage_users", label: "Manage Users" },
|
|
1744
|
-
{ name: "view_groups", label: "View Groups" },
|
|
1745
|
-
{ name: "manage_groups", label: "Manage Groups" },
|
|
1746
|
-
{ name: "view_metrics", label: "View System Metrics" },
|
|
1747
|
-
{ name: "manage_metrics", label: "Manage System Metrics" },
|
|
1748
|
-
{ name: "view_logs", label: "View Logs" },
|
|
1749
|
-
{ name: "view_incidents", label: "View Incidents" },
|
|
1750
|
-
{ name: "manage_incidents", label: "Manage Incidents" },
|
|
1751
|
-
{ name: "view_tickets", label: "View Tickets" },
|
|
1752
|
-
{ name: "manage_tickets", label: "Manage Tickets" },
|
|
1753
|
-
{ name: "view_admin", label: "View Admin" },
|
|
1754
|
-
{ name: "view_jobs", label: "View Jobs" },
|
|
1755
|
-
{ name: "manage_jobs", label: "Manage Jobs" },
|
|
1756
|
-
{ name: "view_global", label: "View Global" },
|
|
1757
|
-
{ name: "manage_notifications", label: "Manage Notifications" },
|
|
1758
|
-
{ name: "manage_files", label: "Manage Files" },
|
|
1759
|
-
{ name: "force_single_session", label: "Force Single Session" },
|
|
1760
|
-
{ name: "file_vault", label: "Access File Vault" },
|
|
1761
|
-
{ name: "manage_aws", label: "Manage AWS" },
|
|
1762
|
-
{ name: "manage_docit", label: "Manage DocIt" }
|
|
1763
|
-
];
|
|
1764
|
-
User.PERMISSION_FIELDS = [
|
|
1765
|
-
...User.PERMISSIONS.map((permission) => ({
|
|
1766
|
-
name: `permissions.${permission.name}`,
|
|
1767
|
-
type: "switch",
|
|
1768
|
-
label: permission.label,
|
|
1769
|
-
columns: 4
|
|
1770
|
-
}))
|
|
1771
|
-
];
|
|
1772
|
-
const UserForms = {
|
|
1773
|
-
create: {
|
|
1774
|
-
title: "Create User",
|
|
1775
|
-
fields: [
|
|
1776
|
-
{ name: "email", type: "text", label: "Email", required: true },
|
|
1777
|
-
{ name: "phone_number", type: "text", label: "Phone number", columns: 12 },
|
|
1778
|
-
{ name: "display_name", type: "text", label: "Display Name" }
|
|
1779
|
-
]
|
|
1780
|
-
},
|
|
1781
|
-
edit: {
|
|
1782
|
-
title: "Edit User",
|
|
1783
|
-
fields: [
|
|
1784
|
-
{ name: "email", type: "email", label: "Email", columns: 12 },
|
|
1785
|
-
{ name: "display_name", type: "text", label: "Display Name", columns: 12 },
|
|
1786
|
-
{ name: "phone_number", type: "text", label: "Phone number", columns: 12 },
|
|
1787
|
-
{ type: "collection", name: "org", label: "Organization", Collection: GroupList, labelField: "name", valueField: "id", columns: 12 }
|
|
1788
|
-
]
|
|
1789
|
-
},
|
|
1790
|
-
permissions: {
|
|
1791
|
-
title: "Edit Permissions",
|
|
1792
|
-
fields: User.PERMISSIONS_FIELDS
|
|
1793
|
-
}
|
|
1794
|
-
};
|
|
1795
|
-
const UserDataView = {
|
|
1796
|
-
// Basic user profile view
|
|
1797
|
-
profile: {
|
|
1798
|
-
title: "User Profile",
|
|
1799
|
-
columns: 2,
|
|
1800
|
-
fields: [
|
|
1801
|
-
{
|
|
1802
|
-
name: "id",
|
|
1803
|
-
label: "User ID",
|
|
1804
|
-
type: "number",
|
|
1805
|
-
columns: 4
|
|
1806
|
-
},
|
|
1807
|
-
{
|
|
1808
|
-
name: "username",
|
|
1809
|
-
label: "Username",
|
|
1810
|
-
type: "text",
|
|
1811
|
-
format: "lowercase",
|
|
1812
|
-
columns: 4
|
|
1813
|
-
},
|
|
1814
|
-
{
|
|
1815
|
-
name: "last_login",
|
|
1816
|
-
label: "Last Login",
|
|
1817
|
-
type: "datetime",
|
|
1818
|
-
format: "relative",
|
|
1819
|
-
columns: 4
|
|
1820
|
-
},
|
|
1821
|
-
{
|
|
1822
|
-
name: "email",
|
|
1823
|
-
label: "Email",
|
|
1824
|
-
type: "email",
|
|
1825
|
-
columns: 4
|
|
1826
|
-
},
|
|
1827
|
-
{
|
|
1828
|
-
name: "display_name",
|
|
1829
|
-
label: "Display Name",
|
|
1830
|
-
type: "text",
|
|
1831
|
-
columns: 4
|
|
1832
|
-
},
|
|
1833
|
-
{
|
|
1834
|
-
name: "last_activity",
|
|
1835
|
-
label: "Last Activity",
|
|
1836
|
-
type: "datetime",
|
|
1837
|
-
format: "relative",
|
|
1838
|
-
columns: 4
|
|
1839
|
-
},
|
|
1840
|
-
{
|
|
1841
|
-
name: "org.name",
|
|
1842
|
-
label: "Organization",
|
|
1843
|
-
type: "text",
|
|
1844
|
-
columns: 4
|
|
1845
|
-
},
|
|
1846
|
-
{
|
|
1847
|
-
name: "phone_number",
|
|
1848
|
-
label: "Phone Number",
|
|
1849
|
-
type: "text",
|
|
1850
|
-
columns: 4
|
|
1851
|
-
}
|
|
1852
|
-
]
|
|
1853
|
-
},
|
|
1854
|
-
// Activity tracking view
|
|
1855
|
-
activity: {
|
|
1856
|
-
title: "User Activity",
|
|
1857
|
-
columns: 2,
|
|
1858
|
-
fields: [
|
|
1859
|
-
{
|
|
1860
|
-
name: "last_login",
|
|
1861
|
-
label: "Last Login",
|
|
1862
|
-
type: "datetime",
|
|
1863
|
-
format: "relative",
|
|
1864
|
-
colSize: 6
|
|
1865
|
-
},
|
|
1866
|
-
{
|
|
1867
|
-
name: "last_activity",
|
|
1868
|
-
label: "Last Activity",
|
|
1869
|
-
type: "datetime",
|
|
1870
|
-
format: "relative",
|
|
1871
|
-
colSize: 6
|
|
1872
|
-
}
|
|
1873
|
-
]
|
|
1874
|
-
},
|
|
1875
|
-
// Comprehensive view with all data
|
|
1876
|
-
detailed: {
|
|
1877
|
-
title: "Detailed User Information",
|
|
1878
|
-
columns: 2,
|
|
1879
|
-
showEmptyValues: true,
|
|
1880
|
-
emptyValueText: "Not set",
|
|
1881
|
-
fields: [
|
|
1882
|
-
// Basic Info Section
|
|
1883
|
-
{
|
|
1884
|
-
name: "id",
|
|
1885
|
-
label: "User ID",
|
|
1886
|
-
type: "number",
|
|
1887
|
-
colSize: 3
|
|
1888
|
-
},
|
|
1889
|
-
{
|
|
1890
|
-
name: "display_name",
|
|
1891
|
-
label: "Display Name",
|
|
1892
|
-
type: "text",
|
|
1893
|
-
format: 'capitalize|default("Unnamed User")',
|
|
1894
|
-
colSize: 9
|
|
1895
|
-
},
|
|
1896
|
-
{
|
|
1897
|
-
name: "username",
|
|
1898
|
-
label: "Username",
|
|
1899
|
-
type: "text",
|
|
1900
|
-
format: "lowercase",
|
|
1901
|
-
colSize: 6
|
|
1902
|
-
},
|
|
1903
|
-
{
|
|
1904
|
-
name: "email",
|
|
1905
|
-
label: "Email Address",
|
|
1906
|
-
type: "email",
|
|
1907
|
-
colSize: 6
|
|
1908
|
-
},
|
|
1909
|
-
{
|
|
1910
|
-
name: "phone_number",
|
|
1911
|
-
label: "Phone Number",
|
|
1912
|
-
type: "phone",
|
|
1913
|
-
format: 'phone|default("Not provided")',
|
|
1914
|
-
colSize: 6
|
|
1915
|
-
},
|
|
1916
|
-
{
|
|
1917
|
-
name: "is_active",
|
|
1918
|
-
label: "Account Status",
|
|
1919
|
-
type: "boolean",
|
|
1920
|
-
colSize: 6
|
|
1921
|
-
},
|
|
1922
|
-
// Activity Info
|
|
1923
|
-
{
|
|
1924
|
-
name: "last_login",
|
|
1925
|
-
label: "Last Login",
|
|
1926
|
-
type: "datetime",
|
|
1927
|
-
format: "relative",
|
|
1928
|
-
colSize: 6
|
|
1929
|
-
},
|
|
1930
|
-
{
|
|
1931
|
-
name: "last_activity",
|
|
1932
|
-
label: "Last Activity",
|
|
1933
|
-
type: "datetime",
|
|
1934
|
-
format: "relative",
|
|
1935
|
-
colSize: 6
|
|
1936
|
-
},
|
|
1937
|
-
// Avatar Info
|
|
1938
|
-
{
|
|
1939
|
-
name: "avatar.url",
|
|
1940
|
-
label: "Avatar",
|
|
1941
|
-
type: "url",
|
|
1942
|
-
colSize: 12
|
|
1943
|
-
},
|
|
1944
|
-
// Complex Data (will use full width automatically)
|
|
1945
|
-
{
|
|
1946
|
-
name: "permissions",
|
|
1947
|
-
label: "User Permissions",
|
|
1948
|
-
type: "dataview",
|
|
1949
|
-
dataViewColumns: 2,
|
|
1950
|
-
showEmptyValues: false
|
|
1951
|
-
},
|
|
1952
|
-
{
|
|
1953
|
-
name: "metadata",
|
|
1954
|
-
label: "User Metadata",
|
|
1955
|
-
type: "dataview",
|
|
1956
|
-
dataViewColumns: 1
|
|
1957
|
-
},
|
|
1958
|
-
{
|
|
1959
|
-
name: "avatar",
|
|
1960
|
-
label: "Avatar Details",
|
|
1961
|
-
type: "dataview",
|
|
1962
|
-
dataViewColumns: 1
|
|
1963
|
-
}
|
|
1964
|
-
]
|
|
1965
|
-
},
|
|
1966
|
-
// Permissions-focused view
|
|
1967
|
-
permissions: {
|
|
1968
|
-
title: "User Permissions",
|
|
1969
|
-
columns: 1,
|
|
1970
|
-
fields: [
|
|
1971
|
-
{
|
|
1972
|
-
name: "display_name",
|
|
1973
|
-
label: "User",
|
|
1974
|
-
type: "text",
|
|
1975
|
-
format: "capitalize",
|
|
1976
|
-
columns: 12
|
|
1977
|
-
},
|
|
1978
|
-
{
|
|
1979
|
-
name: "permissions",
|
|
1980
|
-
label: "Assigned Permissions",
|
|
1981
|
-
type: "dataview",
|
|
1982
|
-
dataViewColumns: 3,
|
|
1983
|
-
showEmptyValues: false,
|
|
1984
|
-
colSize: 12
|
|
1985
|
-
}
|
|
1986
|
-
]
|
|
1987
|
-
},
|
|
1988
|
-
// Compact summary view
|
|
1989
|
-
summary: {
|
|
1990
|
-
title: "User Summary",
|
|
1991
|
-
columns: 3,
|
|
1992
|
-
fields: [
|
|
1993
|
-
{
|
|
1994
|
-
name: "display_name",
|
|
1995
|
-
label: "Name",
|
|
1996
|
-
type: "text",
|
|
1997
|
-
format: "capitalize|truncate(30)"
|
|
1998
|
-
},
|
|
1999
|
-
{
|
|
2000
|
-
name: "email",
|
|
2001
|
-
label: "Email",
|
|
2002
|
-
type: "email"
|
|
2003
|
-
},
|
|
2004
|
-
{
|
|
2005
|
-
name: "is_active",
|
|
2006
|
-
label: "Status",
|
|
2007
|
-
type: "boolean"
|
|
2008
|
-
},
|
|
2009
|
-
{
|
|
2010
|
-
name: "last_activity",
|
|
2011
|
-
label: "Last Seen",
|
|
2012
|
-
type: "datetime",
|
|
2013
|
-
format: "relative",
|
|
2014
|
-
colSize: 12
|
|
2015
|
-
}
|
|
2016
|
-
]
|
|
2017
|
-
}
|
|
2018
|
-
};
|
|
2019
|
-
User.DATA_VIEW = UserDataView.detailed;
|
|
2020
|
-
User.EDIT_FORM = UserForms.edit;
|
|
2021
|
-
User.ADD_FORM = UserForms.create;
|
|
2022
|
-
class UserDevice extends Model {
|
|
2023
|
-
constructor(data = {}) {
|
|
2024
|
-
super(data, {
|
|
2025
|
-
endpoint: "/api/user/device"
|
|
2026
|
-
});
|
|
2027
|
-
}
|
|
2028
|
-
static async getByDuid(duid) {
|
|
2029
|
-
const model = new UserDevice();
|
|
2030
|
-
const resp = await model.rest.GET("/api/user/device/lookup", { duid });
|
|
2031
|
-
if (resp.success && resp.data && resp.data.data) {
|
|
2032
|
-
return new UserDevice(resp.data.data);
|
|
2033
|
-
}
|
|
2034
|
-
return null;
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
class UserDeviceList extends Collection {
|
|
2038
|
-
constructor(options = {}) {
|
|
2039
|
-
super({
|
|
2040
|
-
ModelClass: UserDevice,
|
|
2041
|
-
endpoint: "/api/user/device",
|
|
2042
|
-
...options
|
|
2043
|
-
});
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
class UserDeviceLocation extends Model {
|
|
2047
|
-
constructor(data = {}) {
|
|
2048
|
-
super(data, {
|
|
2049
|
-
endpoint: "/api/user/device/location"
|
|
2050
|
-
});
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
class UserDeviceLocationList extends Collection {
|
|
2054
|
-
constructor(options = {}) {
|
|
2055
|
-
super({
|
|
2056
|
-
ModelClass: UserDeviceLocation,
|
|
2057
|
-
endpoint: "/api/user/device/location",
|
|
2058
|
-
...options
|
|
2059
|
-
});
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
class ContextMenu extends View {
|
|
2063
|
-
constructor(options = {}) {
|
|
2064
|
-
super({
|
|
2065
|
-
tagName: "div",
|
|
2066
|
-
className: "context-menu-view dropdown",
|
|
2067
|
-
...options
|
|
2068
|
-
});
|
|
2069
|
-
this.config = options.contextMenu || options.config || {};
|
|
2070
|
-
this.context = options.context || {};
|
|
2071
|
-
}
|
|
2072
|
-
/**
|
|
2073
|
-
* Build the dropdown menu HTML from the configuration.
|
|
2074
|
-
*/
|
|
2075
|
-
async renderTemplate() {
|
|
2076
|
-
const menuItems = this.config.items || [];
|
|
2077
|
-
if (menuItems.length === 0) {
|
|
2078
|
-
return "";
|
|
2079
|
-
}
|
|
2080
|
-
const triggerIcon = this.config.icon || "bi-three-dots-horizontal";
|
|
2081
|
-
const buttonClass = this.config.buttonClass || "btn btn-link text-secondary ps-3 pe-0 pt-0 pb-1";
|
|
2082
|
-
const dropdownId = `context-menu-${this.id}`;
|
|
2083
|
-
const menuItemsHtml = menuItems.map((item) => this.buildMenuItemHTML(item)).join("");
|
|
2084
|
-
return `
|
|
2085
|
-
<button class="${buttonClass}" type="button" id="${dropdownId}" data-bs-toggle="dropdown" aria-expanded="false">
|
|
2086
|
-
<i class="${triggerIcon}"></i>
|
|
2087
|
-
</button>
|
|
2088
|
-
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="${dropdownId}">
|
|
2089
|
-
${menuItemsHtml}
|
|
2090
|
-
</ul>
|
|
2091
|
-
`;
|
|
2092
|
-
}
|
|
2093
|
-
/**
|
|
2094
|
-
* Build the HTML for a single menu item.
|
|
2095
|
-
* @param {object} item - The menu item configuration.
|
|
2096
|
-
* @returns {string} The HTML string for the list item.
|
|
2097
|
-
*/
|
|
2098
|
-
buildMenuItemHTML(item) {
|
|
2099
|
-
if (item.type === "divider" || item.separator) {
|
|
2100
|
-
return '<li><hr class="dropdown-divider"></li>';
|
|
2101
|
-
}
|
|
2102
|
-
const icon = item.icon ? `<i class="${item.icon} me-2"></i>` : "";
|
|
2103
|
-
const label = item.label || "";
|
|
2104
|
-
const itemClass = `dropdown-item ${item.danger ? "text-danger" : ""} ${item.disabled ? "disabled" : ""}`;
|
|
2105
|
-
const action = item.action || "";
|
|
2106
|
-
if (item.href) {
|
|
2107
|
-
return `<li><a class="${itemClass}" href="${item.href}" target="${item.target || "_self"}">${icon}${label}</a></li>`;
|
|
2108
|
-
}
|
|
2109
|
-
return `<li><a class="${itemClass}" href="#" data-action="menu-item-click" data-item-action="${action}">${icon}${label}</a></li>`;
|
|
2110
|
-
}
|
|
2111
|
-
/**
|
|
2112
|
-
* Handle clicks on menu items.
|
|
2113
|
-
* @param {Event} event - The click event.
|
|
2114
|
-
* @param {HTMLElement} element - The clicked anchor element.
|
|
2115
|
-
*/
|
|
2116
|
-
async onActionMenuItemClick(event, element) {
|
|
2117
|
-
event.preventDefault();
|
|
2118
|
-
const action = element.getAttribute("data-item-action");
|
|
2119
|
-
if (!action) return;
|
|
2120
|
-
const menuItem = this.config.items.find((item) => item.action === action);
|
|
2121
|
-
if (!menuItem || menuItem.disabled) return;
|
|
2122
|
-
if (typeof menuItem.handler === "function") {
|
|
2123
|
-
menuItem.handler(this.context, event, element);
|
|
2124
|
-
} else {
|
|
2125
|
-
this.parent.events.dispatch(action, event, element);
|
|
2126
|
-
}
|
|
2127
|
-
this.closeDropdown();
|
|
2128
|
-
}
|
|
2129
|
-
closeDropdown() {
|
|
2130
|
-
const dropdownTrigger = this.element.querySelector('[data-bs-toggle="dropdown"]');
|
|
2131
|
-
if (dropdownTrigger) {
|
|
2132
|
-
const dropdownInstance = window.bootstrap?.Dropdown.getInstance(dropdownTrigger);
|
|
2133
|
-
dropdownInstance?.hide();
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
export {
|
|
2138
|
-
Collection as C,
|
|
2139
|
-
GroupList as G,
|
|
2140
|
-
Model as M,
|
|
2141
|
-
ToastService as T,
|
|
2142
|
-
User as U,
|
|
2143
|
-
Group as a,
|
|
2144
|
-
ContextMenu as b,
|
|
2145
|
-
GroupForms as c,
|
|
2146
|
-
UserList as d,
|
|
2147
|
-
UserForms as e,
|
|
2148
|
-
UserDataView as f,
|
|
2149
|
-
UserDevice as g,
|
|
2150
|
-
UserDeviceList as h,
|
|
2151
|
-
UserDeviceLocation as i,
|
|
2152
|
-
UserDeviceLocationList as j,
|
|
2153
|
-
Group$1 as k
|
|
2154
|
-
};
|
|
2155
|
-
//# sourceMappingURL=ContextMenu-CE77rUmn.js.map
|