web-mojo 2.2.64 → 2.2.66

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 (100) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/admin.cjs.js +1 -1
  3. package/dist/admin.cjs.js.map +1 -1
  4. package/dist/admin.es.js +1 -1
  5. package/dist/admin.es.js.map +1 -1
  6. package/dist/auth.cjs.js +1 -1
  7. package/dist/auth.es.js +1 -1
  8. package/dist/charts.cjs.js +1 -1
  9. package/dist/charts.es.js +1 -1
  10. package/dist/chunks/ChatView-CYIOOvHM.js +2 -0
  11. package/dist/chunks/ChatView-CYIOOvHM.js.map +1 -0
  12. package/dist/chunks/ChatView-DTcs3Xh7.js +2 -0
  13. package/dist/chunks/ChatView-DTcs3Xh7.js.map +1 -0
  14. package/dist/chunks/{Collection-1sPoIFvQ.js → Collection-BgfCzEE5.js} +2 -2
  15. package/dist/chunks/{Collection-1sPoIFvQ.js.map → Collection-BgfCzEE5.js.map} +1 -1
  16. package/dist/chunks/ContextMenu-C7QJDUar.js +2 -0
  17. package/dist/chunks/ContextMenu-C7QJDUar.js.map +1 -0
  18. package/dist/chunks/ContextMenu-C9RiB1kl.js +2 -0
  19. package/dist/chunks/ContextMenu-C9RiB1kl.js.map +1 -0
  20. package/dist/chunks/{DataView--nUWtq6r.js → DataView-Dl2jl_0h.js} +2 -2
  21. package/dist/chunks/{DataView--nUWtq6r.js.map → DataView-Dl2jl_0h.js.map} +1 -1
  22. package/dist/chunks/{Dialog-DwCTFV6O.js → Dialog-9FzcToYV.js} +2 -2
  23. package/dist/chunks/{Dialog-BcgSR01Z.js.map → Dialog-9FzcToYV.js.map} +1 -1
  24. package/dist/chunks/{Dialog-BcgSR01Z.js → Dialog-DZG_XYbk.js} +2 -2
  25. package/dist/chunks/{Dialog-DwCTFV6O.js.map → Dialog-DZG_XYbk.js.map} +1 -1
  26. package/dist/chunks/Files-BDqCTw9A.js +2 -0
  27. package/dist/chunks/Files-BDqCTw9A.js.map +1 -0
  28. package/dist/chunks/Files-B_NE3QwI.js +2 -0
  29. package/dist/chunks/Files-B_NE3QwI.js.map +1 -0
  30. package/dist/chunks/FormView-CQ0eFCJu.js +3 -0
  31. package/dist/chunks/FormView-CQ0eFCJu.js.map +1 -0
  32. package/dist/chunks/FormView-D2Ia0Idj.js +3 -0
  33. package/dist/chunks/FormView-D2Ia0Idj.js.map +1 -0
  34. package/dist/chunks/{ListView-6JQ6tRXs.js → ListView-BunTmkAU.js} +2 -2
  35. package/dist/chunks/{ListView-6JQ6tRXs.js.map → ListView-BunTmkAU.js.map} +1 -1
  36. package/dist/chunks/{MetricsCountryMapView-J067qrrt.js → MetricsCountryMapView-C_p6hcly.js} +2 -2
  37. package/dist/chunks/{MetricsCountryMapView-J067qrrt.js.map → MetricsCountryMapView-C_p6hcly.js.map} +1 -1
  38. package/dist/chunks/{MetricsMiniChartWidget-BzuQ1Imn.js → MetricsMiniChartWidget-C1RCd-Xw.js} +2 -2
  39. package/dist/chunks/{MetricsMiniChartWidget-BzuQ1Imn.js.map → MetricsMiniChartWidget-C1RCd-Xw.js.map} +1 -1
  40. package/dist/chunks/{MetricsMiniChartWidget-C3oO3WuD.js → MetricsMiniChartWidget-C9QdxxBn.js} +2 -2
  41. package/dist/chunks/{MetricsMiniChartWidget-C3oO3WuD.js.map → MetricsMiniChartWidget-C9QdxxBn.js.map} +1 -1
  42. package/dist/chunks/{PDFViewer-DSa4BZCm.js → PDFViewer-B_LSyM7g.js} +2 -2
  43. package/dist/chunks/{PDFViewer-DSa4BZCm.js.map → PDFViewer-B_LSyM7g.js.map} +1 -1
  44. package/dist/chunks/{PDFViewer-CsyKn-gh.js → PDFViewer-DI9v3Vkg.js} +2 -2
  45. package/dist/chunks/{PDFViewer-CsyKn-gh.js.map → PDFViewer-DI9v3Vkg.js.map} +1 -1
  46. package/dist/chunks/{Rest-DHbszkuP.js → Rest-DFnCoMvk.js} +2 -2
  47. package/dist/chunks/{Rest-DHbszkuP.js.map → Rest-DFnCoMvk.js.map} +1 -1
  48. package/dist/chunks/TokenManager-Dk5oiCh1.js +2 -0
  49. package/dist/chunks/{TokenManager-FlWEoTIa.js.map → TokenManager-Dk5oiCh1.js.map} +1 -1
  50. package/dist/chunks/TokenManager-DzLjXShS.js +2 -0
  51. package/dist/chunks/{TokenManager-DZJGwkbs.js.map → TokenManager-DzLjXShS.js.map} +1 -1
  52. package/dist/chunks/User-BwDOeY-q.js +3 -0
  53. package/dist/chunks/User-BwDOeY-q.js.map +1 -0
  54. package/dist/chunks/User-CbqYlTBK.js +3 -0
  55. package/dist/chunks/User-CbqYlTBK.js.map +1 -0
  56. package/dist/chunks/{WebApp-DovLtA60.js → WebApp-BCfA2Dlw.js} +2 -2
  57. package/dist/chunks/{WebApp-DovLtA60.js.map → WebApp-BCfA2Dlw.js.map} +1 -1
  58. package/dist/chunks/{WebApp-CULZpO_0.js → WebApp-kKz5FV57.js} +2 -2
  59. package/dist/chunks/{WebApp-CULZpO_0.js.map → WebApp-kKz5FV57.js.map} +1 -1
  60. package/dist/chunks/{WebSocketClient-BoF7TV7i.js → WebSocketClient-fEGJDFXJ.js} +2 -2
  61. package/dist/chunks/{WebSocketClient-BoF7TV7i.js.map → WebSocketClient-fEGJDFXJ.js.map} +1 -1
  62. package/dist/chunks/{version-CaVHauah.js → version-Bykv8fhW.js} +2 -2
  63. package/dist/chunks/{version-CaVHauah.js.map → version-Bykv8fhW.js.map} +1 -1
  64. package/dist/chunks/{version-3OFdtu10.js → version-D0hwRoO4.js} +2 -2
  65. package/dist/chunks/{version-3OFdtu10.js.map → version-D0hwRoO4.js.map} +1 -1
  66. package/dist/css/web-mojo.css +1 -1
  67. package/dist/docit.cjs.js +1 -1
  68. package/dist/docit.cjs.js.map +1 -1
  69. package/dist/docit.es.js +1 -1
  70. package/dist/docit.es.js.map +1 -1
  71. package/dist/index.cjs.js +1 -1
  72. package/dist/index.cjs.js.map +1 -1
  73. package/dist/index.es.js +1 -1
  74. package/dist/index.es.js.map +1 -1
  75. package/dist/lightbox.cjs.js +1 -1
  76. package/dist/lightbox.cjs.js.map +1 -1
  77. package/dist/lightbox.es.js +1 -1
  78. package/dist/lightbox.es.js.map +1 -1
  79. package/dist/map.cjs.js +1 -1
  80. package/dist/map.es.js +1 -1
  81. package/dist/timeline.es.js +1 -1
  82. package/dist/web-mojo.lite.iife.js +1226 -3
  83. package/dist/web-mojo.lite.iife.js.map +1 -1
  84. package/dist/web-mojo.lite.iife.min.js +95 -75
  85. package/dist/web-mojo.lite.iife.min.js.map +1 -1
  86. package/package.json +1 -1
  87. package/dist/chunks/ChatView-1-S71P4O.js +0 -2
  88. package/dist/chunks/ChatView-1-S71P4O.js.map +0 -1
  89. package/dist/chunks/ChatView-p0x1jRbF.js +0 -2
  90. package/dist/chunks/ChatView-p0x1jRbF.js.map +0 -1
  91. package/dist/chunks/ContextMenu-CsQGpSrv.js +0 -3
  92. package/dist/chunks/ContextMenu-CsQGpSrv.js.map +0 -1
  93. package/dist/chunks/ContextMenu-dqFxitXY.js +0 -3
  94. package/dist/chunks/ContextMenu-dqFxitXY.js.map +0 -1
  95. package/dist/chunks/FormView-CRmEReTC.js +0 -3
  96. package/dist/chunks/FormView-CRmEReTC.js.map +0 -1
  97. package/dist/chunks/FormView-OLA7t-yv.js +0 -3
  98. package/dist/chunks/FormView-OLA7t-yv.js.map +0 -1
  99. package/dist/chunks/TokenManager-DZJGwkbs.js +0 -2
  100. package/dist/chunks/TokenManager-FlWEoTIa.js +0 -2
@@ -7464,7 +7464,6 @@ var MOJO = (function(exports) {
7464
7464
  {{#help}}<div class="{{helpClass}}">{{help}}</div>{{/help}}
7465
7465
  {{#error}}<div class="{{errorClass}}">{{error}}</div>{{/error}}
7466
7466
  </div>
7467
- </div>
7468
7467
  `,
7469
7468
  switch: `
7470
7469
  <div class="mojo-form-control">
@@ -7481,7 +7480,7 @@ var MOJO = (function(exports) {
7481
7480
  </div>
7482
7481
  `,
7483
7482
  image: `
7484
- <div class="mojo-form-control">
7483
+ <div class="mojo-form-control" style="display: flex; flex-direction: column; align-items: center;">
7485
7484
  {{#label}}
7486
7485
  <label for="{{fieldId}}" class="{{labelClass}}">
7487
7486
  {{label}}{{#required}}<span class="text-danger">*</span>{{/required}}
@@ -13865,7 +13864,7 @@ var MOJO = (function(exports) {
13865
13864
  console.log("FormView: initializeChangeHandlers - found", inputs.length, "inputs");
13866
13865
  inputs.forEach((input) => {
13867
13866
  console.log("FormView: Processing input:", input.type, input.name, input.getAttribute("data-change-action"));
13868
- if (input.hasAttribute("data-component") || input.classList.contains("form-check-input")) {
13867
+ if (input.hasAttribute("data-component") || input.hasAttribute("data-change-action") || input.classList.contains("form-check-input")) {
13869
13868
  return;
13870
13869
  }
13871
13870
  input.addEventListener("change", (event) => {
@@ -17313,6 +17312,56 @@ var MOJO = (function(exports) {
17313
17312
  });
17314
17313
  });
17315
17314
  }
17315
+ /**
17316
+ * Show form in a dialog for simple data collection (no model saving)
17317
+ * @param {object} options - Configuration options
17318
+ * @returns {Promise} Promise that resolves with form data or null if cancelled
17319
+ */
17320
+ static async updateModelImage(options = {}, fieldOptions = {}) {
17321
+ const upload = options.upload || false;
17322
+ const fieldName = fieldOptions.name || options.field || "image";
17323
+ const formOptions = {
17324
+ title: "Upload Your Avatar",
17325
+ model: null,
17326
+ autoSave: !upload,
17327
+ size: "sm",
17328
+ fields: [
17329
+ {
17330
+ type: "image",
17331
+ name: fieldName,
17332
+ size: "lg",
17333
+ imageSize: { width: 200, height: 200 },
17334
+ placeholder: "Upload your image",
17335
+ ...fieldOptions
17336
+ }
17337
+ ],
17338
+ ...options
17339
+ };
17340
+ const result = await Dialog2.showForm(formOptions);
17341
+ if (!upload || !result || !options.model) return result;
17342
+ const base64Data = result[fieldName];
17343
+ if (!base64Data || !base64Data.startsWith("data:")) return result;
17344
+ const arr = base64Data.split(",");
17345
+ const mime = arr[0].match(/:(.*?);/)[1];
17346
+ const bstr = atob(arr[1]);
17347
+ let n = bstr.length;
17348
+ const u8arr = new Uint8Array(n);
17349
+ while (n--) {
17350
+ u8arr[n] = bstr.charCodeAt(n);
17351
+ }
17352
+ const ext = mime.split("/")[1] || "png";
17353
+ const file = new window.File([u8arr], `${fieldName}.${ext}`, { type: mime });
17354
+ const { File: FileModel } = await Promise.resolve().then(() => Files);
17355
+ const fileModel = new FileModel();
17356
+ await fileModel.upload({
17357
+ file,
17358
+ name: `${fieldName}.${ext}`,
17359
+ description: options.uploadDescription || `${fieldName} upload`,
17360
+ showToast: true
17361
+ });
17362
+ const resp = await options.model.save({ [fieldName]: fileModel.id });
17363
+ return resp;
17364
+ }
17316
17365
  static async showModelView(model, options) {
17317
17366
  const modelClass = model.constructor;
17318
17367
  const modelView = modelClass.VIEW_CLASS;
@@ -20968,6 +21017,1180 @@ var MOJO = (function(exports) {
20968
21017
  if (MOJO2) {
20969
21018
  attachLite(MOJO2);
20970
21019
  }
21020
+ class ToastService {
21021
+ constructor(options = {}) {
21022
+ this.options = {
21023
+ containerId: "toast-container",
21024
+ position: "top-end",
21025
+ // top-start, top-center, top-end, middle-start, etc.
21026
+ autohide: true,
21027
+ defaultDelay: 5e3,
21028
+ // 5 seconds
21029
+ maxToasts: 5,
21030
+ // Maximum number of toasts to show at once
21031
+ ...options
21032
+ };
21033
+ this.toasts = /* @__PURE__ */ new Map();
21034
+ this.toastCounter = 0;
21035
+ this.init();
21036
+ }
21037
+ /**
21038
+ * Initialize the toast service
21039
+ */
21040
+ init() {
21041
+ this.createContainer();
21042
+ }
21043
+ /**
21044
+ * Create the toast container if it doesn't exist
21045
+ */
21046
+ createContainer() {
21047
+ let container = document.getElementById(this.options.containerId);
21048
+ if (!container) {
21049
+ container = document.createElement("div");
21050
+ container.id = this.options.containerId;
21051
+ container.className = `toast-container position-fixed ${this.getPositionClasses()}`;
21052
+ container.style.zIndex = "1070";
21053
+ container.setAttribute("aria-live", "polite");
21054
+ container.setAttribute("aria-atomic", "true");
21055
+ document.body.appendChild(container);
21056
+ }
21057
+ this.container = container;
21058
+ }
21059
+ /**
21060
+ * Get CSS classes for toast positioning
21061
+ */
21062
+ getPositionClasses() {
21063
+ const positionMap = {
21064
+ "top-start": "top-0 start-0 p-3",
21065
+ "top-center": "top-0 start-50 translate-middle-x p-3",
21066
+ "top-end": "top-0 end-0 p-3",
21067
+ "middle-start": "top-50 start-0 translate-middle-y p-3",
21068
+ "middle-center": "top-50 start-50 translate-middle p-3",
21069
+ "middle-end": "top-50 end-0 translate-middle-y p-3",
21070
+ "bottom-start": "bottom-0 start-0 p-3",
21071
+ "bottom-center": "bottom-0 start-50 translate-middle-x p-3",
21072
+ "bottom-end": "bottom-0 end-0 p-3"
21073
+ };
21074
+ return positionMap[this.options.position] || positionMap["top-end"];
21075
+ }
21076
+ /**
21077
+ * Show a success toast
21078
+ * @param {string} message - The message to display
21079
+ * @param {object} options - Additional options
21080
+ */
21081
+ success(message, options = {}) {
21082
+ return this.show(message, "success", {
21083
+ icon: "bi-check-circle-fill",
21084
+ ...options
21085
+ });
21086
+ }
21087
+ /**
21088
+ * Show an error toast
21089
+ * @param {string} message - The message to display
21090
+ * @param {object} options - Additional options
21091
+ */
21092
+ error(message, options = {}) {
21093
+ return this.show(message, "error", {
21094
+ icon: "bi-exclamation-triangle-fill",
21095
+ autohide: true,
21096
+ // Keep error toasts visible until manually dismissed
21097
+ ...options
21098
+ });
21099
+ }
21100
+ /**
21101
+ * Show an info toast
21102
+ * @param {string} message - The message to display
21103
+ * @param {object} options - Additional options
21104
+ */
21105
+ info(message, options = {}) {
21106
+ return this.show(message, "info", {
21107
+ icon: "bi-info-circle-fill",
21108
+ ...options
21109
+ });
21110
+ }
21111
+ /**
21112
+ * Show a warning toast
21113
+ * @param {string} message - The message to display
21114
+ * @param {object} options - Additional options
21115
+ */
21116
+ warning(message, options = {}) {
21117
+ return this.show(message, "warning", {
21118
+ icon: "bi-exclamation-triangle-fill",
21119
+ ...options
21120
+ });
21121
+ }
21122
+ /**
21123
+ * Show a plain toast without specific styling
21124
+ * @param {string} message - The message to display
21125
+ * @param {object} options - Additional options
21126
+ */
21127
+ plain(message, options = {}) {
21128
+ return this.show(message, "plain", {
21129
+ ...options
21130
+ });
21131
+ }
21132
+ /**
21133
+ * Show a toast with specified type and options
21134
+ * @param {string} message - The message to display
21135
+ * @param {string} type - Toast type (success, error, info, warning)
21136
+ * @param {object} options - Additional options
21137
+ */
21138
+ show(message, type = "info", options = {}) {
21139
+ this.enforceMaxToasts();
21140
+ const toastId = `toast-${++this.toastCounter}`;
21141
+ const config = {
21142
+ title: this.getDefaultTitle(type),
21143
+ icon: this.getDefaultIcon(type),
21144
+ autohide: this.options.autohide,
21145
+ delay: this.options.defaultDelay,
21146
+ dismissible: true,
21147
+ ...options
21148
+ };
21149
+ const toastElement = this.createToastElement(toastId, message, type, config);
21150
+ this.container.appendChild(toastElement);
21151
+ if (typeof bootstrap === "undefined") {
21152
+ throw new Error("Bootstrap is required for ToastService. Make sure Bootstrap 5 is loaded.");
21153
+ }
21154
+ const bsToast = new bootstrap.Toast(toastElement, {
21155
+ autohide: config.autohide,
21156
+ delay: config.delay
21157
+ });
21158
+ this.toasts.set(toastId, {
21159
+ element: toastElement,
21160
+ bootstrap: bsToast,
21161
+ type,
21162
+ message
21163
+ });
21164
+ toastElement.addEventListener("hidden.bs.toast", () => {
21165
+ this.cleanup(toastId);
21166
+ });
21167
+ bsToast.show();
21168
+ return {
21169
+ id: toastId,
21170
+ hide: () => {
21171
+ try {
21172
+ bsToast.hide();
21173
+ } catch (error) {
21174
+ console.warn("Error hiding toast:", error);
21175
+ }
21176
+ },
21177
+ dispose: () => this.cleanup(toastId),
21178
+ updateProgress: options.updateProgress || null
21179
+ };
21180
+ }
21181
+ /**
21182
+ * Show a toast with a View component in the body
21183
+ * @param {View} view - The View component to display
21184
+ * @param {string} type - Toast type (success, error, info, warning, plain)
21185
+ * @param {object} options - Additional options
21186
+ */
21187
+ showView(view, type = "info", options = {}) {
21188
+ this.enforceMaxToasts();
21189
+ const toastId = `toast-${++this.toastCounter}`;
21190
+ const config = {
21191
+ title: options.title || this.getDefaultTitle(type),
21192
+ icon: options.icon || this.getDefaultIcon(type),
21193
+ autohide: this.options.autohide,
21194
+ delay: this.options.defaultDelay,
21195
+ dismissible: true,
21196
+ ...options
21197
+ };
21198
+ const toastElement = this.createViewToastElement(toastId, view, type, config);
21199
+ this.container.appendChild(toastElement);
21200
+ if (typeof bootstrap === "undefined") {
21201
+ throw new Error("Bootstrap is required for ToastService. Make sure Bootstrap 5 is loaded.");
21202
+ }
21203
+ const bsToast = new bootstrap.Toast(toastElement, {
21204
+ autohide: config.autohide,
21205
+ delay: config.delay
21206
+ });
21207
+ this.toasts.set(toastId, {
21208
+ element: toastElement,
21209
+ bootstrap: bsToast,
21210
+ type,
21211
+ view,
21212
+ message: "View toast"
21213
+ });
21214
+ toastElement.addEventListener("hidden.bs.toast", () => {
21215
+ this.cleanupView(toastId);
21216
+ });
21217
+ const bodyContainer = toastElement.querySelector(".toast-view-body");
21218
+ if (bodyContainer && view) {
21219
+ view.render(true, bodyContainer);
21220
+ }
21221
+ bsToast.show();
21222
+ return {
21223
+ id: toastId,
21224
+ view,
21225
+ hide: () => {
21226
+ try {
21227
+ bsToast.hide();
21228
+ } catch (error) {
21229
+ console.warn("Error hiding view toast:", error);
21230
+ }
21231
+ },
21232
+ dispose: () => this.cleanupView(toastId),
21233
+ updateProgress: (progressInfo) => {
21234
+ if (view && typeof view.updateProgress === "function") {
21235
+ view.updateProgress(progressInfo);
21236
+ }
21237
+ }
21238
+ };
21239
+ }
21240
+ /**
21241
+ * Create toast DOM element
21242
+ */
21243
+ createToastElement(id, message, type, config) {
21244
+ const toast = document.createElement("div");
21245
+ toast.id = id;
21246
+ toast.className = `toast toast-service-${type}`;
21247
+ toast.setAttribute("role", "alert");
21248
+ toast.setAttribute("aria-live", "assertive");
21249
+ toast.setAttribute("aria-atomic", "true");
21250
+ const header = config.title || config.icon ? this.createToastHeader(config, type) : "";
21251
+ const body = this.createToastBody(message, config.icon && !config.title);
21252
+ toast.innerHTML = `
21253
+ ${header}
21254
+ ${body}
21255
+ `;
21256
+ return toast;
21257
+ }
21258
+ /**
21259
+ * Create toast DOM element for View component
21260
+ */
21261
+ createViewToastElement(id, view, type, config) {
21262
+ const toast = document.createElement("div");
21263
+ toast.id = id;
21264
+ toast.className = `toast toast-service-${type}`;
21265
+ toast.setAttribute("role", "alert");
21266
+ toast.setAttribute("aria-live", "assertive");
21267
+ toast.setAttribute("aria-atomic", "true");
21268
+ const header = config.title || config.icon ? this.createToastHeader(config, type) : "";
21269
+ const body = this.createViewToastBody();
21270
+ toast.innerHTML = `
21271
+ ${header}
21272
+ ${body}
21273
+ `;
21274
+ return toast;
21275
+ }
21276
+ /**
21277
+ * Create toast body for View component
21278
+ */
21279
+ createViewToastBody() {
21280
+ return `
21281
+ <div class="toast-body p-0">
21282
+ <div class="toast-view-body p-3"></div>
21283
+ </div>
21284
+ `;
21285
+ }
21286
+ /**
21287
+ * Create toast header with title and icon
21288
+ */
21289
+ createToastHeader(config, _type) {
21290
+ const iconHtml = config.icon ? `<i class="${config.icon} toast-service-icon me-2"></i>` : "";
21291
+ const titleHtml = config.title ? `<strong class="me-auto">${iconHtml}${this.escapeHtml(config.title)}</strong>` : "";
21292
+ const timeHtml = config.showTime ? `<small class="text-muted">${this.getTimeString()}</small>` : "";
21293
+ const closeButton = config.dismissible ? `<button type="button" class="btn-close toast-service-close" data-bs-dismiss="toast" aria-label="Close"></button>` : "";
21294
+ if (!titleHtml && !timeHtml && !closeButton) {
21295
+ return "";
21296
+ }
21297
+ return `
21298
+ <div class="toast-header">
21299
+ ${titleHtml}
21300
+ ${timeHtml}
21301
+ ${closeButton}
21302
+ </div>
21303
+ `;
21304
+ }
21305
+ /**
21306
+ * Create toast body with message
21307
+ */
21308
+ createToastBody(message, showIcon = false) {
21309
+ const iconHtml = showIcon ? `<i class="${this.getDefaultIcon("info")} toast-service-icon me-2"></i>` : "";
21310
+ return `
21311
+ <div class="toast-body d-flex align-items-center">
21312
+ ${iconHtml}
21313
+ <span>${this.escapeHtml(message)}</span>
21314
+ </div>
21315
+ `;
21316
+ }
21317
+ /**
21318
+ * Get default title for toast type
21319
+ */
21320
+ getDefaultTitle(type) {
21321
+ const titles = {
21322
+ success: "Success",
21323
+ error: "Error",
21324
+ warning: "Warning",
21325
+ info: "Information",
21326
+ plain: ""
21327
+ };
21328
+ return titles[type] || "Notification";
21329
+ }
21330
+ /**
21331
+ * Get default icon for toast type
21332
+ */
21333
+ getDefaultIcon(type) {
21334
+ const icons = {
21335
+ success: "bi-check-circle-fill",
21336
+ error: "bi-exclamation-triangle-fill",
21337
+ warning: "bi-exclamation-triangle-fill",
21338
+ info: "bi-info-circle-fill",
21339
+ plain: ""
21340
+ };
21341
+ return icons[type] || "bi-info-circle-fill";
21342
+ }
21343
+ /**
21344
+ * Enforce maximum number of toasts
21345
+ */
21346
+ enforceMaxToasts() {
21347
+ const activeToasts = Array.from(this.toasts.values());
21348
+ if (activeToasts.length >= this.options.maxToasts) {
21349
+ const oldestId = this.toasts.keys().next().value;
21350
+ const oldest = this.toasts.get(oldestId);
21351
+ if (oldest) {
21352
+ oldest.bootstrap.hide();
21353
+ }
21354
+ }
21355
+ }
21356
+ /**
21357
+ * Clean up toast resources
21358
+ */
21359
+ cleanup(toastId) {
21360
+ const toast = this.toasts.get(toastId);
21361
+ if (toast) {
21362
+ try {
21363
+ toast.bootstrap.dispose();
21364
+ } catch (e) {
21365
+ console.warn("Error disposing toast:", e);
21366
+ }
21367
+ if (toast.element && toast.element.parentNode) {
21368
+ toast.element.parentNode.removeChild(toast.element);
21369
+ }
21370
+ this.toasts.delete(toastId);
21371
+ }
21372
+ }
21373
+ /**
21374
+ * Clean up view toast resources with proper view disposal
21375
+ */
21376
+ cleanupView(toastId) {
21377
+ const toast = this.toasts.get(toastId);
21378
+ if (toast) {
21379
+ if (toast.view && typeof toast.view.dispose === "function") {
21380
+ try {
21381
+ toast.view.dispose();
21382
+ } catch (e) {
21383
+ console.warn("Error disposing view in toast:", e);
21384
+ }
21385
+ }
21386
+ try {
21387
+ toast.bootstrap.dispose();
21388
+ } catch (e) {
21389
+ console.warn("Error disposing toast:", e);
21390
+ }
21391
+ if (toast.element && toast.element.parentNode) {
21392
+ toast.element.parentNode.removeChild(toast.element);
21393
+ }
21394
+ this.toasts.delete(toastId);
21395
+ }
21396
+ }
21397
+ /**
21398
+ * Hide all active toasts
21399
+ */
21400
+ hideAll() {
21401
+ this.toasts.forEach((toast, _id) => {
21402
+ toast.bootstrap.hide();
21403
+ });
21404
+ }
21405
+ /**
21406
+ * Clear all toasts immediately
21407
+ */
21408
+ clearAll() {
21409
+ this.toasts.forEach((toast, id) => {
21410
+ this.cleanup(id);
21411
+ });
21412
+ }
21413
+ /**
21414
+ * Get current time string
21415
+ */
21416
+ getTimeString() {
21417
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
21418
+ hour: "2-digit",
21419
+ minute: "2-digit"
21420
+ });
21421
+ }
21422
+ /**
21423
+ * Escape HTML to prevent XSS
21424
+ */
21425
+ escapeHtml(str) {
21426
+ const div = document.createElement("div");
21427
+ div.textContent = str;
21428
+ return div.innerHTML;
21429
+ }
21430
+ /**
21431
+ * Dispose of the entire toast service
21432
+ */
21433
+ dispose() {
21434
+ this.clearAll();
21435
+ if (this.container && this.container.parentNode) {
21436
+ this.container.parentNode.removeChild(this.container);
21437
+ }
21438
+ }
21439
+ /**
21440
+ * Get statistics about active toasts
21441
+ */
21442
+ getStats() {
21443
+ const stats = {
21444
+ total: this.toasts.size,
21445
+ byType: {}
21446
+ };
21447
+ this.toasts.forEach((toast) => {
21448
+ stats.byType[toast.type] = (stats.byType[toast.type] || 0) + 1;
21449
+ });
21450
+ return stats;
21451
+ }
21452
+ /**
21453
+ * Set global options
21454
+ */
21455
+ setOptions(newOptions) {
21456
+ this.options = { ...this.options, ...newOptions };
21457
+ if (newOptions.position) {
21458
+ if (this.container) {
21459
+ this.container.className = `toast-container position-fixed ${this.getPositionClasses()}`;
21460
+ }
21461
+ }
21462
+ }
21463
+ }
21464
+ class FileUpload {
21465
+ constructor(fileModel, options = {}) {
21466
+ this.fileModel = fileModel;
21467
+ this.options = {
21468
+ file: null,
21469
+ name: null,
21470
+ group: null,
21471
+ description: null,
21472
+ onProgress: null,
21473
+ onComplete: null,
21474
+ onError: null,
21475
+ showToast: true,
21476
+ ...options
21477
+ };
21478
+ if (!this.options.file || !(this.options.file instanceof File)) {
21479
+ throw new Error("FileUpload requires a valid File object");
21480
+ }
21481
+ this.cancelled = false;
21482
+ this.uploadRequest = null;
21483
+ this.progressToast = null;
21484
+ this.progressView = null;
21485
+ this.toastService = null;
21486
+ if (this.options.showToast) {
21487
+ this.toastService = new ToastService();
21488
+ }
21489
+ this.promise = this._startUpload();
21490
+ }
21491
+ /**
21492
+ * Main upload orchestration
21493
+ * @returns {Promise} Upload promise
21494
+ * @private
21495
+ */
21496
+ async _startUpload() {
21497
+ try {
21498
+ if (this.options.showToast) {
21499
+ this._showProgressToast();
21500
+ }
21501
+ let uploadData;
21502
+ try {
21503
+ uploadData = await this._initiateUpload();
21504
+ } catch (error) {
21505
+ throw new Error(`Failed to initiate upload: ${error.message}`);
21506
+ }
21507
+ if (this.cancelled) {
21508
+ throw new Error("Upload cancelled");
21509
+ }
21510
+ if (!uploadData || !uploadData.upload_url) {
21511
+ throw new Error("Invalid upload response: missing upload URL");
21512
+ }
21513
+ let uploadConfig;
21514
+ if (typeof uploadData.upload_url === "string") {
21515
+ uploadConfig = {
21516
+ url: uploadData.upload_url,
21517
+ method: "PUT",
21518
+ fields: null,
21519
+ headers: {}
21520
+ };
21521
+ } else if (uploadData.upload_url && typeof uploadData.upload_url === "object" && uploadData.upload_url.upload_url) {
21522
+ uploadConfig = {
21523
+ url: uploadData.upload_url.upload_url,
21524
+ method: uploadData.upload_url.method || "POST",
21525
+ fields: uploadData.upload_url.fields || null,
21526
+ headers: uploadData.upload_url.headers || {}
21527
+ };
21528
+ } else {
21529
+ throw new Error(
21530
+ `Invalid upload response: unrecognised upload_url format. Server returned: ${JSON.stringify(uploadData.upload_url)}`
21531
+ );
21532
+ }
21533
+ let result;
21534
+ try {
21535
+ result = await this._performUpload(uploadConfig);
21536
+ } catch (error) {
21537
+ throw new Error(`File upload failed: ${error.message}`);
21538
+ }
21539
+ if (this.cancelled) {
21540
+ throw new Error("Upload cancelled");
21541
+ }
21542
+ try {
21543
+ await this._completeUpload();
21544
+ } catch (error) {
21545
+ console.warn("Failed to mark upload as completed:", error);
21546
+ }
21547
+ this._onComplete(this.fileModel);
21548
+ return this.fileModel;
21549
+ } catch (error) {
21550
+ if (error.message !== "Upload cancelled") {
21551
+ this._onError(error);
21552
+ }
21553
+ throw error;
21554
+ }
21555
+ }
21556
+ /**
21557
+ * Initiate upload by calling the API to get signed URL
21558
+ * @returns {Promise<Object>} Upload initiation data
21559
+ * @private
21560
+ */
21561
+ async _initiateUpload() {
21562
+ try {
21563
+ const payload = {
21564
+ filename: this.options.name || this.options.file.name,
21565
+ file_size: this.options.file.size,
21566
+ content_type: this.options.file.type
21567
+ };
21568
+ if (this.options.group) payload.group = this.options.group;
21569
+ if (this.options.description) payload.description = this.options.description;
21570
+ const response = await this.fileModel.rest.POST("/api/fileman/upload/initiate", payload);
21571
+ if (!response) {
21572
+ throw new Error("No response from upload initiation API");
21573
+ }
21574
+ if (!response.data) {
21575
+ throw new Error("Upload initiation response missing data");
21576
+ }
21577
+ if (!response.data.status) {
21578
+ const errorMessage = response.data.error || "Upload initiation failed";
21579
+ throw new Error(errorMessage);
21580
+ }
21581
+ if (!response.data.data) {
21582
+ throw new Error("Upload initiation response missing data payload");
21583
+ }
21584
+ if (response.data.data.id) {
21585
+ this.fileModel.set("id", response.data.data.id);
21586
+ }
21587
+ return response.data.data;
21588
+ } catch (error) {
21589
+ if (error.message === "Network Error" || error.name === "TypeError") {
21590
+ throw new Error("Network error during upload initiation. Please check your connection.");
21591
+ }
21592
+ throw error;
21593
+ }
21594
+ }
21595
+ /**
21596
+ * Upload file using the normalised upload config from _startUpload.
21597
+ *
21598
+ * Two dispatch paths:
21599
+ * - PUT + raw bytes → legacy plain-string upload_url, or any config with method PUT.
21600
+ * Used by backends that issue a direct signed PUT URL (e.g. S3 signed URL).
21601
+ * - POST + FormData → config object with method POST and optional fields dict.
21602
+ * Used by the local filesystem backend and S3 presigned POST.
21603
+ * fields are appended first, then the file as the "file" key,
21604
+ * matching Django's request.FILES['file'] convention.
21605
+ * Content-Type is intentionally NOT set manually so the browser
21606
+ * can write the correct multipart boundary.
21607
+ *
21608
+ * @param {{ url: string, method: string, fields: object|null, headers: object }} uploadConfig
21609
+ * @returns {Promise} Upload result
21610
+ * @private
21611
+ */
21612
+ async _performUpload(uploadConfig) {
21613
+ return new Promise((resolve, reject) => {
21614
+ if (!(this.options.file instanceof File)) {
21615
+ reject(new Error("Only single File objects are supported"));
21616
+ return;
21617
+ }
21618
+ const { url, method, fields, headers } = uploadConfig;
21619
+ const useFormData = method === "POST" && fields !== null;
21620
+ const xhr = new XMLHttpRequest();
21621
+ this.uploadRequest = xhr;
21622
+ xhr.upload.onprogress = (event) => {
21623
+ if (this.cancelled) return;
21624
+ this._onProgress({
21625
+ progress: event.loaded / event.total,
21626
+ loaded: event.loaded,
21627
+ total: event.total,
21628
+ percentage: Math.round(event.loaded / event.total * 100)
21629
+ });
21630
+ };
21631
+ xhr.onload = () => {
21632
+ if (xhr.status >= 200 && xhr.status < 300) {
21633
+ resolve({ data: xhr.response, status: xhr.status, statusText: xhr.statusText, xhr });
21634
+ } else {
21635
+ reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
21636
+ }
21637
+ };
21638
+ xhr.onerror = () => reject(new Error("Upload failed: Network error"));
21639
+ xhr.ontimeout = () => reject(new Error("Upload timed out — file may be too large or connection too slow"));
21640
+ xhr.onabort = () => reject(new Error("Upload cancelled"));
21641
+ let apiUrl = url;
21642
+ if (url.startsWith("/") && !url.startsWith("/api/")) {
21643
+ apiUrl = "/api" + url;
21644
+ }
21645
+ const resolvedUrl = this.fileModel.rest.buildUrl(apiUrl);
21646
+ xhr.open(method, resolvedUrl);
21647
+ xhr.timeout = 3e4;
21648
+ if (useFormData) {
21649
+ for (const [key, value] of Object.entries(headers || {})) {
21650
+ if (key.toLowerCase() !== "content-type") {
21651
+ xhr.setRequestHeader(key, value);
21652
+ }
21653
+ }
21654
+ const formData = new FormData();
21655
+ for (const [key, value] of Object.entries(fields)) {
21656
+ formData.append(key, value);
21657
+ }
21658
+ formData.append("file", this.options.file);
21659
+ xhr.send(formData);
21660
+ } else {
21661
+ xhr.setRequestHeader("Content-Type", this.options.file.type);
21662
+ for (const [key, value] of Object.entries(headers || {})) {
21663
+ if (key.toLowerCase() !== "content-type") {
21664
+ xhr.setRequestHeader(key, value);
21665
+ }
21666
+ }
21667
+ xhr.send(this.options.file);
21668
+ }
21669
+ });
21670
+ }
21671
+ /**
21672
+ * Mark upload as completed in the API
21673
+ * @returns {Promise} Completion result
21674
+ * @private
21675
+ */
21676
+ async _completeUpload() {
21677
+ try {
21678
+ const response = await this.fileModel.save({ action: "mark_as_completed" });
21679
+ if (!response) {
21680
+ throw new Error("No response from upload completion API");
21681
+ }
21682
+ if (response.data && !response.data.status) {
21683
+ const errorMessage = response.data.error || "Failed to mark upload as completed";
21684
+ throw new Error(errorMessage);
21685
+ }
21686
+ return response;
21687
+ } catch (error) {
21688
+ if (error.message === "Network Error" || error.name === "TypeError") {
21689
+ throw new Error("Network error during upload completion. The file may have uploaded successfully.");
21690
+ }
21691
+ throw error;
21692
+ }
21693
+ }
21694
+ /**
21695
+ * Handle progress updates
21696
+ * @param {Object} progressInfo - Progress information
21697
+ * @private
21698
+ */
21699
+ _onProgress(progressInfo) {
21700
+ if (this.progressToast && this.progressToast.updateProgress) {
21701
+ this.progressToast.updateProgress(progressInfo);
21702
+ }
21703
+ if (typeof this.options.onProgress === "function") {
21704
+ this.options.onProgress(progressInfo);
21705
+ }
21706
+ }
21707
+ /**
21708
+ * Handle successful upload completion
21709
+ * @param {Object} result - Upload result
21710
+ * @private
21711
+ */
21712
+ _onComplete(result) {
21713
+ if (this.progressView) {
21714
+ this.progressView.markCompleted("Upload completed successfully!");
21715
+ }
21716
+ if (this.progressToast) {
21717
+ setTimeout(() => {
21718
+ try {
21719
+ if (this.progressToast && typeof this.progressToast.hide === "function") {
21720
+ this.progressToast.hide();
21721
+ }
21722
+ } catch (error) {
21723
+ console.warn("Error hiding progress toast:", error);
21724
+ }
21725
+ }, 2e3);
21726
+ }
21727
+ if (typeof this.options.onComplete === "function") {
21728
+ this.options.onComplete(result);
21729
+ }
21730
+ }
21731
+ /**
21732
+ * Handle upload errors
21733
+ * @param {Error} error - Error object
21734
+ * @private
21735
+ */
21736
+ _onError(error) {
21737
+ if (this.progressToast) {
21738
+ try {
21739
+ this.progressToast.hide();
21740
+ } catch (error2) {
21741
+ console.warn("Error hiding progress toast on error:", error2);
21742
+ }
21743
+ }
21744
+ if (this.toastService) {
21745
+ this.toastService.error(`Upload failed: ${error.message}`);
21746
+ }
21747
+ if (typeof this.options.onError === "function") {
21748
+ this.options.onError(error);
21749
+ }
21750
+ }
21751
+ /**
21752
+ * Show progress toast with ProgressView component
21753
+ * @private
21754
+ */
21755
+ _showProgressToast() {
21756
+ this.progressView = new ProgressView({
21757
+ filename: this.options.name || this.options.file.name,
21758
+ filesize: this.options.file.size,
21759
+ showCancel: true,
21760
+ onCancel: () => this.cancel()
21761
+ });
21762
+ this.progressToast = this.toastService.showView(this.progressView, "info", {
21763
+ title: "File Upload",
21764
+ autohide: false,
21765
+ dismissible: false
21766
+ });
21767
+ }
21768
+ /**
21769
+ * Cancel the upload
21770
+ * @returns {boolean} True if cancelled, false if already completed
21771
+ */
21772
+ cancel() {
21773
+ if (this.cancelled) {
21774
+ return false;
21775
+ }
21776
+ this.cancelled = true;
21777
+ if (this.uploadRequest && typeof this.uploadRequest.abort === "function") {
21778
+ this.uploadRequest.abort();
21779
+ }
21780
+ if (this.progressView) {
21781
+ this.progressView.markCancelled();
21782
+ }
21783
+ if (this.progressToast) {
21784
+ setTimeout(() => {
21785
+ try {
21786
+ if (this.progressToast && typeof this.progressToast.hide === "function") {
21787
+ this.progressToast.hide();
21788
+ }
21789
+ } catch (error) {
21790
+ console.warn("Error hiding progress toast on cancel:", error);
21791
+ }
21792
+ }, 1500);
21793
+ }
21794
+ return true;
21795
+ }
21796
+ /**
21797
+ * Check if upload is cancelled
21798
+ * @returns {boolean} True if cancelled
21799
+ */
21800
+ isCancelled() {
21801
+ return this.cancelled;
21802
+ }
21803
+ /**
21804
+ * Promise interface - then
21805
+ * @param {function} onSuccess - Success handler
21806
+ * @param {function} onError - Error handler
21807
+ * @returns {Promise} Promise chain
21808
+ */
21809
+ then(onSuccess, onError) {
21810
+ return this.promise.then(onSuccess, onError);
21811
+ }
21812
+ /**
21813
+ * Promise interface - catch
21814
+ * @param {function} onError - Error handler
21815
+ * @returns {Promise} Promise chain
21816
+ */
21817
+ catch(onError) {
21818
+ return this.promise.catch(onError);
21819
+ }
21820
+ /**
21821
+ * Promise interface - finally
21822
+ * @param {function} onFinally - Finally handler
21823
+ * @returns {Promise} Promise chain
21824
+ */
21825
+ finally(onFinally) {
21826
+ return this.promise.finally(onFinally);
21827
+ }
21828
+ /**
21829
+ * Get upload statistics
21830
+ * @returns {Object} Upload stats
21831
+ */
21832
+ getStats() {
21833
+ return {
21834
+ filename: this.options.file.name,
21835
+ size: this.options.file.size,
21836
+ type: this.options.file.type,
21837
+ cancelled: this.cancelled,
21838
+ group: this.options.group,
21839
+ description: this.options.description
21840
+ };
21841
+ }
21842
+ }
21843
+ class Group extends Model {
21844
+ constructor(data = {}) {
21845
+ super(data, {
21846
+ endpoint: "/api/group"
21847
+ });
21848
+ }
21849
+ }
21850
+ class GroupList extends Collection {
21851
+ constructor(options = {}) {
21852
+ super({
21853
+ ModelClass: Group,
21854
+ endpoint: "/api/group",
21855
+ size: 10,
21856
+ ...options
21857
+ });
21858
+ }
21859
+ }
21860
+ const GroupKinds = {
21861
+ "org": "Organization",
21862
+ "division": "Division",
21863
+ "department": "Department",
21864
+ "team": "Team",
21865
+ "merchant": "Merchant",
21866
+ "partner": "Partner",
21867
+ "client": "Client",
21868
+ "iso": "ISO",
21869
+ "sales": "Sales",
21870
+ "reseller": "Reseller",
21871
+ "location": "Location",
21872
+ "region": "Region",
21873
+ "route": "Route",
21874
+ "project": "Project",
21875
+ "inventory": "Inventory",
21876
+ "test": "Testing",
21877
+ "misc": "Miscellaneous",
21878
+ "qa": "Quality Assurance"
21879
+ };
21880
+ const GroupKindOptions = Object.entries(GroupKinds).map(([key, label]) => ({
21881
+ value: key,
21882
+ label
21883
+ }));
21884
+ const GroupForms = {
21885
+ create: {
21886
+ title: "Create Group",
21887
+ fields: [
21888
+ {
21889
+ name: "name",
21890
+ type: "text",
21891
+ label: "Group Name",
21892
+ required: true,
21893
+ placeholder: "Enter group name"
21894
+ },
21895
+ {
21896
+ name: "kind",
21897
+ type: "select",
21898
+ label: "Group Kind",
21899
+ required: true,
21900
+ options: GroupKindOptions
21901
+ },
21902
+ {
21903
+ type: "collection",
21904
+ name: "parent",
21905
+ label: "Parent Group",
21906
+ Collection: GroupList,
21907
+ // Collection class
21908
+ labelField: "name",
21909
+ // Field to display in dropdown
21910
+ valueField: "id",
21911
+ // Field to use as value
21912
+ maxItems: 10,
21913
+ // Max items to show in dropdown
21914
+ placeholder: "Search groups...",
21915
+ emptyFetch: false,
21916
+ debounceMs: 300
21917
+ // Search debounce delay
21918
+ }
21919
+ ]
21920
+ },
21921
+ edit: {
21922
+ title: "Edit Group",
21923
+ fields: [
21924
+ {
21925
+ name: "name",
21926
+ type: "text",
21927
+ label: "Group Name",
21928
+ required: true,
21929
+ placeholder: "Enter group name"
21930
+ },
21931
+ {
21932
+ name: "kind",
21933
+ type: "select",
21934
+ label: "Group Kind",
21935
+ required: true,
21936
+ options: GroupKindOptions
21937
+ },
21938
+ {
21939
+ type: "collection",
21940
+ name: "parent",
21941
+ label: "Parent Group",
21942
+ Collection: GroupList,
21943
+ // Collection class
21944
+ labelField: "name",
21945
+ // Field to display in dropdown
21946
+ valueField: "id",
21947
+ // Field to use as value
21948
+ maxItems: 10,
21949
+ // Max items to show in dropdown
21950
+ placeholder: "Search groups...",
21951
+ emptyFetch: false,
21952
+ debounceMs: 300
21953
+ // Search debounce delay
21954
+ },
21955
+ {
21956
+ name: "metadata.domain",
21957
+ type: "text",
21958
+ label: "Default Domain",
21959
+ placeholder: "Enter Domain"
21960
+ },
21961
+ {
21962
+ name: "metadata.portal",
21963
+ type: "text",
21964
+ label: "Default Portal",
21965
+ placeholder: "Enter Portal URL"
21966
+ },
21967
+ {
21968
+ name: "is_active",
21969
+ type: "switch",
21970
+ label: "Is Active",
21971
+ cols: 4
21972
+ }
21973
+ ]
21974
+ }
21975
+ };
21976
+ Group.EDIT_FORM = GroupForms.edit;
21977
+ Group.ADD_FORM = GroupForms.create;
21978
+ Group.CREATE_FORM = GroupForms.create;
21979
+ Group.GroupKindOptions = GroupKindOptions;
21980
+ Group.GroupKinds = GroupKinds;
21981
+ class User extends Model {
21982
+ constructor(data = {}) {
21983
+ super(data, {
21984
+ endpoint: "/api/user"
21985
+ });
21986
+ }
21987
+ hasPermission(permission) {
21988
+ if (this.get("is_superuser")) return true;
21989
+ if (Array.isArray(permission)) {
21990
+ return permission.some((p) => this.hasPermission(p));
21991
+ }
21992
+ const isSysPermission = permission.startsWith("sys.");
21993
+ const permissionToCheck = isSysPermission ? permission.substring(4) : permission;
21994
+ if (this._hasPermission(permissionToCheck)) {
21995
+ return true;
21996
+ }
21997
+ if (!isSysPermission && this.member && this.member.hasPermission(permission)) {
21998
+ return true;
21999
+ }
22000
+ return false;
22001
+ }
22002
+ _hasPermission(permission) {
22003
+ const permissions = this.get("permissions");
22004
+ if (!permissions) {
22005
+ return false;
22006
+ }
22007
+ return permissions[permission] == true;
22008
+ }
22009
+ hasPerm(p) {
22010
+ return this.hasPermission(p);
22011
+ }
22012
+ }
22013
+ User.PERMISSIONS = [
22014
+ { name: "manage_users", label: "Manage Users" },
22015
+ { name: "view_users", label: "View Users" },
22016
+ { name: "view_groups", label: "View Groups" },
22017
+ { name: "manage_groups", label: "Manage Groups" },
22018
+ { name: "view_metrics", label: "View System Metrics" },
22019
+ { name: "manage_metrics", label: "Manage System Metrics" },
22020
+ { name: "view_logs", label: "View Logs" },
22021
+ { name: "view_incidents", label: "View Incidents" },
22022
+ { name: "manage_incidents", label: "Manage Incidents" },
22023
+ { name: "view_tickets", label: "View Tickets" },
22024
+ { name: "manage_tickets", label: "Manage Tickets" },
22025
+ { name: "view_admin", label: "View Admin" },
22026
+ { name: "view_jobs", label: "View Jobs" },
22027
+ { name: "manage_jobs", label: "Manage Jobs" },
22028
+ { name: "view_global", label: "View Global" },
22029
+ { name: "manage_notifications", label: "Manage Notifications" },
22030
+ { name: "manage_files", label: "Manage Files" },
22031
+ { name: "force_single_session", label: "Force Single Session" },
22032
+ { name: "file_vault", label: "Access File Vault" },
22033
+ { name: "manage_aws", label: "Manage AWS" },
22034
+ { name: "manage_docit", label: "Manage DocIt" }
22035
+ ];
22036
+ User.PERMISSION_FIELDS = [
22037
+ ...User.PERMISSIONS.map((permission) => ({
22038
+ name: `permissions.${permission.name}`,
22039
+ type: "switch",
22040
+ label: permission.label,
22041
+ columns: 4
22042
+ }))
22043
+ ];
22044
+ const UserForms = {
22045
+ create: {
22046
+ title: "Create User",
22047
+ fields: [
22048
+ { name: "email", type: "text", label: "Email", required: true },
22049
+ { name: "phone_number", type: "text", label: "Phone number", columns: 12 },
22050
+ { name: "display_name", type: "text", label: "Display Name" }
22051
+ ]
22052
+ },
22053
+ edit: {
22054
+ title: "Edit User",
22055
+ fields: [
22056
+ { name: "email", type: "email", label: "Email", columns: 12 },
22057
+ { name: "display_name", type: "text", label: "Display Name", columns: 12 },
22058
+ { name: "phone_number", type: "text", label: "Phone number", columns: 12 },
22059
+ { type: "collection", name: "org", label: "Organization", Collection: GroupList, labelField: "name", valueField: "id", columns: 12 }
22060
+ ]
22061
+ },
22062
+ permissions: {
22063
+ fields: User.PERMISSION_FIELDS
22064
+ }
22065
+ };
22066
+ const UserDataView = {
22067
+ // Comprehensive view with all data
22068
+ detailed: {
22069
+ title: "Detailed User Information",
22070
+ columns: 2,
22071
+ showEmptyValues: true,
22072
+ emptyValueText: "Not set",
22073
+ fields: [
22074
+ // Basic Info Section
22075
+ {
22076
+ name: "id",
22077
+ label: "User ID",
22078
+ type: "number",
22079
+ colSize: 3
22080
+ },
22081
+ {
22082
+ name: "display_name",
22083
+ label: "Display Name",
22084
+ type: "text",
22085
+ format: 'capitalize|default("Unnamed User")',
22086
+ colSize: 9
22087
+ },
22088
+ {
22089
+ name: "username",
22090
+ label: "Username",
22091
+ type: "text",
22092
+ format: "lowercase",
22093
+ colSize: 6
22094
+ },
22095
+ {
22096
+ name: "email",
22097
+ label: "Email Address",
22098
+ type: "email",
22099
+ colSize: 6
22100
+ },
22101
+ {
22102
+ name: "phone_number",
22103
+ label: "Phone Number",
22104
+ type: "phone",
22105
+ format: 'phone|default("Not provided")',
22106
+ colSize: 6
22107
+ },
22108
+ {
22109
+ name: "is_active",
22110
+ label: "Account Status",
22111
+ type: "boolean",
22112
+ colSize: 6
22113
+ },
22114
+ // Activity Info
22115
+ {
22116
+ name: "last_login",
22117
+ label: "Last Login",
22118
+ type: "datetime",
22119
+ format: "relative",
22120
+ colSize: 6
22121
+ },
22122
+ {
22123
+ name: "last_activity",
22124
+ label: "Last Activity",
22125
+ type: "datetime",
22126
+ format: "relative",
22127
+ colSize: 6
22128
+ },
22129
+ // Avatar Info
22130
+ {
22131
+ name: "avatar.url",
22132
+ label: "Avatar",
22133
+ type: "url",
22134
+ colSize: 12
22135
+ },
22136
+ // Complex Data (will use full width automatically)
22137
+ {
22138
+ name: "permissions",
22139
+ label: "User Permissions",
22140
+ type: "dataview",
22141
+ dataViewColumns: 2,
22142
+ showEmptyValues: false
22143
+ },
22144
+ {
22145
+ name: "metadata",
22146
+ label: "User Metadata",
22147
+ type: "dataview",
22148
+ dataViewColumns: 1
22149
+ },
22150
+ {
22151
+ name: "avatar",
22152
+ label: "Avatar Details",
22153
+ type: "dataview",
22154
+ dataViewColumns: 1
22155
+ }
22156
+ ]
22157
+ }
22158
+ };
22159
+ User.DATA_VIEW = UserDataView.detailed;
22160
+ User.EDIT_FORM = UserForms.edit;
22161
+ User.ADD_FORM = UserForms.create;
22162
+ let File$1 = class File extends Model {
22163
+ constructor(data = {}) {
22164
+ super(data, {
22165
+ endpoint: "/api/fileman/file"
22166
+ });
22167
+ }
22168
+ isImage() {
22169
+ return this.get("category") === "image";
22170
+ }
22171
+ /**
22172
+ * Upload file with progress tracking and UI integration
22173
+ * Returns a FileUpload instance with promise interface and cancellation support
22174
+ *
22175
+ * @param {object} options - Upload configuration
22176
+ * @param {File} options.file - File object to upload
22177
+ * @param {string} options.name - Custom filename (optional)
22178
+ * @param {string} options.group - File group/category (optional)
22179
+ * @param {string} options.description - File description (optional)
22180
+ * @param {function} options.onProgress - Progress callback ({ progress, loaded, total, percentage })
22181
+ * @param {function} options.onComplete - Success callback
22182
+ * @param {function} options.onError - Error callback
22183
+ * @param {boolean} options.showToast - Show progress toast (default: true)
22184
+ * @returns {FileUpload} Upload instance with promise interface
22185
+ */
22186
+ upload(options = {}) {
22187
+ return new FileUpload(this, options);
22188
+ }
22189
+ };
22190
+ const Files = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
22191
+ __proto__: null,
22192
+ File: File$1
22193
+ }, Symbol.toStringTag, { value: "Module" }));
20971
22194
  class DataView extends View {
20972
22195
  constructor(options = {}) {
20973
22196
  const {