web-mojo 2.2.65 → 2.2.67

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