web-mojo 2.1.936 → 2.1.954

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/admin.cjs.js +1 -1
  2. package/dist/admin.cjs.js.map +1 -1
  3. package/dist/admin.es.js +122 -229
  4. package/dist/admin.es.js.map +1 -1
  5. package/dist/auth.cjs.js +1 -1
  6. package/dist/auth.cjs.js.map +1 -1
  7. package/dist/auth.es.js +7 -7
  8. package/dist/auth.es.js.map +1 -1
  9. package/dist/charts.cjs.js +1 -1
  10. package/dist/charts.es.js +7 -7
  11. package/dist/chunks/ChatView-BKa78eKF.js +2 -0
  12. package/dist/chunks/ChatView-BKa78eKF.js.map +1 -0
  13. package/dist/chunks/{ChatView-DlSxjxah.js → ChatView-Dcz7LkwL.js} +89 -574
  14. package/dist/chunks/ChatView-Dcz7LkwL.js.map +1 -0
  15. package/dist/chunks/Collection-OP7c9Zyu.js +989 -0
  16. package/dist/chunks/Collection-OP7c9Zyu.js.map +1 -0
  17. package/dist/chunks/Collection-aQF5eOUH.js +2 -0
  18. package/dist/chunks/Collection-aQF5eOUH.js.map +1 -0
  19. package/dist/chunks/ContextMenu-DV_wsr9B.js +3 -0
  20. package/dist/chunks/ContextMenu-DV_wsr9B.js.map +1 -0
  21. package/dist/chunks/ContextMenu-ixMyAUGS.js +1171 -0
  22. package/dist/chunks/ContextMenu-ixMyAUGS.js.map +1 -0
  23. package/dist/chunks/DataView-CdDY9ijM.js +2 -0
  24. package/dist/chunks/{DataView-XJbTQ5q0.js.map → DataView-CdDY9ijM.js.map} +1 -1
  25. package/dist/chunks/{DataView-Vmjx4eCr.js → DataView-OUqaLmGB.js} +2 -2
  26. package/dist/chunks/{DataView-Vmjx4eCr.js.map → DataView-OUqaLmGB.js.map} +1 -1
  27. package/dist/chunks/{Dialog-D_rAf4gQ.js → Dialog-C2mRUxga.js} +8 -6
  28. package/dist/chunks/{Dialog-D_rAf4gQ.js.map → Dialog-C2mRUxga.js.map} +1 -1
  29. package/dist/chunks/Dialog-Cl6MN8if.js +2 -0
  30. package/dist/chunks/{Dialog-BinTQTfO.js.map → Dialog-Cl6MN8if.js.map} +1 -1
  31. package/dist/chunks/FormView-BSWaXDav.js +3 -0
  32. package/dist/chunks/{FormView-TPFsq8ZX.js.map → FormView-BSWaXDav.js.map} +1 -1
  33. package/dist/chunks/{FormView-CIriLDZY.js → FormView-HWvIdFkB.js} +10 -6
  34. package/dist/chunks/FormView-HWvIdFkB.js.map +1 -0
  35. package/dist/chunks/ListView-Dcz0Gs6D.js +492 -0
  36. package/dist/chunks/ListView-Dcz0Gs6D.js.map +1 -0
  37. package/dist/chunks/ListView-eXgn0F0-.js +2 -0
  38. package/dist/chunks/ListView-eXgn0F0-.js.map +1 -0
  39. package/dist/chunks/MetricsMiniChartWidget-BkTEO87S.js +2 -0
  40. package/dist/chunks/{MetricsMiniChartWidget-sONcM0pG.js.map → MetricsMiniChartWidget-BkTEO87S.js.map} +1 -1
  41. package/dist/chunks/{MetricsMiniChartWidget-BolRZ-Ja.js → MetricsMiniChartWidget-Y70IHFIe.js} +3 -3
  42. package/dist/chunks/{MetricsMiniChartWidget-BolRZ-Ja.js.map → MetricsMiniChartWidget-Y70IHFIe.js.map} +1 -1
  43. package/dist/chunks/PDFViewer-C0aMqGJL.js +2 -0
  44. package/dist/chunks/{PDFViewer-UBhinN8A.js.map → PDFViewer-C0aMqGJL.js.map} +1 -1
  45. package/dist/chunks/{PDFViewer-D6SKOl85.js → PDFViewer-DkbYnnoV.js} +3 -3
  46. package/dist/chunks/{PDFViewer-D6SKOl85.js.map → PDFViewer-DkbYnnoV.js.map} +1 -1
  47. package/dist/chunks/Page-CvbwEoLv.js +2 -0
  48. package/dist/chunks/{Page-CnvHhwLZ.js.map → Page-CvbwEoLv.js.map} +1 -1
  49. package/dist/chunks/{Page-B7L25Omb.js → Page-Deq4y2Kq.js} +2 -2
  50. package/dist/chunks/{Page-B7L25Omb.js.map → Page-Deq4y2Kq.js.map} +1 -1
  51. package/dist/chunks/Rest-BNYqGlnP.js +2 -0
  52. package/dist/chunks/Rest-BNYqGlnP.js.map +1 -0
  53. package/dist/chunks/{WebApp-El07OMHH.js → Rest-CS4jRCAs.js} +5 -1389
  54. package/dist/chunks/Rest-CS4jRCAs.js.map +1 -0
  55. package/dist/chunks/{TopNav-CPA884W7.js → TopNav-DXLRdU0o.js} +5 -5
  56. package/dist/chunks/{TopNav-CPA884W7.js.map → TopNav-DXLRdU0o.js.map} +1 -1
  57. package/dist/chunks/TopNav-GyvI31l2.js +2 -0
  58. package/dist/chunks/{TopNav-Dcmcic-i.js.map → TopNav-GyvI31l2.js.map} +1 -1
  59. package/dist/chunks/WebApp-BAadsDpO.js +2 -0
  60. package/dist/chunks/WebApp-BAadsDpO.js.map +1 -0
  61. package/dist/chunks/WebApp-BVGZC2rj.js +1388 -0
  62. package/dist/chunks/WebApp-BVGZC2rj.js.map +1 -0
  63. package/dist/css/web-mojo.css +2 -2
  64. package/dist/docit.cjs.js +1 -1
  65. package/dist/docit.cjs.js.map +1 -1
  66. package/dist/docit.es.js +12 -10
  67. package/dist/docit.es.js.map +1 -1
  68. package/dist/index.cjs.js +1 -1
  69. package/dist/index.cjs.js.map +1 -1
  70. package/dist/index.es.js +120 -116
  71. package/dist/index.es.js.map +1 -1
  72. package/dist/lightbox.cjs.js +1 -1
  73. package/dist/lightbox.cjs.js.map +1 -1
  74. package/dist/lightbox.es.js +121 -121
  75. package/dist/lightbox.es.js.map +1 -1
  76. package/dist/map.cjs.js +2 -0
  77. package/dist/map.cjs.js.map +1 -0
  78. package/dist/map.es.js +188 -0
  79. package/dist/map.es.js.map +1 -0
  80. package/dist/timeline.cjs.js +2 -0
  81. package/dist/timeline.cjs.js.map +1 -0
  82. package/dist/timeline.es.js +225 -0
  83. package/dist/timeline.es.js.map +1 -0
  84. package/package.json +9 -1
  85. package/dist/chunks/ChatView-DlSxjxah.js.map +0 -1
  86. package/dist/chunks/ChatView-DnqrGXMC.js +0 -2
  87. package/dist/chunks/ChatView-DnqrGXMC.js.map +0 -1
  88. package/dist/chunks/ContextMenu-CE77rUmn.js +0 -2155
  89. package/dist/chunks/ContextMenu-CE77rUmn.js.map +0 -1
  90. package/dist/chunks/ContextMenu-KVxd0Kgd.js +0 -3
  91. package/dist/chunks/ContextMenu-KVxd0Kgd.js.map +0 -1
  92. package/dist/chunks/DataView-XJbTQ5q0.js +0 -2
  93. package/dist/chunks/Dialog-BinTQTfO.js +0 -2
  94. package/dist/chunks/FormView-CIriLDZY.js.map +0 -1
  95. package/dist/chunks/FormView-TPFsq8ZX.js +0 -3
  96. package/dist/chunks/MetricsMiniChartWidget-sONcM0pG.js +0 -2
  97. package/dist/chunks/PDFViewer-UBhinN8A.js +0 -2
  98. package/dist/chunks/Page-CnvHhwLZ.js +0 -2
  99. package/dist/chunks/TopNav-Dcmcic-i.js +0 -2
  100. package/dist/chunks/WebApp-El07OMHH.js.map +0 -1
  101. package/dist/chunks/WebApp-b9DQWz1d.js +0 -2
  102. 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