web-mojo 2.1.46

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