web-mojo 2.2.68 → 2.2.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -9
- package/dist/admin.cjs.js +1 -1
- package/dist/admin.cjs.js.map +1 -1
- package/dist/admin.es.js +1 -1
- package/dist/admin.es.js.map +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.es.js +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -1
- package/dist/chunks/ChatView-CZ3Key2k.js +2 -0
- package/dist/chunks/ChatView-CZ3Key2k.js.map +1 -0
- package/dist/chunks/ChatView-Dw-iVmht.js +2 -0
- package/dist/chunks/ChatView-Dw-iVmht.js.map +1 -0
- package/dist/chunks/{Dialog-DW7PHzUc.js → Dialog-Dhqtd9Yz.js} +2 -2
- package/dist/chunks/{Dialog-DW7PHzUc.js.map → Dialog-Dhqtd9Yz.js.map} +1 -1
- package/dist/chunks/{Dialog-jfBsXy5X.js → Dialog-t_9l2Mou.js} +2 -2
- package/dist/chunks/{Dialog-jfBsXy5X.js.map → Dialog-t_9l2Mou.js.map} +1 -1
- package/dist/chunks/Files-6eRT5k3r.js +2 -0
- package/dist/chunks/{Files-C-ChBvr5.js.map → Files-6eRT5k3r.js.map} +1 -1
- package/dist/chunks/Files-Dh_5PFBn.js +2 -0
- package/dist/chunks/{Files-DNbHDy43.js.map → Files-Dh_5PFBn.js.map} +1 -1
- package/dist/chunks/{FormView-EoB_ZdIB.js → FormView-B1CXO2t8.js} +2 -2
- package/dist/chunks/{FormView-EoB_ZdIB.js.map → FormView-B1CXO2t8.js.map} +1 -1
- package/dist/chunks/{FormView-Q_lFA0nr.js → FormView-BRHAIawp.js} +2 -2
- package/dist/chunks/{FormView-Q_lFA0nr.js.map → FormView-BRHAIawp.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-lzq4lSTF.js → MetricsMiniChartWidget-D1w608Jy.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-lzq4lSTF.js.map → MetricsMiniChartWidget-D1w608Jy.js.map} +1 -1
- package/dist/chunks/{MetricsMiniChartWidget-ukn-NRMR.js → MetricsMiniChartWidget-Dg1e6EQJ.js} +2 -2
- package/dist/chunks/{MetricsMiniChartWidget-ukn-NRMR.js.map → MetricsMiniChartWidget-Dg1e6EQJ.js.map} +1 -1
- package/dist/chunks/{PDFViewer-iOqYpg-6.js → PDFViewer-CDeV9OBs.js} +2 -2
- package/dist/chunks/{PDFViewer-iOqYpg-6.js.map → PDFViewer-CDeV9OBs.js.map} +1 -1
- package/dist/chunks/{PDFViewer-sFoyopz3.js → PDFViewer-D_3V8QJe.js} +2 -2
- package/dist/chunks/{PDFViewer-sFoyopz3.js.map → PDFViewer-D_3V8QJe.js.map} +1 -1
- package/dist/chunks/TableView-CI_7a-kD.js +2 -0
- package/dist/chunks/TableView-CI_7a-kD.js.map +1 -0
- package/dist/chunks/TableView-CWk5k4LQ.js +2 -0
- package/dist/chunks/TableView-CWk5k4LQ.js.map +1 -0
- package/dist/chunks/ToastService-C2tTooFn.js +3 -0
- package/dist/chunks/ToastService-C2tTooFn.js.map +1 -0
- package/dist/chunks/ToastService-nUaGVpSl.js +3 -0
- package/dist/chunks/ToastService-nUaGVpSl.js.map +1 -0
- package/dist/chunks/{TokenManager-ChNOca0K.js → TokenManager-ien2XzwO.js} +2 -2
- package/dist/chunks/{TokenManager-ChNOca0K.js.map → TokenManager-ien2XzwO.js.map} +1 -1
- package/dist/chunks/{TokenManager-DKzxBt6g.js → TokenManager-sZgt--C9.js} +2 -2
- package/dist/chunks/{TokenManager-DKzxBt6g.js.map → TokenManager-sZgt--C9.js.map} +1 -1
- package/dist/chunks/User-BL9M_PWB.js +2 -0
- package/dist/chunks/User-BL9M_PWB.js.map +1 -0
- package/dist/chunks/{User-BnlvMG5J.js → User-DqHG5Gr1.js} +2 -3
- package/dist/chunks/User-DqHG5Gr1.js.map +1 -0
- package/dist/chunks/UserProfileView-DnVMHcLH.js +2 -0
- package/dist/chunks/UserProfileView-DnVMHcLH.js.map +1 -0
- package/dist/chunks/UserProfileView-kupeq2rN.js +2 -0
- package/dist/chunks/UserProfileView-kupeq2rN.js.map +1 -0
- package/dist/chunks/{WebApp-Bsic6FPo.js → WebApp-Bti0Gqqo.js} +2 -2
- package/dist/chunks/{WebApp-Bsic6FPo.js.map → WebApp-Bti0Gqqo.js.map} +1 -1
- package/dist/chunks/{WebApp-B0m6JCjO.js → WebApp-CcVF73yg.js} +2 -2
- package/dist/chunks/{WebApp-B0m6JCjO.js.map → WebApp-CcVF73yg.js.map} +1 -1
- package/dist/chunks/index-Aq9ke4vg.js +2 -0
- package/dist/chunks/index-Aq9ke4vg.js.map +1 -0
- package/dist/chunks/index-Da9sT-tE.js +2 -0
- package/dist/chunks/index-Da9sT-tE.js.map +1 -0
- package/dist/chunks/{version-BmVUtM_7.js → version-D8JjsPW0.js} +2 -2
- package/dist/chunks/{version-BmVUtM_7.js.map → version-D8JjsPW0.js.map} +1 -1
- package/dist/chunks/{version-i7K_82Qy.js → version-XmirKYWA.js} +2 -2
- package/dist/chunks/{version-i7K_82Qy.js.map → version-XmirKYWA.js.map} +1 -1
- package/dist/css/web-mojo.css +1 -1
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.cjs.js.map +1 -1
- package/dist/docit.es.js +1 -1
- package/dist/docit.es.js.map +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1 -1
- package/dist/index.es.js.map +1 -1
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.es.js +1 -1
- package/dist/map.cjs.js +1 -1
- package/dist/map.es.js +1 -1
- package/dist/user-profile.cjs.js +2 -0
- package/dist/user-profile.cjs.js.map +1 -0
- package/dist/user-profile.es.js +2 -0
- package/dist/user-profile.es.js.map +1 -0
- package/dist/web-mojo.lite.iife.js +9 -6
- package/dist/web-mojo.lite.iife.js.map +1 -1
- package/dist/web-mojo.lite.iife.min.js +11 -11
- package/dist/web-mojo.lite.iife.min.js.map +1 -1
- package/package.json +5 -1
- package/dist/chunks/ChatView-Cfe0ZGvr.js +0 -2
- package/dist/chunks/ChatView-Cfe0ZGvr.js.map +0 -1
- package/dist/chunks/ChatView-DuQVFrCY.js +0 -2
- package/dist/chunks/ChatView-DuQVFrCY.js.map +0 -1
- package/dist/chunks/Files-C-ChBvr5.js +0 -2
- package/dist/chunks/Files-DNbHDy43.js +0 -2
- package/dist/chunks/User-BnlvMG5J.js.map +0 -1
- package/dist/chunks/User-DSqcOwPL.js +0 -3
- package/dist/chunks/User-DSqcOwPL.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Files-C-ChBvr5.js","sources":["../../src/core/views/feedback/ProgressView.js","../../src/core/services/FileUpload.js","../../src/core/models/Files.js"],"sourcesContent":["/**\n * ProgressView - File upload progress component\n * \n * Shows upload progress with progress bar, filename, and cancellation option\n * Integrates with FileUpload service for real-time progress updates\n * \n * Features:\n * - Bootstrap progress bar with percentage\n * - File information (name, size)\n * - Bytes uploaded/total display\n * - Cancel button with confirmation\n * - Responsive design\n * \n * Events:\n * - 'cancel' - Emitted when user cancels upload\n * \n * @example\n * const progressView = new ProgressView({\n * filename: 'document.pdf',\n * filesize: 1024000,\n * onCancel: () => fileUpload.cancel()\n * });\n * \n * // Update progress\n * progressView.updateProgress({ progress: 0.5, loaded: 512000, total: 1024000 });\n */\n\nimport View from '@core/View.js';\nimport dataFormatter from '@core/utils/DataFormatter.js';\n\nclass ProgressView extends View {\n constructor(options = {}) {\n super({\n template: 'progress-view-template',\n ...options\n });\n\n // Initialize progress data\n this.filename = options.filename || 'Unknown file';\n this.filesize = options.filesize || 0;\n this.filesizeFormatted = dataFormatter.pipe(this.filesize, 'filesize');\n \n // Progress state\n this.progress = 0;\n this.percentage = 0;\n this.loaded = 0;\n this.total = this.filesize;\n this.loadedFormatted = '0 B';\n this.totalFormatted = this.filesizeFormatted;\n this.status = 'Starting upload...';\n \n // Options\n this.showCancel = options.showCancel !== false;\n this.onCancel = options.onCancel || null;\n \n // State\n this.cancelled = false;\n this.completed = false;\n }\n\n /**\n * Get template for the progress view\n */\n getTemplate() {\n return `\n <div class=\"progress-view\">\n <div class=\"d-flex justify-content-between align-items-start mb-2\">\n <div class=\"flex-grow-1 min-width-0\">\n <div class=\"fw-medium text-truncate\" title=\"{{filename}}\">\n <i class=\"bi bi-file-earmark me-1\"></i>\n {{filename}}\n </div>\n <small class=\"text-muted\">{{status}}</small>\n </div>\n {{#showCancel}}\n <button type=\"button\" \n class=\"btn btn-sm btn-outline-secondary ms-2\" \n data-action=\"cancel\"\n {{#cancelled}}disabled{{/cancelled}}>\n <i class=\"bi bi-x\"></i>\n </button>\n {{/showCancel}}\n </div>\n \n <div class=\"progress mb-2\" style=\"height: 8px;\">\n <div class=\"progress-bar\" \n role=\"progressbar\" \n style=\"width: {{percentage}}%\"\n aria-valuenow=\"{{percentage}}\" \n aria-valuemin=\"0\" \n aria-valuemax=\"100\">\n </div>\n </div>\n \n <div class=\"d-flex justify-content-between\">\n <small class=\"text-muted\">\n {{loadedFormatted}} / {{totalFormatted}}\n </small>\n <small class=\"text-muted\">\n {{percentage}}%\n </small>\n </div>\n </div>\n `;\n }\n\n /**\n * Update progress information\n * @param {Object} progressInfo - Progress data\n * @param {number} progressInfo.progress - Progress as decimal (0-1)\n * @param {number} progressInfo.loaded - Bytes loaded\n * @param {number} progressInfo.total - Total bytes\n * @param {number} progressInfo.percentage - Progress as percentage (0-100)\n */\n updateProgress(progressInfo) {\n if (this.cancelled || this.completed) {\n return;\n }\n\n this.progress = progressInfo.progress;\n this.percentage = progressInfo.percentage;\n this.loaded = progressInfo.loaded;\n this.total = progressInfo.total || this.filesize;\n \n // Format bytes for display\n this.loadedFormatted = dataFormatter.pipe(this.loaded, 'filesize');\n this.totalFormatted = dataFormatter.pipe(this.total, 'filesize');\n \n // Update status message\n if (this.percentage < 100) {\n this.status = `Uploading... ${this.percentage}%`;\n } else {\n this.status = 'Finalizing upload...';\n }\n\n // Re-render to show updated progress\n this.render();\n }\n\n /**\n * Mark upload as completed\n * @param {string} message - Success message\n */\n markCompleted(message = 'Upload completed!') {\n this.completed = true;\n this.progress = 1;\n this.percentage = 100;\n this.status = message;\n this.render();\n }\n\n /**\n * Mark upload as failed\n * @param {string} message - Error message\n */\n markFailed(message = 'Upload failed') {\n this.status = message;\n this.render();\n }\n\n /**\n * Mark upload as cancelled\n */\n markCancelled() {\n this.cancelled = true;\n this.status = 'Upload cancelled';\n this.render();\n }\n\n /**\n * Handle cancel button click\n * @param {string} action - Action name\n * @param {Event} event - Click event\n * @param {Element} element - Button element\n */\n async onActionCancel(action, event, element) {\n if (this.cancelled || this.completed) {\n return;\n }\n\n // Disable button immediately\n element.disabled = true;\n \n // Mark as cancelled\n this.markCancelled();\n \n // Emit cancel event\n this.emit('cancel');\n \n // Call cancel callback if provided\n if (typeof this.onCancel === 'function') {\n try {\n await this.onCancel();\n } catch (error) {\n console.error('Error in cancel callback:', error);\n }\n }\n }\n\n /**\n * Set filename\n * @param {string} filename - New filename\n */\n setFilename(filename) {\n this.filename = filename;\n this.render();\n }\n\n /**\n * Set file size\n * @param {number} size - File size in bytes\n */\n setFilesize(size) {\n this.filesize = size;\n this.filesizeFormatted = dataFormatter.pipe(size, 'filesize');\n this.total = size;\n this.totalFormatted = this.filesizeFormatted;\n this.render();\n }\n\n /**\n * Get current progress as percentage\n * @returns {number} Progress percentage (0-100)\n */\n getPercentage() {\n return this.percentage;\n }\n\n /**\n * Check if upload is completed\n * @returns {boolean} True if completed\n */\n isCompleted() {\n return this.completed;\n }\n\n /**\n * Check if upload is cancelled\n * @returns {boolean} True if cancelled\n */\n isCancelled() {\n return this.cancelled;\n }\n\n /**\n * Get upload statistics\n * @returns {Object} Upload stats\n */\n getStats() {\n return {\n filename: this.filename,\n filesize: this.filesize,\n progress: this.progress,\n percentage: this.percentage,\n loaded: this.loaded,\n total: this.total,\n cancelled: this.cancelled,\n completed: this.completed,\n status: this.status\n };\n }\n}\n\nexport default ProgressView;","/**\n * FileUpload - File upload service with progress tracking and UI integration\n *\n * Features:\n * - Auto-start upload process\n * - Progress tracking with detailed information\n * - Promise interface with cancellation support\n * - Toast integration for user feedback\n * - Three-stage upload process (initiate → upload → complete)\n *\n * @example\n * const file = new File();\n * const upload = file.upload({\n * file: fileObject,\n * name: 'avatar.jpg',\n * group: 'profile-pics',\n * description: 'User avatar',\n * onProgress: ({ progress, loaded, total, percentage }) => {\n * console.log(`${percentage}% complete`);\n * }\n * });\n *\n * upload.then(result => console.log('Success!'))\n * .catch(error => console.error('Failed:', error));\n */\n\nimport ToastService from './ToastService.js';\nimport ProgressView from '../views/feedback/ProgressView.js';\n\nclass FileUpload {\n constructor(fileModel, options = {}) {\n this.fileModel = fileModel;\n this.options = {\n file: null,\n name: null,\n group: null,\n description: null,\n onProgress: null,\n onComplete: null,\n onError: null,\n showToast: true,\n ...options\n };\n\n // Validation\n if (!this.options.file || !(this.options.file instanceof File)) {\n throw new Error('FileUpload requires a valid File object');\n }\n\n // State management\n this.cancelled = false;\n this.uploadRequest = null;\n this.progressToast = null;\n this.progressView = null;\n this.toastService = null;\n\n // Initialize toast service if needed\n if (this.options.showToast) {\n this.toastService = new ToastService();\n }\n\n // Auto-start upload (Option 3 behavior)\n this.promise = this._startUpload();\n }\n\n /**\n * Main upload orchestration\n * @returns {Promise} Upload promise\n * @private\n */\n async _startUpload() {\n try {\n if (this.options.showToast) {\n this._showProgressToast();\n }\n\n // Stage 1: Initiate upload and get signed URL\n let uploadData;\n try {\n uploadData = await this._initiateUpload();\n } catch (error) {\n throw new Error(`Failed to initiate upload: ${error.message}`);\n }\n\n if (this.cancelled) {\n throw new Error('Upload cancelled');\n }\n\n // Validate upload data\n if (!uploadData || !uploadData.upload_url) {\n throw new Error('Invalid upload response: missing upload URL');\n }\n\n // Normalise upload_url — the backend can return either:\n // • a plain string → direct PUT with raw bytes (e.g. S3 signed URL)\n // • a config object → POST multipart/form-data (e.g. filesystem backend)\n // { upload_url: string, method: string, fields: object, headers: object }\n let uploadConfig;\n if (typeof uploadData.upload_url === 'string') {\n uploadConfig = {\n url: uploadData.upload_url,\n method: 'PUT',\n fields: null,\n headers: {}\n };\n } else if (uploadData.upload_url && typeof uploadData.upload_url === 'object'\n && uploadData.upload_url.upload_url) {\n uploadConfig = {\n url: uploadData.upload_url.upload_url,\n method: uploadData.upload_url.method || 'POST',\n fields: uploadData.upload_url.fields || null,\n headers: uploadData.upload_url.headers || {}\n };\n } else {\n throw new Error(\n `Invalid upload response: unrecognised upload_url format. ` +\n `Server returned: ${JSON.stringify(uploadData.upload_url)}`\n );\n }\n\n // Stage 2: Upload file to signed URL / endpoint\n let result;\n try {\n result = await this._performUpload(uploadConfig);\n } catch (error) {\n throw new Error(`File upload failed: ${error.message}`);\n }\n\n if (this.cancelled) {\n throw new Error('Upload cancelled');\n }\n\n // Stage 3: Mark upload as completed\n try {\n await this._completeUpload();\n } catch (error) {\n console.warn('Failed to mark upload as completed:', error);\n // Don't fail the entire upload for completion marking errors\n // The file was successfully uploaded\n }\n\n // Handle success\n this._onComplete(this.fileModel);\n return this.fileModel;\n\n } catch (error) {\n if (error.message !== 'Upload cancelled') {\n this._onError(error);\n }\n throw error;\n }\n }\n\n /**\n * Initiate upload by calling the API to get signed URL\n * @returns {Promise<Object>} Upload initiation data\n * @private\n */\n async _initiateUpload() {\n try {\n const payload = {\n filename: this.options.name || this.options.file.name,\n file_size: this.options.file.size,\n content_type: this.options.file.type,\n };\n\n if (this.options.group) payload.group = this.options.group;\n if (this.options.description) payload.description = this.options.description;\n\n const response = await this.fileModel.rest.POST('/api/fileman/upload/initiate', payload);\n\n if (!response) {\n throw new Error('No response from upload initiation API');\n }\n\n if (!response.data) {\n throw new Error('Upload initiation response missing data');\n }\n\n // Check server response first (prefer server error messages)\n if (!response.data.status) {\n const errorMessage = response.data.error || 'Upload initiation failed';\n throw new Error(errorMessage);\n }\n\n if (!response.data.data) {\n throw new Error('Upload initiation response missing data payload');\n }\n\n // Set model ID for completion step\n if (response.data.data.id) {\n this.fileModel.set('id', response.data.data.id);\n }\n\n return response.data.data; // { id, upload_url }\n\n } catch (error) {\n // Re-throw with more context if it's a generic error\n if (error.message === 'Network Error' || error.name === 'TypeError') {\n throw new Error('Network error during upload initiation. Please check your connection.');\n }\n throw error;\n }\n }\n\n /**\n * Upload file using the normalised upload config from _startUpload.\n *\n * Two dispatch paths:\n * - PUT + raw bytes → legacy plain-string upload_url, or any config with method PUT.\n * Used by backends that issue a direct signed PUT URL (e.g. S3 signed URL).\n * - POST + FormData → config object with method POST and optional fields dict.\n * Used by the local filesystem backend and S3 presigned POST.\n * fields are appended first, then the file as the \"file\" key,\n * matching Django's request.FILES['file'] convention.\n * Content-Type is intentionally NOT set manually so the browser\n * can write the correct multipart boundary.\n *\n * @param {{ url: string, method: string, fields: object|null, headers: object }} uploadConfig\n * @returns {Promise} Upload result\n * @private\n */\n async _performUpload(uploadConfig) {\n return new Promise((resolve, reject) => {\n if (!(this.options.file instanceof File)) {\n reject(new Error('Only single File objects are supported'));\n return;\n }\n\n const { url, method, fields, headers } = uploadConfig;\n const useFormData = method === 'POST' && fields !== null;\n\n const xhr = new XMLHttpRequest();\n this.uploadRequest = xhr;\n\n // Progress tracking\n xhr.upload.onprogress = (event) => {\n if (this.cancelled) return;\n this._onProgress({\n progress: event.loaded / event.total,\n loaded: event.loaded,\n total: event.total,\n percentage: Math.round((event.loaded / event.total) * 100)\n });\n };\n\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve({ data: xhr.response, status: xhr.status, statusText: xhr.statusText, xhr });\n } else {\n reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));\n }\n };\n\n xhr.onerror = () => reject(new Error('Upload failed: Network error'));\n xhr.ontimeout = () => reject(new Error('Upload timed out — file may be too large or connection too slow'));\n xhr.onabort = () => reject(new Error('Upload cancelled'));\n\n // Relative URLs must be prefixed with /api/ so Django's URL router picks them\n // up correctly, then resolved through the REST service so they target the\n // configured API host rather than the browser's current web host.\n // Absolute URLs (http/https) are returned unchanged — needed for S3 etc.\n let apiUrl = url;\n if (url.startsWith('/') && !url.startsWith('/api/')) {\n apiUrl = '/api' + url;\n }\n const resolvedUrl = this.fileModel.rest.buildUrl(apiUrl);\n xhr.open(method, resolvedUrl);\n xhr.timeout = 30000;\n\n if (useFormData) {\n // POST multipart/form-data — filesystem backend and S3 presigned POST.\n // Any extra headers from the config (e.g. x-amz-* for S3) are set here,\n // but Content-Type is deliberately skipped so the browser sets the boundary.\n for (const [key, value] of Object.entries(headers || {})) {\n if (key.toLowerCase() !== 'content-type') {\n xhr.setRequestHeader(key, value);\n }\n }\n\n const formData = new FormData();\n for (const [key, value] of Object.entries(fields)) {\n formData.append(key, value);\n }\n // File must be last — required by S3 presigned POST policy; harmless for Django.\n formData.append('file', this.options.file);\n xhr.send(formData);\n\n } else {\n // PUT raw bytes — legacy / direct signed URL path.\n xhr.setRequestHeader('Content-Type', this.options.file.type);\n for (const [key, value] of Object.entries(headers || {})) {\n if (key.toLowerCase() !== 'content-type') {\n xhr.setRequestHeader(key, value);\n }\n }\n xhr.send(this.options.file);\n }\n });\n }\n\n /**\n * Mark upload as completed in the API\n * @returns {Promise} Completion result\n * @private\n */\n async _completeUpload() {\n try {\n const response = await this.fileModel.save({ action: 'mark_as_completed' });\n\n if (!response) {\n throw new Error('No response from upload completion API');\n }\n\n // Check server response format (prefer server errors over HTTP errors)\n if (response.data && !response.data.status) {\n const errorMessage = response.data.error || 'Failed to mark upload as completed';\n throw new Error(errorMessage);\n }\n\n return response;\n } catch (error) {\n // Re-throw with more context\n if (error.message === 'Network Error' || error.name === 'TypeError') {\n throw new Error('Network error during upload completion. The file may have uploaded successfully.');\n }\n throw error;\n }\n }\n\n /**\n * Handle progress updates\n * @param {Object} progressInfo - Progress information\n * @private\n */\n _onProgress(progressInfo) {\n // Update progress toast if shown\n if (this.progressToast && this.progressToast.updateProgress) {\n this.progressToast.updateProgress(progressInfo);\n }\n\n // Call user-provided progress callback\n if (typeof this.options.onProgress === 'function') {\n this.options.onProgress(progressInfo);\n }\n }\n\n /**\n * Handle successful upload completion\n * @param {Object} result - Upload result\n * @private\n */\n _onComplete(result) {\n // Mark progress view as completed\n if (this.progressView) {\n this.progressView.markCompleted('Upload completed successfully!');\n }\n\n // Auto-hide progress toast after a delay\n if (this.progressToast) {\n setTimeout(() => {\n try {\n if (this.progressToast && typeof this.progressToast.hide === 'function') {\n this.progressToast.hide();\n }\n } catch (error) {\n console.warn('Error hiding progress toast:', error);\n }\n }, 2000);\n }\n\n // Call user-provided completion callback\n if (typeof this.options.onComplete === 'function') {\n this.options.onComplete(result);\n }\n }\n\n /**\n * Handle upload errors\n * @param {Error} error - Error object\n * @private\n */\n _onError(error) {\n // Hide progress toast immediately and show error toast\n if (this.progressToast) {\n try {\n this.progressToast.hide();\n } catch (error) {\n console.warn('Error hiding progress toast on error:', error);\n }\n }\n\n // Show error toast with immediate feedback\n if (this.toastService) {\n this.toastService.error(`Upload failed: ${error.message}`);\n }\n\n // Call user-provided error callback\n if (typeof this.options.onError === 'function') {\n this.options.onError(error);\n }\n }\n\n /**\n * Show progress toast with ProgressView component\n * @private\n */\n _showProgressToast() {\n // Create progress view with file information\n this.progressView = new ProgressView({\n filename: this.options.name || this.options.file.name,\n filesize: this.options.file.size,\n showCancel: true,\n onCancel: () => this.cancel()\n });\n\n // Show progress view in toast\n this.progressToast = this.toastService.showView(this.progressView, 'info', {\n title: 'File Upload',\n autohide: false,\n dismissible: false\n });\n }\n\n /**\n * Cancel the upload\n * @returns {boolean} True if cancelled, false if already completed\n */\n cancel() {\n if (this.cancelled) {\n return false;\n }\n\n this.cancelled = true;\n\n // Cancel the upload request if in progress\n if (this.uploadRequest && typeof this.uploadRequest.abort === 'function') {\n this.uploadRequest.abort();\n }\n\n // Mark progress view as cancelled\n if (this.progressView) {\n this.progressView.markCancelled();\n }\n\n // Hide progress toast after a delay\n if (this.progressToast) {\n setTimeout(() => {\n try {\n if (this.progressToast && typeof this.progressToast.hide === 'function') {\n this.progressToast.hide();\n }\n } catch (error) {\n console.warn('Error hiding progress toast on cancel:', error);\n }\n }, 1500);\n }\n\n return true;\n }\n\n /**\n * Check if upload is cancelled\n * @returns {boolean} True if cancelled\n */\n isCancelled() {\n return this.cancelled;\n }\n\n /**\n * Promise interface - then\n * @param {function} onSuccess - Success handler\n * @param {function} onError - Error handler\n * @returns {Promise} Promise chain\n */\n then(onSuccess, onError) {\n return this.promise.then(onSuccess, onError);\n }\n\n /**\n * Promise interface - catch\n * @param {function} onError - Error handler\n * @returns {Promise} Promise chain\n */\n catch(onError) {\n return this.promise.catch(onError);\n }\n\n /**\n * Promise interface - finally\n * @param {function} onFinally - Finally handler\n * @returns {Promise} Promise chain\n */\n finally(onFinally) {\n return this.promise.finally(onFinally);\n }\n\n /**\n * Get upload statistics\n * @returns {Object} Upload stats\n */\n getStats() {\n return {\n filename: this.options.file.name,\n size: this.options.file.size,\n type: this.options.file.type,\n cancelled: this.cancelled,\n group: this.options.group,\n description: this.options.description\n };\n }\n}\n\nexport default FileUpload;\n","\nimport Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\nimport FileUpload from '@core/services/FileUpload.js';\nimport {UserList} from '@core/models/User.js';\nimport {GroupList} from '@core/models/Group.js';\n/* =========================\n * FileManager\n * ========================= */\nclass FileManager extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/fileman/manager',\n });\n }\n}\n\nclass FileManagerList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: FileManager,\n endpoint: '/api/fileman/manager',\n size: 10,\n ...options,\n });\n }\n}\n\nconst FileManagerForms = {\n create: {\n title: 'Add Storage Backend',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Display Name',\n placeholder: 'Enter Display Name',\n cols: 12,\n },\n {\n name: 'use',\n type: 'text',\n label: 'Use',\n placeholder: 'Enter User or Leave Blank',\n cols: 12,\n },\n {\n name: 'backend_url',\n type: 'text',\n label: 'Backend URL',\n required: true,\n value: \"s3://BUCKET_NAME/OPTION_FOLDER\",\n placeholder: 's3://bucket_name/optional folder',\n help: 'Format: service://path. Valid services: s3',\n cols: 12,\n },\n {\n name: 'aws_region',\n type: 'select',\n label: 'AWS Region (optional)',\n value: 'us-east-1',\n options: [\n { value: '', text: 'System Default' },\n { value: 'us-east-1', text: 'US East (N. Virginia)' },\n { value: 'us-east-2', text: 'US East (Ohio)' },\n { value: 'us-west-1', text: 'US West (N. California)' },\n { value: 'us-west-2', text: 'US West (Oregon)' },\n { value: 'ca-central-1', text: 'Canada (Central)' },\n { value: 'eu-west-1', text: 'Europe (Ireland)' },\n { value: 'eu-west-2', text: 'Europe (London)' },\n { value: 'eu-west-3', text: 'Europe (Paris)' },\n { value: 'eu-central-1', text: 'Europe (Frankfurt)' },\n { value: 'eu-north-1', text: 'Europe (Stockholm)' },\n { value: 'eu-south-1', text: 'Europe (Milan)' },\n { value: 'ap-southeast-2', text: 'Asia Pacific (Sydney)' }\n ],\n columns: 12,\n help: 'Optional. Defaults to project AWS_REGION if omitted.'\n },\n {\n name: 'aws_key',\n type: 'text',\n label: 'AWS Key (optional)',\n placeholder: 'enter your AWS Key with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Key with S3 permissions'\n },\n {\n name: 'aws_secret',\n type: 'text',\n label: 'AWS Secret (optional)',\n placeholder: 'enter your AWS Secret with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Secret with S3 permissions'\n },\n {\n name: 'is_default',\n type: 'switch',\n label: 'Is Default',\n cols: 6,\n },\n {\n name: 'is_active',\n type: 'switch',\n label: 'Is Active',\n default: true,\n cols: 6,\n },\n ],\n },\n\n edit: {\n title: 'Edit Storage Backend',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Display Name',\n placeholder: 'Enter Display Name',\n cols: 12,\n },\n {\n name: 'use',\n type: 'text',\n label: 'Use',\n placeholder: 'Enter User or Leave Blank',\n cols: 12,\n },\n {\n name: 'backend_url',\n type: 'text',\n label: 'Backend URL',\n required: true,\n placeholder: 's3://bucket_name/optional folder',\n help: 'Format: service://path. Valid services: s3',\n cols: 12,\n },\n {\n name: 'allowed_origins',\n type: 'text',\n label: 'Domains Who Can Upload',\n cols: 12,\n },\n {\n name: 'is_default',\n type: 'switch',\n label: 'Is Default',\n cols: 6,\n },\n {\n name: 'is_active',\n type: 'switch',\n label: 'Is Active',\n default: true,\n cols: 6,\n },\n {\n name: 'is_public',\n type: 'switch',\n label: 'Is Public',\n default: true,\n cols: 6,\n }\n ],\n },\n\n owners: {\n fields: [\n {\n type: 'collection',\n name: 'group',\n label: 'Group (Owner)',\n Collection: GroupList, // Collection class\n labelField: 'name', // Field to display in dropdown\n valueField: 'id', // Field to use as value\n maxItems: 10, // Max items to show in dropdown\n placeholder: 'Search groups...',\n emptyFetch: false,\n debounceMs: 300, // Search debounce delay\n },\n {\n type: 'collection',\n name: 'user',\n label: 'User (Owner)',\n Collection: UserList, // Collection class\n labelField: 'display_name', // Field to display in dropdown\n valueField: 'id', // Field to use as value\n maxItems: 10, // Max items to show in dropdown\n placeholder: 'Search users...',\n emptyFetch: false,\n debounceMs: 300, // Search debounce delay\n },\n ],\n },\n\n credentials: {\n fields: [\n {\n name: 'aws_region',\n type: 'select',\n label: 'AWS Region (optional)',\n value: 'us-east-1',\n options: [\n { value: '', text: 'System Default' },\n { value: 'us-east-1', text: 'US East (N. Virginia)' },\n { value: 'us-east-2', text: 'US East (Ohio)' },\n { value: 'us-west-1', text: 'US West (N. California)' },\n { value: 'us-west-2', text: 'US West (Oregon)' },\n { value: 'ca-central-1', text: 'Canada (Central)' },\n { value: 'eu-west-1', text: 'Europe (Ireland)' },\n { value: 'eu-west-2', text: 'Europe (London)' },\n { value: 'eu-west-3', text: 'Europe (Paris)' },\n { value: 'eu-central-1', text: 'Europe (Frankfurt)' },\n { value: 'eu-north-1', text: 'Europe (Stockholm)' },\n { value: 'eu-south-1', text: 'Europe (Milan)' },\n { value: 'ap-southeast-2', text: 'Asia Pacific (Sydney)' }\n ],\n columns: 12,\n help: 'Optional. Defaults to project AWS_REGION if omitted.'\n },\n {\n name: 'aws_key',\n type: 'text',\n label: 'AWS Key (optional)',\n placeholder: 'enter your AWS Key with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Key with S3 permissions'\n },\n {\n name: 'aws_secret',\n type: 'text',\n label: 'AWS Secret (optional)',\n placeholder: 'enter your AWS Secret with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Secret with S3 permissions'\n },\n ]\n }\n};\n\n/* =========================\n * File\n * ========================= */\nclass File extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/fileman/file',\n });\n }\n\n isImage() {\n return this.get(\"category\") === 'image';\n }\n\n /**\n * Upload file with progress tracking and UI integration\n * Returns a FileUpload instance with promise interface and cancellation support\n *\n * @param {object} options - Upload configuration\n * @param {File} options.file - File object to upload\n * @param {string} options.name - Custom filename (optional)\n * @param {string} options.group - File group/category (optional)\n * @param {string} options.description - File description (optional)\n * @param {function} options.onProgress - Progress callback ({ progress, loaded, total, percentage })\n * @param {function} options.onComplete - Success callback\n * @param {function} options.onError - Error callback\n * @param {boolean} options.showToast - Show progress toast (default: true)\n * @returns {FileUpload} Upload instance with promise interface\n */\n upload(options = {}) {\n return new FileUpload(this, options);\n }\n}\n\nclass FileList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: File,\n endpoint: '/api/fileman/file',\n size: 10,\n ...options,\n });\n }\n}\n\nconst FileForms = {\n create: {\n title: 'Add File',\n fields: [\n\n ],\n },\n\n edit: {\n title: 'Edit File Backend',\n fields: [\n\n ],\n },\n};\n\nexport {\n FileManager,\n FileManagerList,\n FileManagerForms,\n File,\n FileList,\n FileForms,\n};\n"],"names":["ProgressView","View","constructor","options","super","template","this","filename","filesize","filesizeFormatted","dataFormatter","pipe","progress","percentage","loaded","total","loadedFormatted","totalFormatted","status","showCancel","onCancel","cancelled","completed","getTemplate","updateProgress","progressInfo","render","markCompleted","message","markFailed","markCancelled","onActionCancel","action","event","element","disabled","emit","error","console","setFilename","setFilesize","size","getPercentage","isCompleted","isCancelled","getStats","FileUpload","fileModel","file","name","group","description","onProgress","onComplete","onError","showToast","File","Error","uploadRequest","progressToast","progressView","toastService","ToastService","promise","_startUpload","uploadData","uploadConfig","result","_showProgressToast","_initiateUpload","upload_url","url","method","fields","headers","JSON","stringify","_performUpload","_completeUpload","warn","_onComplete","_onError","payload","file_size","content_type","type","response","rest","POST","data","errorMessage","id","set","Promise","resolve","reject","useFormData","xhr","XMLHttpRequest","upload","onprogress","_onProgress","Math","round","onload","statusText","onerror","ontimeout","onabort","apiUrl","startsWith","resolvedUrl","buildUrl","open","timeout","key","value","Object","entries","toLowerCase","setRequestHeader","formData","FormData","append","send","save","setTimeout","hide","cancel","showView","title","autohide","dismissible","abort","then","onSuccess","catch","onFinally","finally","FileManager","Model","endpoint","FileManagerList","Collection","ModelClass","FileManagerForms","create","label","placeholder","cols","required","help","text","columns","default","edit","owners","GroupList","labelField","valueField","maxItems","emptyFetch","debounceMs","UserList","credentials","isImage","get","FileList","FileForms"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Files-6eRT5k3r.js","sources":["../../src/core/views/feedback/ProgressView.js","../../src/core/services/FileUpload.js","../../src/core/models/Files.js"],"sourcesContent":["/**\n * ProgressView - File upload progress component\n * \n * Shows upload progress with progress bar, filename, and cancellation option\n * Integrates with FileUpload service for real-time progress updates\n * \n * Features:\n * - Bootstrap progress bar with percentage\n * - File information (name, size)\n * - Bytes uploaded/total display\n * - Cancel button with confirmation\n * - Responsive design\n * \n * Events:\n * - 'cancel' - Emitted when user cancels upload\n * \n * @example\n * const progressView = new ProgressView({\n * filename: 'document.pdf',\n * filesize: 1024000,\n * onCancel: () => fileUpload.cancel()\n * });\n * \n * // Update progress\n * progressView.updateProgress({ progress: 0.5, loaded: 512000, total: 1024000 });\n */\n\nimport View from '@core/View.js';\nimport dataFormatter from '@core/utils/DataFormatter.js';\n\nclass ProgressView extends View {\n constructor(options = {}) {\n super({\n template: 'progress-view-template',\n ...options\n });\n\n // Initialize progress data\n this.filename = options.filename || 'Unknown file';\n this.filesize = options.filesize || 0;\n this.filesizeFormatted = dataFormatter.pipe(this.filesize, 'filesize');\n \n // Progress state\n this.progress = 0;\n this.percentage = 0;\n this.loaded = 0;\n this.total = this.filesize;\n this.loadedFormatted = '0 B';\n this.totalFormatted = this.filesizeFormatted;\n this.status = 'Starting upload...';\n \n // Options\n this.showCancel = options.showCancel !== false;\n this.onCancel = options.onCancel || null;\n \n // State\n this.cancelled = false;\n this.completed = false;\n }\n\n /**\n * Get template for the progress view\n */\n getTemplate() {\n return `\n <div class=\"progress-view\">\n <div class=\"d-flex justify-content-between align-items-start mb-2\">\n <div class=\"flex-grow-1 min-width-0\">\n <div class=\"fw-medium text-truncate\" title=\"{{filename}}\">\n <i class=\"bi bi-file-earmark me-1\"></i>\n {{filename}}\n </div>\n <small class=\"text-muted\">{{status}}</small>\n </div>\n {{#showCancel}}\n <button type=\"button\" \n class=\"btn btn-sm btn-outline-secondary ms-2\" \n data-action=\"cancel\"\n {{#cancelled}}disabled{{/cancelled}}>\n <i class=\"bi bi-x\"></i>\n </button>\n {{/showCancel}}\n </div>\n \n <div class=\"progress mb-2\" style=\"height: 8px;\">\n <div class=\"progress-bar\" \n role=\"progressbar\" \n style=\"width: {{percentage}}%\"\n aria-valuenow=\"{{percentage}}\" \n aria-valuemin=\"0\" \n aria-valuemax=\"100\">\n </div>\n </div>\n \n <div class=\"d-flex justify-content-between\">\n <small class=\"text-muted\">\n {{loadedFormatted}} / {{totalFormatted}}\n </small>\n <small class=\"text-muted\">\n {{percentage}}%\n </small>\n </div>\n </div>\n `;\n }\n\n /**\n * Update progress information\n * @param {Object} progressInfo - Progress data\n * @param {number} progressInfo.progress - Progress as decimal (0-1)\n * @param {number} progressInfo.loaded - Bytes loaded\n * @param {number} progressInfo.total - Total bytes\n * @param {number} progressInfo.percentage - Progress as percentage (0-100)\n */\n updateProgress(progressInfo) {\n if (this.cancelled || this.completed) {\n return;\n }\n\n this.progress = progressInfo.progress;\n this.percentage = progressInfo.percentage;\n this.loaded = progressInfo.loaded;\n this.total = progressInfo.total || this.filesize;\n \n // Format bytes for display\n this.loadedFormatted = dataFormatter.pipe(this.loaded, 'filesize');\n this.totalFormatted = dataFormatter.pipe(this.total, 'filesize');\n \n // Update status message\n if (this.percentage < 100) {\n this.status = `Uploading... ${this.percentage}%`;\n } else {\n this.status = 'Finalizing upload...';\n }\n\n // Re-render to show updated progress\n this.render();\n }\n\n /**\n * Mark upload as completed\n * @param {string} message - Success message\n */\n markCompleted(message = 'Upload completed!') {\n this.completed = true;\n this.progress = 1;\n this.percentage = 100;\n this.status = message;\n this.render();\n }\n\n /**\n * Mark upload as failed\n * @param {string} message - Error message\n */\n markFailed(message = 'Upload failed') {\n this.status = message;\n this.render();\n }\n\n /**\n * Mark upload as cancelled\n */\n markCancelled() {\n this.cancelled = true;\n this.status = 'Upload cancelled';\n this.render();\n }\n\n /**\n * Handle cancel button click\n * @param {string} action - Action name\n * @param {Event} event - Click event\n * @param {Element} element - Button element\n */\n async onActionCancel(action, event, element) {\n if (this.cancelled || this.completed) {\n return;\n }\n\n // Disable button immediately\n element.disabled = true;\n \n // Mark as cancelled\n this.markCancelled();\n \n // Emit cancel event\n this.emit('cancel');\n \n // Call cancel callback if provided\n if (typeof this.onCancel === 'function') {\n try {\n await this.onCancel();\n } catch (error) {\n console.error('Error in cancel callback:', error);\n }\n }\n }\n\n /**\n * Set filename\n * @param {string} filename - New filename\n */\n setFilename(filename) {\n this.filename = filename;\n this.render();\n }\n\n /**\n * Set file size\n * @param {number} size - File size in bytes\n */\n setFilesize(size) {\n this.filesize = size;\n this.filesizeFormatted = dataFormatter.pipe(size, 'filesize');\n this.total = size;\n this.totalFormatted = this.filesizeFormatted;\n this.render();\n }\n\n /**\n * Get current progress as percentage\n * @returns {number} Progress percentage (0-100)\n */\n getPercentage() {\n return this.percentage;\n }\n\n /**\n * Check if upload is completed\n * @returns {boolean} True if completed\n */\n isCompleted() {\n return this.completed;\n }\n\n /**\n * Check if upload is cancelled\n * @returns {boolean} True if cancelled\n */\n isCancelled() {\n return this.cancelled;\n }\n\n /**\n * Get upload statistics\n * @returns {Object} Upload stats\n */\n getStats() {\n return {\n filename: this.filename,\n filesize: this.filesize,\n progress: this.progress,\n percentage: this.percentage,\n loaded: this.loaded,\n total: this.total,\n cancelled: this.cancelled,\n completed: this.completed,\n status: this.status\n };\n }\n}\n\nexport default ProgressView;","/**\n * FileUpload - File upload service with progress tracking and UI integration\n *\n * Features:\n * - Auto-start upload process\n * - Progress tracking with detailed information\n * - Promise interface with cancellation support\n * - Toast integration for user feedback\n * - Three-stage upload process (initiate → upload → complete)\n *\n * @example\n * const file = new File();\n * const upload = file.upload({\n * file: fileObject,\n * name: 'avatar.jpg',\n * group: 'profile-pics',\n * description: 'User avatar',\n * onProgress: ({ progress, loaded, total, percentage }) => {\n * console.log(`${percentage}% complete`);\n * }\n * });\n *\n * upload.then(result => console.log('Success!'))\n * .catch(error => console.error('Failed:', error));\n */\n\nimport ToastService from './ToastService.js';\nimport ProgressView from '../views/feedback/ProgressView.js';\n\nclass FileUpload {\n constructor(fileModel, options = {}) {\n this.fileModel = fileModel;\n this.options = {\n file: null,\n name: null,\n group: null,\n description: null,\n onProgress: null,\n onComplete: null,\n onError: null,\n showToast: true,\n ...options\n };\n\n // Validation\n if (!this.options.file || !(this.options.file instanceof File)) {\n throw new Error('FileUpload requires a valid File object');\n }\n\n // State management\n this.cancelled = false;\n this.uploadRequest = null;\n this.progressToast = null;\n this.progressView = null;\n this.toastService = null;\n\n // Initialize toast service if needed\n if (this.options.showToast) {\n this.toastService = new ToastService();\n }\n\n // Auto-start upload (Option 3 behavior)\n this.promise = this._startUpload();\n }\n\n /**\n * Main upload orchestration\n * @returns {Promise} Upload promise\n * @private\n */\n async _startUpload() {\n try {\n if (this.options.showToast) {\n this._showProgressToast();\n }\n\n // Stage 1: Initiate upload and get signed URL\n let uploadData;\n try {\n uploadData = await this._initiateUpload();\n } catch (error) {\n throw new Error(`Failed to initiate upload: ${error.message}`);\n }\n\n if (this.cancelled) {\n throw new Error('Upload cancelled');\n }\n\n // Validate upload data\n if (!uploadData || !uploadData.upload_url) {\n throw new Error('Invalid upload response: missing upload URL');\n }\n\n // Normalise upload_url — the backend can return either:\n // • a plain string → direct PUT with raw bytes (e.g. S3 signed URL)\n // • a config object → POST multipart/form-data (e.g. filesystem backend)\n // { upload_url: string, method: string, fields: object, headers: object }\n let uploadConfig;\n if (typeof uploadData.upload_url === 'string') {\n uploadConfig = {\n url: uploadData.upload_url,\n method: 'PUT',\n fields: null,\n headers: {}\n };\n } else if (uploadData.upload_url && typeof uploadData.upload_url === 'object'\n && uploadData.upload_url.upload_url) {\n uploadConfig = {\n url: uploadData.upload_url.upload_url,\n method: uploadData.upload_url.method || 'POST',\n fields: uploadData.upload_url.fields || null,\n headers: uploadData.upload_url.headers || {}\n };\n } else {\n throw new Error(\n `Invalid upload response: unrecognised upload_url format. ` +\n `Server returned: ${JSON.stringify(uploadData.upload_url)}`\n );\n }\n\n // Stage 2: Upload file to signed URL / endpoint\n let result;\n try {\n result = await this._performUpload(uploadConfig);\n } catch (error) {\n throw new Error(`File upload failed: ${error.message}`);\n }\n\n if (this.cancelled) {\n throw new Error('Upload cancelled');\n }\n\n // Stage 3: Mark upload as completed\n try {\n await this._completeUpload();\n } catch (error) {\n console.warn('Failed to mark upload as completed:', error);\n // Don't fail the entire upload for completion marking errors\n // The file was successfully uploaded\n }\n\n // Handle success\n this._onComplete(this.fileModel);\n return this.fileModel;\n\n } catch (error) {\n if (error.message !== 'Upload cancelled') {\n this._onError(error);\n }\n throw error;\n }\n }\n\n /**\n * Initiate upload by calling the API to get signed URL\n * @returns {Promise<Object>} Upload initiation data\n * @private\n */\n async _initiateUpload() {\n try {\n const payload = {\n filename: this.options.name || this.options.file.name,\n file_size: this.options.file.size,\n content_type: this.options.file.type,\n };\n\n if (this.options.group) payload.group = this.options.group;\n if (this.options.description) payload.description = this.options.description;\n\n const response = await this.fileModel.rest.POST('/api/fileman/upload/initiate', payload);\n\n if (!response) {\n throw new Error('No response from upload initiation API');\n }\n\n if (!response.data) {\n throw new Error('Upload initiation response missing data');\n }\n\n // Check server response first (prefer server error messages)\n if (!response.data.status) {\n const errorMessage = response.data.error || 'Upload initiation failed';\n throw new Error(errorMessage);\n }\n\n if (!response.data.data) {\n throw new Error('Upload initiation response missing data payload');\n }\n\n // Set model ID for completion step\n if (response.data.data.id) {\n this.fileModel.set('id', response.data.data.id);\n }\n\n return response.data.data; // { id, upload_url }\n\n } catch (error) {\n // Re-throw with more context if it's a generic error\n if (error.message === 'Network Error' || error.name === 'TypeError') {\n throw new Error('Network error during upload initiation. Please check your connection.');\n }\n throw error;\n }\n }\n\n /**\n * Upload file using the normalised upload config from _startUpload.\n *\n * Two dispatch paths:\n * - PUT + raw bytes → legacy plain-string upload_url, or any config with method PUT.\n * Used by backends that issue a direct signed PUT URL (e.g. S3 signed URL).\n * - POST + FormData → config object with method POST and optional fields dict.\n * Used by the local filesystem backend and S3 presigned POST.\n * fields are appended first, then the file as the \"file\" key,\n * matching Django's request.FILES['file'] convention.\n * Content-Type is intentionally NOT set manually so the browser\n * can write the correct multipart boundary.\n *\n * @param {{ url: string, method: string, fields: object|null, headers: object }} uploadConfig\n * @returns {Promise} Upload result\n * @private\n */\n async _performUpload(uploadConfig) {\n return new Promise((resolve, reject) => {\n if (!(this.options.file instanceof File)) {\n reject(new Error('Only single File objects are supported'));\n return;\n }\n\n const { url, method, fields, headers } = uploadConfig;\n const useFormData = method === 'POST' && fields !== null;\n\n const xhr = new XMLHttpRequest();\n this.uploadRequest = xhr;\n\n // Progress tracking\n xhr.upload.onprogress = (event) => {\n if (this.cancelled) return;\n this._onProgress({\n progress: event.loaded / event.total,\n loaded: event.loaded,\n total: event.total,\n percentage: Math.round((event.loaded / event.total) * 100)\n });\n };\n\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve({ data: xhr.response, status: xhr.status, statusText: xhr.statusText, xhr });\n } else {\n reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));\n }\n };\n\n xhr.onerror = () => reject(new Error('Upload failed: Network error'));\n xhr.ontimeout = () => reject(new Error('Upload timed out — file may be too large or connection too slow'));\n xhr.onabort = () => reject(new Error('Upload cancelled'));\n\n // Relative URLs must be prefixed with /api/ so Django's URL router picks them\n // up correctly, then resolved through the REST service so they target the\n // configured API host rather than the browser's current web host.\n // Absolute URLs (http/https) are returned unchanged — needed for S3 etc.\n let apiUrl = url;\n if (url.startsWith('/') && !url.startsWith('/api/')) {\n apiUrl = '/api' + url;\n }\n const resolvedUrl = this.fileModel.rest.buildUrl(apiUrl);\n xhr.open(method, resolvedUrl);\n xhr.timeout = 30000;\n\n if (useFormData) {\n // POST multipart/form-data — filesystem backend and S3 presigned POST.\n // Any extra headers from the config (e.g. x-amz-* for S3) are set here,\n // but Content-Type is deliberately skipped so the browser sets the boundary.\n for (const [key, value] of Object.entries(headers || {})) {\n if (key.toLowerCase() !== 'content-type') {\n xhr.setRequestHeader(key, value);\n }\n }\n\n const formData = new FormData();\n for (const [key, value] of Object.entries(fields)) {\n formData.append(key, value);\n }\n // File must be last — required by S3 presigned POST policy; harmless for Django.\n formData.append('file', this.options.file);\n xhr.send(formData);\n\n } else {\n // PUT raw bytes — legacy / direct signed URL path.\n xhr.setRequestHeader('Content-Type', this.options.file.type);\n for (const [key, value] of Object.entries(headers || {})) {\n if (key.toLowerCase() !== 'content-type') {\n xhr.setRequestHeader(key, value);\n }\n }\n xhr.send(this.options.file);\n }\n });\n }\n\n /**\n * Mark upload as completed in the API\n * @returns {Promise} Completion result\n * @private\n */\n async _completeUpload() {\n try {\n const response = await this.fileModel.save({ action: 'mark_as_completed' });\n\n if (!response) {\n throw new Error('No response from upload completion API');\n }\n\n // Check server response format (prefer server errors over HTTP errors)\n if (response.data && !response.data.status) {\n const errorMessage = response.data.error || 'Failed to mark upload as completed';\n throw new Error(errorMessage);\n }\n\n return response;\n } catch (error) {\n // Re-throw with more context\n if (error.message === 'Network Error' || error.name === 'TypeError') {\n throw new Error('Network error during upload completion. The file may have uploaded successfully.');\n }\n throw error;\n }\n }\n\n /**\n * Handle progress updates\n * @param {Object} progressInfo - Progress information\n * @private\n */\n _onProgress(progressInfo) {\n // Update progress toast if shown\n if (this.progressToast && this.progressToast.updateProgress) {\n this.progressToast.updateProgress(progressInfo);\n }\n\n // Call user-provided progress callback\n if (typeof this.options.onProgress === 'function') {\n this.options.onProgress(progressInfo);\n }\n }\n\n /**\n * Handle successful upload completion\n * @param {Object} result - Upload result\n * @private\n */\n _onComplete(result) {\n // Mark progress view as completed\n if (this.progressView) {\n this.progressView.markCompleted('Upload completed successfully!');\n }\n\n // Auto-hide progress toast after a delay\n if (this.progressToast) {\n setTimeout(() => {\n try {\n if (this.progressToast && typeof this.progressToast.hide === 'function') {\n this.progressToast.hide();\n }\n } catch (error) {\n console.warn('Error hiding progress toast:', error);\n }\n }, 2000);\n }\n\n // Call user-provided completion callback\n if (typeof this.options.onComplete === 'function') {\n this.options.onComplete(result);\n }\n }\n\n /**\n * Handle upload errors\n * @param {Error} error - Error object\n * @private\n */\n _onError(error) {\n // Hide progress toast immediately and show error toast\n if (this.progressToast) {\n try {\n this.progressToast.hide();\n } catch (error) {\n console.warn('Error hiding progress toast on error:', error);\n }\n }\n\n // Show error toast with immediate feedback\n if (this.toastService) {\n this.toastService.error(`Upload failed: ${error.message}`);\n }\n\n // Call user-provided error callback\n if (typeof this.options.onError === 'function') {\n this.options.onError(error);\n }\n }\n\n /**\n * Show progress toast with ProgressView component\n * @private\n */\n _showProgressToast() {\n // Create progress view with file information\n this.progressView = new ProgressView({\n filename: this.options.name || this.options.file.name,\n filesize: this.options.file.size,\n showCancel: true,\n onCancel: () => this.cancel()\n });\n\n // Show progress view in toast\n this.progressToast = this.toastService.showView(this.progressView, 'info', {\n title: 'File Upload',\n autohide: false,\n dismissible: false\n });\n }\n\n /**\n * Cancel the upload\n * @returns {boolean} True if cancelled, false if already completed\n */\n cancel() {\n if (this.cancelled) {\n return false;\n }\n\n this.cancelled = true;\n\n // Cancel the upload request if in progress\n if (this.uploadRequest && typeof this.uploadRequest.abort === 'function') {\n this.uploadRequest.abort();\n }\n\n // Mark progress view as cancelled\n if (this.progressView) {\n this.progressView.markCancelled();\n }\n\n // Hide progress toast after a delay\n if (this.progressToast) {\n setTimeout(() => {\n try {\n if (this.progressToast && typeof this.progressToast.hide === 'function') {\n this.progressToast.hide();\n }\n } catch (error) {\n console.warn('Error hiding progress toast on cancel:', error);\n }\n }, 1500);\n }\n\n return true;\n }\n\n /**\n * Check if upload is cancelled\n * @returns {boolean} True if cancelled\n */\n isCancelled() {\n return this.cancelled;\n }\n\n /**\n * Promise interface - then\n * @param {function} onSuccess - Success handler\n * @param {function} onError - Error handler\n * @returns {Promise} Promise chain\n */\n then(onSuccess, onError) {\n return this.promise.then(onSuccess, onError);\n }\n\n /**\n * Promise interface - catch\n * @param {function} onError - Error handler\n * @returns {Promise} Promise chain\n */\n catch(onError) {\n return this.promise.catch(onError);\n }\n\n /**\n * Promise interface - finally\n * @param {function} onFinally - Finally handler\n * @returns {Promise} Promise chain\n */\n finally(onFinally) {\n return this.promise.finally(onFinally);\n }\n\n /**\n * Get upload statistics\n * @returns {Object} Upload stats\n */\n getStats() {\n return {\n filename: this.options.file.name,\n size: this.options.file.size,\n type: this.options.file.type,\n cancelled: this.cancelled,\n group: this.options.group,\n description: this.options.description\n };\n }\n}\n\nexport default FileUpload;\n","\nimport Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\nimport FileUpload from '@core/services/FileUpload.js';\nimport {UserList} from '@core/models/User.js';\nimport {GroupList} from '@core/models/Group.js';\n/* =========================\n * FileManager\n * ========================= */\nclass FileManager extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/fileman/manager',\n });\n }\n}\n\nclass FileManagerList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: FileManager,\n endpoint: '/api/fileman/manager',\n size: 10,\n ...options,\n });\n }\n}\n\nconst FileManagerForms = {\n create: {\n title: 'Add Storage Backend',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Display Name',\n placeholder: 'Enter Display Name',\n cols: 12,\n },\n {\n name: 'use',\n type: 'text',\n label: 'Use',\n placeholder: 'Enter User or Leave Blank',\n cols: 12,\n },\n {\n name: 'backend_url',\n type: 'text',\n label: 'Backend URL',\n required: true,\n value: \"s3://BUCKET_NAME/OPTION_FOLDER\",\n placeholder: 's3://bucket_name/optional folder',\n help: 'Format: service://path. Valid services: s3',\n cols: 12,\n },\n {\n name: 'aws_region',\n type: 'select',\n label: 'AWS Region (optional)',\n value: 'us-east-1',\n options: [\n { value: '', text: 'System Default' },\n { value: 'us-east-1', text: 'US East (N. Virginia)' },\n { value: 'us-east-2', text: 'US East (Ohio)' },\n { value: 'us-west-1', text: 'US West (N. California)' },\n { value: 'us-west-2', text: 'US West (Oregon)' },\n { value: 'ca-central-1', text: 'Canada (Central)' },\n { value: 'eu-west-1', text: 'Europe (Ireland)' },\n { value: 'eu-west-2', text: 'Europe (London)' },\n { value: 'eu-west-3', text: 'Europe (Paris)' },\n { value: 'eu-central-1', text: 'Europe (Frankfurt)' },\n { value: 'eu-north-1', text: 'Europe (Stockholm)' },\n { value: 'eu-south-1', text: 'Europe (Milan)' },\n { value: 'ap-southeast-2', text: 'Asia Pacific (Sydney)' }\n ],\n columns: 12,\n help: 'Optional. Defaults to project AWS_REGION if omitted.'\n },\n {\n name: 'aws_key',\n type: 'text',\n label: 'AWS Key (optional)',\n placeholder: 'enter your AWS Key with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Key with S3 permissions'\n },\n {\n name: 'aws_secret',\n type: 'text',\n label: 'AWS Secret (optional)',\n placeholder: 'enter your AWS Secret with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Secret with S3 permissions'\n },\n {\n name: 'is_default',\n type: 'switch',\n label: 'Is Default',\n cols: 6,\n },\n {\n name: 'is_active',\n type: 'switch',\n label: 'Is Active',\n default: true,\n cols: 6,\n },\n ],\n },\n\n edit: {\n title: 'Edit Storage Backend',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Display Name',\n placeholder: 'Enter Display Name',\n cols: 12,\n },\n {\n name: 'use',\n type: 'text',\n label: 'Use',\n placeholder: 'Enter User or Leave Blank',\n cols: 12,\n },\n {\n name: 'backend_url',\n type: 'text',\n label: 'Backend URL',\n required: true,\n placeholder: 's3://bucket_name/optional folder',\n help: 'Format: service://path. Valid services: s3',\n cols: 12,\n },\n {\n name: 'allowed_origins',\n type: 'text',\n label: 'Domains Who Can Upload',\n cols: 12,\n },\n {\n name: 'is_default',\n type: 'switch',\n label: 'Is Default',\n cols: 6,\n },\n {\n name: 'is_active',\n type: 'switch',\n label: 'Is Active',\n default: true,\n cols: 6,\n },\n {\n name: 'is_public',\n type: 'switch',\n label: 'Is Public',\n default: true,\n cols: 6,\n }\n ],\n },\n\n owners: {\n fields: [\n {\n type: 'collection',\n name: 'group',\n label: 'Group (Owner)',\n Collection: GroupList, // Collection class\n labelField: 'name', // Field to display in dropdown\n valueField: 'id', // Field to use as value\n maxItems: 10, // Max items to show in dropdown\n placeholder: 'Search groups...',\n emptyFetch: false,\n debounceMs: 300, // Search debounce delay\n },\n {\n type: 'collection',\n name: 'user',\n label: 'User (Owner)',\n Collection: UserList, // Collection class\n labelField: 'display_name', // Field to display in dropdown\n valueField: 'id', // Field to use as value\n maxItems: 10, // Max items to show in dropdown\n placeholder: 'Search users...',\n emptyFetch: false,\n debounceMs: 300, // Search debounce delay\n },\n ],\n },\n\n credentials: {\n fields: [\n {\n name: 'aws_region',\n type: 'select',\n label: 'AWS Region (optional)',\n value: 'us-east-1',\n options: [\n { value: '', text: 'System Default' },\n { value: 'us-east-1', text: 'US East (N. Virginia)' },\n { value: 'us-east-2', text: 'US East (Ohio)' },\n { value: 'us-west-1', text: 'US West (N. California)' },\n { value: 'us-west-2', text: 'US West (Oregon)' },\n { value: 'ca-central-1', text: 'Canada (Central)' },\n { value: 'eu-west-1', text: 'Europe (Ireland)' },\n { value: 'eu-west-2', text: 'Europe (London)' },\n { value: 'eu-west-3', text: 'Europe (Paris)' },\n { value: 'eu-central-1', text: 'Europe (Frankfurt)' },\n { value: 'eu-north-1', text: 'Europe (Stockholm)' },\n { value: 'eu-south-1', text: 'Europe (Milan)' },\n { value: 'ap-southeast-2', text: 'Asia Pacific (Sydney)' }\n ],\n columns: 12,\n help: 'Optional. Defaults to project AWS_REGION if omitted.'\n },\n {\n name: 'aws_key',\n type: 'text',\n label: 'AWS Key (optional)',\n placeholder: 'enter your AWS Key with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Key with S3 permissions'\n },\n {\n name: 'aws_secret',\n type: 'text',\n label: 'AWS Secret (optional)',\n placeholder: 'enter your AWS Secret with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Secret with S3 permissions'\n },\n ]\n }\n};\n\n/* =========================\n * File\n * ========================= */\nclass File extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/fileman/file',\n });\n }\n\n isImage() {\n return this.get(\"category\") === 'image';\n }\n\n /**\n * Upload file with progress tracking and UI integration\n * Returns a FileUpload instance with promise interface and cancellation support\n *\n * @param {object} options - Upload configuration\n * @param {File} options.file - File object to upload\n * @param {string} options.name - Custom filename (optional)\n * @param {string} options.group - File group/category (optional)\n * @param {string} options.description - File description (optional)\n * @param {function} options.onProgress - Progress callback ({ progress, loaded, total, percentage })\n * @param {function} options.onComplete - Success callback\n * @param {function} options.onError - Error callback\n * @param {boolean} options.showToast - Show progress toast (default: true)\n * @returns {FileUpload} Upload instance with promise interface\n */\n upload(options = {}) {\n return new FileUpload(this, options);\n }\n}\n\nclass FileList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: File,\n endpoint: '/api/fileman/file',\n size: 10,\n ...options,\n });\n }\n}\n\nconst FileForms = {\n create: {\n title: 'Add File',\n fields: [\n\n ],\n },\n\n edit: {\n title: 'Edit File Backend',\n fields: [\n\n ],\n },\n};\n\nexport {\n FileManager,\n FileManagerList,\n FileManagerForms,\n File,\n FileList,\n FileForms,\n};\n"],"names":["ProgressView","View","constructor","options","super","template","this","filename","filesize","filesizeFormatted","dataFormatter","pipe","progress","percentage","loaded","total","loadedFormatted","totalFormatted","status","showCancel","onCancel","cancelled","completed","getTemplate","updateProgress","progressInfo","render","markCompleted","message","markFailed","markCancelled","onActionCancel","action","event","element","disabled","emit","error","console","setFilename","setFilesize","size","getPercentage","isCompleted","isCancelled","getStats","FileUpload","fileModel","file","name","group","description","onProgress","onComplete","onError","showToast","File","Error","uploadRequest","progressToast","progressView","toastService","ToastService","promise","_startUpload","uploadData","uploadConfig","result","_showProgressToast","_initiateUpload","upload_url","url","method","fields","headers","JSON","stringify","_performUpload","_completeUpload","warn","_onComplete","_onError","payload","file_size","content_type","type","response","rest","POST","data","errorMessage","id","set","Promise","resolve","reject","useFormData","xhr","XMLHttpRequest","upload","onprogress","_onProgress","Math","round","onload","statusText","onerror","ontimeout","onabort","apiUrl","startsWith","resolvedUrl","buildUrl","open","timeout","key","value","Object","entries","toLowerCase","setRequestHeader","formData","FormData","append","send","save","setTimeout","hide","cancel","showView","title","autohide","dismissible","abort","then","onSuccess","catch","onFinally","finally","FileManager","Model","endpoint","FileManagerList","Collection","ModelClass","FileManagerForms","create","label","placeholder","cols","required","help","text","columns","default","edit","owners","GroupList","labelField","valueField","maxItems","emptyFetch","debounceMs","UserList","credentials","isImage","get","FileList","FileForms"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";const e=require("./Collection-CmjTsmrP.js"),t=require("./ToastService-C2tTooFn.js"),s=require("./Rest-B1eUyLX5.js"),o=require("./User-DqHG5Gr1.js");class ProgressView extends s.View{constructor(e={}){super({template:"progress-view-template",...e}),this.filename=e.filename||"Unknown file",this.filesize=e.filesize||0,this.filesizeFormatted=s.dataFormatter.pipe(this.filesize,"filesize"),this.progress=0,this.percentage=0,this.loaded=0,this.total=this.filesize,this.loadedFormatted="0 B",this.totalFormatted=this.filesizeFormatted,this.status="Starting upload...",this.showCancel=!1!==e.showCancel,this.onCancel=e.onCancel||null,this.cancelled=!1,this.completed=!1}getTemplate(){return'\n <div class="progress-view">\n <div class="d-flex justify-content-between align-items-start mb-2">\n <div class="flex-grow-1 min-width-0">\n <div class="fw-medium text-truncate" title="{{filename}}">\n <i class="bi bi-file-earmark me-1"></i>\n {{filename}}\n </div>\n <small class="text-muted">{{status}}</small>\n </div>\n {{#showCancel}}\n <button type="button" \n class="btn btn-sm btn-outline-secondary ms-2" \n data-action="cancel"\n {{#cancelled}}disabled{{/cancelled}}>\n <i class="bi bi-x"></i>\n </button>\n {{/showCancel}}\n </div>\n \n <div class="progress mb-2" style="height: 8px;">\n <div class="progress-bar" \n role="progressbar" \n style="width: {{percentage}}%"\n aria-valuenow="{{percentage}}" \n aria-valuemin="0" \n aria-valuemax="100">\n </div>\n </div>\n \n <div class="d-flex justify-content-between">\n <small class="text-muted">\n {{loadedFormatted}} / {{totalFormatted}}\n </small>\n <small class="text-muted">\n {{percentage}}%\n </small>\n </div>\n </div>\n '}updateProgress(e){this.cancelled||this.completed||(this.progress=e.progress,this.percentage=e.percentage,this.loaded=e.loaded,this.total=e.total||this.filesize,this.loadedFormatted=s.dataFormatter.pipe(this.loaded,"filesize"),this.totalFormatted=s.dataFormatter.pipe(this.total,"filesize"),this.percentage<100?this.status=`Uploading... ${this.percentage}%`:this.status="Finalizing upload...",this.render())}markCompleted(e="Upload completed!"){this.completed=!0,this.progress=1,this.percentage=100,this.status=e,this.render()}markFailed(e="Upload failed"){this.status=e,this.render()}markCancelled(){this.cancelled=!0,this.status="Upload cancelled",this.render()}async onActionCancel(e,t,s){if(!this.cancelled&&!this.completed&&(s.disabled=!0,this.markCancelled(),this.emit("cancel"),"function"==typeof this.onCancel))try{await this.onCancel()}catch(o){console.error("Error in cancel callback:",o)}}setFilename(e){this.filename=e,this.render()}setFilesize(e){this.filesize=e,this.filesizeFormatted=s.dataFormatter.pipe(e,"filesize"),this.total=e,this.totalFormatted=this.filesizeFormatted,this.render()}getPercentage(){return this.percentage}isCompleted(){return this.completed}isCancelled(){return this.cancelled}getStats(){return{filename:this.filename,filesize:this.filesize,progress:this.progress,percentage:this.percentage,loaded:this.loaded,total:this.total,cancelled:this.cancelled,completed:this.completed,status:this.status}}}class FileUpload{constructor(e,s={}){if(this.fileModel=e,this.options={file:null,name:null,group:null,description:null,onProgress:null,onComplete:null,onError:null,showToast:!0,...s},!(this.options.file&&this.options.file instanceof File))throw new Error("FileUpload requires a valid File object");this.cancelled=!1,this.uploadRequest=null,this.progressToast=null,this.progressView=null,this.toastService=null,this.options.showToast&&(this.toastService=new t.ToastService),this.promise=this._startUpload()}async _startUpload(){try{let t,s,o;this.options.showToast&&this._showProgressToast();try{t=await this._initiateUpload()}catch(e){throw new Error(`Failed to initiate upload: ${e.message}`)}if(this.cancelled)throw new Error("Upload cancelled");if(!t||!t.upload_url)throw new Error("Invalid upload response: missing upload URL");if("string"==typeof t.upload_url)s={url:t.upload_url,method:"PUT",fields:null,headers:{}};else{if(!t.upload_url||"object"!=typeof t.upload_url||!t.upload_url.upload_url)throw new Error(`Invalid upload response: unrecognised upload_url format. Server returned: ${JSON.stringify(t.upload_url)}`);s={url:t.upload_url.upload_url,method:t.upload_url.method||"POST",fields:t.upload_url.fields||null,headers:t.upload_url.headers||{}}}try{o=await this._performUpload(s)}catch(e){throw new Error(`File upload failed: ${e.message}`)}if(this.cancelled)throw new Error("Upload cancelled");try{await this._completeUpload()}catch(e){console.warn("Failed to mark upload as completed:",e)}return this._onComplete(this.fileModel),this.fileModel}catch(e){throw"Upload cancelled"!==e.message&&this._onError(e),e}}async _initiateUpload(){try{const e={filename:this.options.name||this.options.file.name,file_size:this.options.file.size,content_type:this.options.file.type};this.options.group&&(e.group=this.options.group),this.options.description&&(e.description=this.options.description);const t=await this.fileModel.rest.POST("/api/fileman/upload/initiate",e);if(!t)throw new Error("No response from upload initiation API");if(!t.data)throw new Error("Upload initiation response missing data");if(!t.data.status){const e=t.data.error||"Upload initiation failed";throw new Error(e)}if(!t.data.data)throw new Error("Upload initiation response missing data payload");return t.data.data.id&&this.fileModel.set("id",t.data.data.id),t.data.data}catch(e){if("Network Error"===e.message||"TypeError"===e.name)throw new Error("Network error during upload initiation. Please check your connection.");throw e}}async _performUpload(e){return new Promise((t,s)=>{if(!(this.options.file instanceof File))return void s(new Error("Only single File objects are supported"));const{url:o,method:i,fields:a,headers:l}=e,r="POST"===i&&null!==a,n=new XMLHttpRequest;this.uploadRequest=n,n.upload.onprogress=e=>{this.cancelled||this._onProgress({progress:e.loaded/e.total,loaded:e.loaded,total:e.total,percentage:Math.round(e.loaded/e.total*100)})},n.onload=()=>{n.status>=200&&n.status<300?t({data:n.response,status:n.status,statusText:n.statusText,xhr:n}):s(new Error(`Upload failed: ${n.status} ${n.statusText}`))},n.onerror=()=>s(new Error("Upload failed: Network error")),n.ontimeout=()=>s(new Error("Upload timed out — file may be too large or connection too slow")),n.onabort=()=>s(new Error("Upload cancelled"));let p=o;o.startsWith("/")&&!o.startsWith("/api/")&&(p="/api"+o);const d=this.fileModel.rest.buildUrl(p);if(n.open(i,d),n.timeout=3e4,r){for(const[t,s]of Object.entries(l||{}))"content-type"!==t.toLowerCase()&&n.setRequestHeader(t,s);const e=new FormData;for(const[t,s]of Object.entries(a))e.append(t,s);e.append("file",this.options.file),n.send(e)}else{n.setRequestHeader("Content-Type",this.options.file.type);for(const[e,t]of Object.entries(l||{}))"content-type"!==e.toLowerCase()&&n.setRequestHeader(e,t);n.send(this.options.file)}})}async _completeUpload(){try{const e=await this.fileModel.save({action:"mark_as_completed"});if(!e)throw new Error("No response from upload completion API");if(e.data&&!e.data.status){const t=e.data.error||"Failed to mark upload as completed";throw new Error(t)}return e}catch(e){if("Network Error"===e.message||"TypeError"===e.name)throw new Error("Network error during upload completion. The file may have uploaded successfully.");throw e}}_onProgress(e){this.progressToast&&this.progressToast.updateProgress&&this.progressToast.updateProgress(e),"function"==typeof this.options.onProgress&&this.options.onProgress(e)}_onComplete(e){this.progressView&&this.progressView.markCompleted("Upload completed successfully!"),this.progressToast&&setTimeout(()=>{try{this.progressToast&&"function"==typeof this.progressToast.hide&&this.progressToast.hide()}catch(e){console.warn("Error hiding progress toast:",e)}},2e3),"function"==typeof this.options.onComplete&&this.options.onComplete(e)}_onError(e){if(this.progressToast)try{this.progressToast.hide()}catch(t){console.warn("Error hiding progress toast on error:",t)}this.toastService&&this.toastService.error(`Upload failed: ${e.message}`),"function"==typeof this.options.onError&&this.options.onError(e)}_showProgressToast(){this.progressView=new ProgressView({filename:this.options.name||this.options.file.name,filesize:this.options.file.size,showCancel:!0,onCancel:()=>this.cancel()}),this.progressToast=this.toastService.showView(this.progressView,"info",{title:"File Upload",autohide:!1,dismissible:!1})}cancel(){return!this.cancelled&&(this.cancelled=!0,this.uploadRequest&&"function"==typeof this.uploadRequest.abort&&this.uploadRequest.abort(),this.progressView&&this.progressView.markCancelled(),this.progressToast&&setTimeout(()=>{try{this.progressToast&&"function"==typeof this.progressToast.hide&&this.progressToast.hide()}catch(e){console.warn("Error hiding progress toast on cancel:",e)}},1500),!0)}isCancelled(){return this.cancelled}then(e,t){return this.promise.then(e,t)}catch(e){return this.promise.catch(e)}finally(e){return this.promise.finally(e)}getStats(){return{filename:this.options.file.name,size:this.options.file.size,type:this.options.file.type,cancelled:this.cancelled,group:this.options.group,description:this.options.description}}}class FileManager extends e.Model{constructor(e={}){super(e,{endpoint:"/api/fileman/manager"})}}class FileManagerList extends e.Collection{constructor(e={}){super({ModelClass:FileManager,endpoint:"/api/fileman/manager",size:10,...e})}}const i={create:{title:"Add Storage Backend",fields:[{name:"name",type:"text",label:"Display Name",placeholder:"Enter Display Name",cols:12},{name:"use",type:"text",label:"Use",placeholder:"Enter User or Leave Blank",cols:12},{name:"backend_url",type:"text",label:"Backend URL",required:!0,value:"s3://BUCKET_NAME/OPTION_FOLDER",placeholder:"s3://bucket_name/optional folder",help:"Format: service://path. Valid services: s3",cols:12},{name:"aws_region",type:"select",label:"AWS Region (optional)",value:"us-east-1",options:[{value:"",text:"System Default"},{value:"us-east-1",text:"US East (N. Virginia)"},{value:"us-east-2",text:"US East (Ohio)"},{value:"us-west-1",text:"US West (N. California)"},{value:"us-west-2",text:"US West (Oregon)"},{value:"ca-central-1",text:"Canada (Central)"},{value:"eu-west-1",text:"Europe (Ireland)"},{value:"eu-west-2",text:"Europe (London)"},{value:"eu-west-3",text:"Europe (Paris)"},{value:"eu-central-1",text:"Europe (Frankfurt)"},{value:"eu-north-1",text:"Europe (Stockholm)"},{value:"eu-south-1",text:"Europe (Milan)"},{value:"ap-southeast-2",text:"Asia Pacific (Sydney)"}],columns:12,help:"Optional. Defaults to project AWS_REGION if omitted."},{name:"aws_key",type:"text",label:"AWS Key (optional)",placeholder:"enter your AWS Key with S3 permissions",columns:12,help:"Optional, AWS Key with S3 permissions"},{name:"aws_secret",type:"text",label:"AWS Secret (optional)",placeholder:"enter your AWS Secret with S3 permissions",columns:12,help:"Optional, AWS Secret with S3 permissions"},{name:"is_default",type:"switch",label:"Is Default",cols:6},{name:"is_active",type:"switch",label:"Is Active",default:!0,cols:6}]},edit:{title:"Edit Storage Backend",fields:[{name:"name",type:"text",label:"Display Name",placeholder:"Enter Display Name",cols:12},{name:"use",type:"text",label:"Use",placeholder:"Enter User or Leave Blank",cols:12},{name:"backend_url",type:"text",label:"Backend URL",required:!0,placeholder:"s3://bucket_name/optional folder",help:"Format: service://path. Valid services: s3",cols:12},{name:"allowed_origins",type:"text",label:"Domains Who Can Upload",cols:12},{name:"is_default",type:"switch",label:"Is Default",cols:6},{name:"is_active",type:"switch",label:"Is Active",default:!0,cols:6},{name:"is_public",type:"switch",label:"Is Public",default:!0,cols:6}]},owners:{fields:[{type:"collection",name:"group",label:"Group (Owner)",Collection:o.GroupList,labelField:"name",valueField:"id",maxItems:10,placeholder:"Search groups...",emptyFetch:!1,debounceMs:300},{type:"collection",name:"user",label:"User (Owner)",Collection:o.UserList,labelField:"display_name",valueField:"id",maxItems:10,placeholder:"Search users...",emptyFetch:!1,debounceMs:300}]},credentials:{fields:[{name:"aws_region",type:"select",label:"AWS Region (optional)",value:"us-east-1",options:[{value:"",text:"System Default"},{value:"us-east-1",text:"US East (N. Virginia)"},{value:"us-east-2",text:"US East (Ohio)"},{value:"us-west-1",text:"US West (N. California)"},{value:"us-west-2",text:"US West (Oregon)"},{value:"ca-central-1",text:"Canada (Central)"},{value:"eu-west-1",text:"Europe (Ireland)"},{value:"eu-west-2",text:"Europe (London)"},{value:"eu-west-3",text:"Europe (Paris)"},{value:"eu-central-1",text:"Europe (Frankfurt)"},{value:"eu-north-1",text:"Europe (Stockholm)"},{value:"eu-south-1",text:"Europe (Milan)"},{value:"ap-southeast-2",text:"Asia Pacific (Sydney)"}],columns:12,help:"Optional. Defaults to project AWS_REGION if omitted."},{name:"aws_key",type:"text",label:"AWS Key (optional)",placeholder:"enter your AWS Key with S3 permissions",columns:12,help:"Optional, AWS Key with S3 permissions"},{name:"aws_secret",type:"text",label:"AWS Secret (optional)",placeholder:"enter your AWS Secret with S3 permissions",columns:12,help:"Optional, AWS Secret with S3 permissions"}]}};let a=class extends e.Model{constructor(e={}){super(e,{endpoint:"/api/fileman/file"})}isImage(){return"image"===this.get("category")}upload(e={}){return new FileUpload(this,e)}};class FileList extends e.Collection{constructor(e={}){super({ModelClass:a,endpoint:"/api/fileman/file",size:10,...e})}}const l={create:{title:"Add File",fields:[]},edit:{title:"Edit File Backend",fields:[]}},r=/* @__PURE__ */Object.freeze(/* @__PURE__ */Object.defineProperty({__proto__:null,File:a,FileForms:l,FileList:FileList,FileManager:FileManager,FileManagerForms:i,FileManagerList:FileManagerList},Symbol.toStringTag,{value:"Module"}));exports.File=a,exports.FileForms=l,exports.FileList=FileList,exports.FileManager=FileManager,exports.FileManagerForms=i,exports.FileManagerList=FileManagerList,exports.FileUpload=FileUpload,exports.Files=r,exports.ProgressView=ProgressView;
|
|
2
|
+
//# sourceMappingURL=Files-Dh_5PFBn.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Files-DNbHDy43.js","sources":["../../src/core/views/feedback/ProgressView.js","../../src/core/services/FileUpload.js","../../src/core/models/Files.js"],"sourcesContent":["/**\n * ProgressView - File upload progress component\n * \n * Shows upload progress with progress bar, filename, and cancellation option\n * Integrates with FileUpload service for real-time progress updates\n * \n * Features:\n * - Bootstrap progress bar with percentage\n * - File information (name, size)\n * - Bytes uploaded/total display\n * - Cancel button with confirmation\n * - Responsive design\n * \n * Events:\n * - 'cancel' - Emitted when user cancels upload\n * \n * @example\n * const progressView = new ProgressView({\n * filename: 'document.pdf',\n * filesize: 1024000,\n * onCancel: () => fileUpload.cancel()\n * });\n * \n * // Update progress\n * progressView.updateProgress({ progress: 0.5, loaded: 512000, total: 1024000 });\n */\n\nimport View from '@core/View.js';\nimport dataFormatter from '@core/utils/DataFormatter.js';\n\nclass ProgressView extends View {\n constructor(options = {}) {\n super({\n template: 'progress-view-template',\n ...options\n });\n\n // Initialize progress data\n this.filename = options.filename || 'Unknown file';\n this.filesize = options.filesize || 0;\n this.filesizeFormatted = dataFormatter.pipe(this.filesize, 'filesize');\n \n // Progress state\n this.progress = 0;\n this.percentage = 0;\n this.loaded = 0;\n this.total = this.filesize;\n this.loadedFormatted = '0 B';\n this.totalFormatted = this.filesizeFormatted;\n this.status = 'Starting upload...';\n \n // Options\n this.showCancel = options.showCancel !== false;\n this.onCancel = options.onCancel || null;\n \n // State\n this.cancelled = false;\n this.completed = false;\n }\n\n /**\n * Get template for the progress view\n */\n getTemplate() {\n return `\n <div class=\"progress-view\">\n <div class=\"d-flex justify-content-between align-items-start mb-2\">\n <div class=\"flex-grow-1 min-width-0\">\n <div class=\"fw-medium text-truncate\" title=\"{{filename}}\">\n <i class=\"bi bi-file-earmark me-1\"></i>\n {{filename}}\n </div>\n <small class=\"text-muted\">{{status}}</small>\n </div>\n {{#showCancel}}\n <button type=\"button\" \n class=\"btn btn-sm btn-outline-secondary ms-2\" \n data-action=\"cancel\"\n {{#cancelled}}disabled{{/cancelled}}>\n <i class=\"bi bi-x\"></i>\n </button>\n {{/showCancel}}\n </div>\n \n <div class=\"progress mb-2\" style=\"height: 8px;\">\n <div class=\"progress-bar\" \n role=\"progressbar\" \n style=\"width: {{percentage}}%\"\n aria-valuenow=\"{{percentage}}\" \n aria-valuemin=\"0\" \n aria-valuemax=\"100\">\n </div>\n </div>\n \n <div class=\"d-flex justify-content-between\">\n <small class=\"text-muted\">\n {{loadedFormatted}} / {{totalFormatted}}\n </small>\n <small class=\"text-muted\">\n {{percentage}}%\n </small>\n </div>\n </div>\n `;\n }\n\n /**\n * Update progress information\n * @param {Object} progressInfo - Progress data\n * @param {number} progressInfo.progress - Progress as decimal (0-1)\n * @param {number} progressInfo.loaded - Bytes loaded\n * @param {number} progressInfo.total - Total bytes\n * @param {number} progressInfo.percentage - Progress as percentage (0-100)\n */\n updateProgress(progressInfo) {\n if (this.cancelled || this.completed) {\n return;\n }\n\n this.progress = progressInfo.progress;\n this.percentage = progressInfo.percentage;\n this.loaded = progressInfo.loaded;\n this.total = progressInfo.total || this.filesize;\n \n // Format bytes for display\n this.loadedFormatted = dataFormatter.pipe(this.loaded, 'filesize');\n this.totalFormatted = dataFormatter.pipe(this.total, 'filesize');\n \n // Update status message\n if (this.percentage < 100) {\n this.status = `Uploading... ${this.percentage}%`;\n } else {\n this.status = 'Finalizing upload...';\n }\n\n // Re-render to show updated progress\n this.render();\n }\n\n /**\n * Mark upload as completed\n * @param {string} message - Success message\n */\n markCompleted(message = 'Upload completed!') {\n this.completed = true;\n this.progress = 1;\n this.percentage = 100;\n this.status = message;\n this.render();\n }\n\n /**\n * Mark upload as failed\n * @param {string} message - Error message\n */\n markFailed(message = 'Upload failed') {\n this.status = message;\n this.render();\n }\n\n /**\n * Mark upload as cancelled\n */\n markCancelled() {\n this.cancelled = true;\n this.status = 'Upload cancelled';\n this.render();\n }\n\n /**\n * Handle cancel button click\n * @param {string} action - Action name\n * @param {Event} event - Click event\n * @param {Element} element - Button element\n */\n async onActionCancel(action, event, element) {\n if (this.cancelled || this.completed) {\n return;\n }\n\n // Disable button immediately\n element.disabled = true;\n \n // Mark as cancelled\n this.markCancelled();\n \n // Emit cancel event\n this.emit('cancel');\n \n // Call cancel callback if provided\n if (typeof this.onCancel === 'function') {\n try {\n await this.onCancel();\n } catch (error) {\n console.error('Error in cancel callback:', error);\n }\n }\n }\n\n /**\n * Set filename\n * @param {string} filename - New filename\n */\n setFilename(filename) {\n this.filename = filename;\n this.render();\n }\n\n /**\n * Set file size\n * @param {number} size - File size in bytes\n */\n setFilesize(size) {\n this.filesize = size;\n this.filesizeFormatted = dataFormatter.pipe(size, 'filesize');\n this.total = size;\n this.totalFormatted = this.filesizeFormatted;\n this.render();\n }\n\n /**\n * Get current progress as percentage\n * @returns {number} Progress percentage (0-100)\n */\n getPercentage() {\n return this.percentage;\n }\n\n /**\n * Check if upload is completed\n * @returns {boolean} True if completed\n */\n isCompleted() {\n return this.completed;\n }\n\n /**\n * Check if upload is cancelled\n * @returns {boolean} True if cancelled\n */\n isCancelled() {\n return this.cancelled;\n }\n\n /**\n * Get upload statistics\n * @returns {Object} Upload stats\n */\n getStats() {\n return {\n filename: this.filename,\n filesize: this.filesize,\n progress: this.progress,\n percentage: this.percentage,\n loaded: this.loaded,\n total: this.total,\n cancelled: this.cancelled,\n completed: this.completed,\n status: this.status\n };\n }\n}\n\nexport default ProgressView;","/**\n * FileUpload - File upload service with progress tracking and UI integration\n *\n * Features:\n * - Auto-start upload process\n * - Progress tracking with detailed information\n * - Promise interface with cancellation support\n * - Toast integration for user feedback\n * - Three-stage upload process (initiate → upload → complete)\n *\n * @example\n * const file = new File();\n * const upload = file.upload({\n * file: fileObject,\n * name: 'avatar.jpg',\n * group: 'profile-pics',\n * description: 'User avatar',\n * onProgress: ({ progress, loaded, total, percentage }) => {\n * console.log(`${percentage}% complete`);\n * }\n * });\n *\n * upload.then(result => console.log('Success!'))\n * .catch(error => console.error('Failed:', error));\n */\n\nimport ToastService from './ToastService.js';\nimport ProgressView from '../views/feedback/ProgressView.js';\n\nclass FileUpload {\n constructor(fileModel, options = {}) {\n this.fileModel = fileModel;\n this.options = {\n file: null,\n name: null,\n group: null,\n description: null,\n onProgress: null,\n onComplete: null,\n onError: null,\n showToast: true,\n ...options\n };\n\n // Validation\n if (!this.options.file || !(this.options.file instanceof File)) {\n throw new Error('FileUpload requires a valid File object');\n }\n\n // State management\n this.cancelled = false;\n this.uploadRequest = null;\n this.progressToast = null;\n this.progressView = null;\n this.toastService = null;\n\n // Initialize toast service if needed\n if (this.options.showToast) {\n this.toastService = new ToastService();\n }\n\n // Auto-start upload (Option 3 behavior)\n this.promise = this._startUpload();\n }\n\n /**\n * Main upload orchestration\n * @returns {Promise} Upload promise\n * @private\n */\n async _startUpload() {\n try {\n if (this.options.showToast) {\n this._showProgressToast();\n }\n\n // Stage 1: Initiate upload and get signed URL\n let uploadData;\n try {\n uploadData = await this._initiateUpload();\n } catch (error) {\n throw new Error(`Failed to initiate upload: ${error.message}`);\n }\n\n if (this.cancelled) {\n throw new Error('Upload cancelled');\n }\n\n // Validate upload data\n if (!uploadData || !uploadData.upload_url) {\n throw new Error('Invalid upload response: missing upload URL');\n }\n\n // Normalise upload_url — the backend can return either:\n // • a plain string → direct PUT with raw bytes (e.g. S3 signed URL)\n // • a config object → POST multipart/form-data (e.g. filesystem backend)\n // { upload_url: string, method: string, fields: object, headers: object }\n let uploadConfig;\n if (typeof uploadData.upload_url === 'string') {\n uploadConfig = {\n url: uploadData.upload_url,\n method: 'PUT',\n fields: null,\n headers: {}\n };\n } else if (uploadData.upload_url && typeof uploadData.upload_url === 'object'\n && uploadData.upload_url.upload_url) {\n uploadConfig = {\n url: uploadData.upload_url.upload_url,\n method: uploadData.upload_url.method || 'POST',\n fields: uploadData.upload_url.fields || null,\n headers: uploadData.upload_url.headers || {}\n };\n } else {\n throw new Error(\n `Invalid upload response: unrecognised upload_url format. ` +\n `Server returned: ${JSON.stringify(uploadData.upload_url)}`\n );\n }\n\n // Stage 2: Upload file to signed URL / endpoint\n let result;\n try {\n result = await this._performUpload(uploadConfig);\n } catch (error) {\n throw new Error(`File upload failed: ${error.message}`);\n }\n\n if (this.cancelled) {\n throw new Error('Upload cancelled');\n }\n\n // Stage 3: Mark upload as completed\n try {\n await this._completeUpload();\n } catch (error) {\n console.warn('Failed to mark upload as completed:', error);\n // Don't fail the entire upload for completion marking errors\n // The file was successfully uploaded\n }\n\n // Handle success\n this._onComplete(this.fileModel);\n return this.fileModel;\n\n } catch (error) {\n if (error.message !== 'Upload cancelled') {\n this._onError(error);\n }\n throw error;\n }\n }\n\n /**\n * Initiate upload by calling the API to get signed URL\n * @returns {Promise<Object>} Upload initiation data\n * @private\n */\n async _initiateUpload() {\n try {\n const payload = {\n filename: this.options.name || this.options.file.name,\n file_size: this.options.file.size,\n content_type: this.options.file.type,\n };\n\n if (this.options.group) payload.group = this.options.group;\n if (this.options.description) payload.description = this.options.description;\n\n const response = await this.fileModel.rest.POST('/api/fileman/upload/initiate', payload);\n\n if (!response) {\n throw new Error('No response from upload initiation API');\n }\n\n if (!response.data) {\n throw new Error('Upload initiation response missing data');\n }\n\n // Check server response first (prefer server error messages)\n if (!response.data.status) {\n const errorMessage = response.data.error || 'Upload initiation failed';\n throw new Error(errorMessage);\n }\n\n if (!response.data.data) {\n throw new Error('Upload initiation response missing data payload');\n }\n\n // Set model ID for completion step\n if (response.data.data.id) {\n this.fileModel.set('id', response.data.data.id);\n }\n\n return response.data.data; // { id, upload_url }\n\n } catch (error) {\n // Re-throw with more context if it's a generic error\n if (error.message === 'Network Error' || error.name === 'TypeError') {\n throw new Error('Network error during upload initiation. Please check your connection.');\n }\n throw error;\n }\n }\n\n /**\n * Upload file using the normalised upload config from _startUpload.\n *\n * Two dispatch paths:\n * - PUT + raw bytes → legacy plain-string upload_url, or any config with method PUT.\n * Used by backends that issue a direct signed PUT URL (e.g. S3 signed URL).\n * - POST + FormData → config object with method POST and optional fields dict.\n * Used by the local filesystem backend and S3 presigned POST.\n * fields are appended first, then the file as the \"file\" key,\n * matching Django's request.FILES['file'] convention.\n * Content-Type is intentionally NOT set manually so the browser\n * can write the correct multipart boundary.\n *\n * @param {{ url: string, method: string, fields: object|null, headers: object }} uploadConfig\n * @returns {Promise} Upload result\n * @private\n */\n async _performUpload(uploadConfig) {\n return new Promise((resolve, reject) => {\n if (!(this.options.file instanceof File)) {\n reject(new Error('Only single File objects are supported'));\n return;\n }\n\n const { url, method, fields, headers } = uploadConfig;\n const useFormData = method === 'POST' && fields !== null;\n\n const xhr = new XMLHttpRequest();\n this.uploadRequest = xhr;\n\n // Progress tracking\n xhr.upload.onprogress = (event) => {\n if (this.cancelled) return;\n this._onProgress({\n progress: event.loaded / event.total,\n loaded: event.loaded,\n total: event.total,\n percentage: Math.round((event.loaded / event.total) * 100)\n });\n };\n\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve({ data: xhr.response, status: xhr.status, statusText: xhr.statusText, xhr });\n } else {\n reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));\n }\n };\n\n xhr.onerror = () => reject(new Error('Upload failed: Network error'));\n xhr.ontimeout = () => reject(new Error('Upload timed out — file may be too large or connection too slow'));\n xhr.onabort = () => reject(new Error('Upload cancelled'));\n\n // Relative URLs must be prefixed with /api/ so Django's URL router picks them\n // up correctly, then resolved through the REST service so they target the\n // configured API host rather than the browser's current web host.\n // Absolute URLs (http/https) are returned unchanged — needed for S3 etc.\n let apiUrl = url;\n if (url.startsWith('/') && !url.startsWith('/api/')) {\n apiUrl = '/api' + url;\n }\n const resolvedUrl = this.fileModel.rest.buildUrl(apiUrl);\n xhr.open(method, resolvedUrl);\n xhr.timeout = 30000;\n\n if (useFormData) {\n // POST multipart/form-data — filesystem backend and S3 presigned POST.\n // Any extra headers from the config (e.g. x-amz-* for S3) are set here,\n // but Content-Type is deliberately skipped so the browser sets the boundary.\n for (const [key, value] of Object.entries(headers || {})) {\n if (key.toLowerCase() !== 'content-type') {\n xhr.setRequestHeader(key, value);\n }\n }\n\n const formData = new FormData();\n for (const [key, value] of Object.entries(fields)) {\n formData.append(key, value);\n }\n // File must be last — required by S3 presigned POST policy; harmless for Django.\n formData.append('file', this.options.file);\n xhr.send(formData);\n\n } else {\n // PUT raw bytes — legacy / direct signed URL path.\n xhr.setRequestHeader('Content-Type', this.options.file.type);\n for (const [key, value] of Object.entries(headers || {})) {\n if (key.toLowerCase() !== 'content-type') {\n xhr.setRequestHeader(key, value);\n }\n }\n xhr.send(this.options.file);\n }\n });\n }\n\n /**\n * Mark upload as completed in the API\n * @returns {Promise} Completion result\n * @private\n */\n async _completeUpload() {\n try {\n const response = await this.fileModel.save({ action: 'mark_as_completed' });\n\n if (!response) {\n throw new Error('No response from upload completion API');\n }\n\n // Check server response format (prefer server errors over HTTP errors)\n if (response.data && !response.data.status) {\n const errorMessage = response.data.error || 'Failed to mark upload as completed';\n throw new Error(errorMessage);\n }\n\n return response;\n } catch (error) {\n // Re-throw with more context\n if (error.message === 'Network Error' || error.name === 'TypeError') {\n throw new Error('Network error during upload completion. The file may have uploaded successfully.');\n }\n throw error;\n }\n }\n\n /**\n * Handle progress updates\n * @param {Object} progressInfo - Progress information\n * @private\n */\n _onProgress(progressInfo) {\n // Update progress toast if shown\n if (this.progressToast && this.progressToast.updateProgress) {\n this.progressToast.updateProgress(progressInfo);\n }\n\n // Call user-provided progress callback\n if (typeof this.options.onProgress === 'function') {\n this.options.onProgress(progressInfo);\n }\n }\n\n /**\n * Handle successful upload completion\n * @param {Object} result - Upload result\n * @private\n */\n _onComplete(result) {\n // Mark progress view as completed\n if (this.progressView) {\n this.progressView.markCompleted('Upload completed successfully!');\n }\n\n // Auto-hide progress toast after a delay\n if (this.progressToast) {\n setTimeout(() => {\n try {\n if (this.progressToast && typeof this.progressToast.hide === 'function') {\n this.progressToast.hide();\n }\n } catch (error) {\n console.warn('Error hiding progress toast:', error);\n }\n }, 2000);\n }\n\n // Call user-provided completion callback\n if (typeof this.options.onComplete === 'function') {\n this.options.onComplete(result);\n }\n }\n\n /**\n * Handle upload errors\n * @param {Error} error - Error object\n * @private\n */\n _onError(error) {\n // Hide progress toast immediately and show error toast\n if (this.progressToast) {\n try {\n this.progressToast.hide();\n } catch (error) {\n console.warn('Error hiding progress toast on error:', error);\n }\n }\n\n // Show error toast with immediate feedback\n if (this.toastService) {\n this.toastService.error(`Upload failed: ${error.message}`);\n }\n\n // Call user-provided error callback\n if (typeof this.options.onError === 'function') {\n this.options.onError(error);\n }\n }\n\n /**\n * Show progress toast with ProgressView component\n * @private\n */\n _showProgressToast() {\n // Create progress view with file information\n this.progressView = new ProgressView({\n filename: this.options.name || this.options.file.name,\n filesize: this.options.file.size,\n showCancel: true,\n onCancel: () => this.cancel()\n });\n\n // Show progress view in toast\n this.progressToast = this.toastService.showView(this.progressView, 'info', {\n title: 'File Upload',\n autohide: false,\n dismissible: false\n });\n }\n\n /**\n * Cancel the upload\n * @returns {boolean} True if cancelled, false if already completed\n */\n cancel() {\n if (this.cancelled) {\n return false;\n }\n\n this.cancelled = true;\n\n // Cancel the upload request if in progress\n if (this.uploadRequest && typeof this.uploadRequest.abort === 'function') {\n this.uploadRequest.abort();\n }\n\n // Mark progress view as cancelled\n if (this.progressView) {\n this.progressView.markCancelled();\n }\n\n // Hide progress toast after a delay\n if (this.progressToast) {\n setTimeout(() => {\n try {\n if (this.progressToast && typeof this.progressToast.hide === 'function') {\n this.progressToast.hide();\n }\n } catch (error) {\n console.warn('Error hiding progress toast on cancel:', error);\n }\n }, 1500);\n }\n\n return true;\n }\n\n /**\n * Check if upload is cancelled\n * @returns {boolean} True if cancelled\n */\n isCancelled() {\n return this.cancelled;\n }\n\n /**\n * Promise interface - then\n * @param {function} onSuccess - Success handler\n * @param {function} onError - Error handler\n * @returns {Promise} Promise chain\n */\n then(onSuccess, onError) {\n return this.promise.then(onSuccess, onError);\n }\n\n /**\n * Promise interface - catch\n * @param {function} onError - Error handler\n * @returns {Promise} Promise chain\n */\n catch(onError) {\n return this.promise.catch(onError);\n }\n\n /**\n * Promise interface - finally\n * @param {function} onFinally - Finally handler\n * @returns {Promise} Promise chain\n */\n finally(onFinally) {\n return this.promise.finally(onFinally);\n }\n\n /**\n * Get upload statistics\n * @returns {Object} Upload stats\n */\n getStats() {\n return {\n filename: this.options.file.name,\n size: this.options.file.size,\n type: this.options.file.type,\n cancelled: this.cancelled,\n group: this.options.group,\n description: this.options.description\n };\n }\n}\n\nexport default FileUpload;\n","\nimport Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\nimport FileUpload from '@core/services/FileUpload.js';\nimport {UserList} from '@core/models/User.js';\nimport {GroupList} from '@core/models/Group.js';\n/* =========================\n * FileManager\n * ========================= */\nclass FileManager extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/fileman/manager',\n });\n }\n}\n\nclass FileManagerList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: FileManager,\n endpoint: '/api/fileman/manager',\n size: 10,\n ...options,\n });\n }\n}\n\nconst FileManagerForms = {\n create: {\n title: 'Add Storage Backend',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Display Name',\n placeholder: 'Enter Display Name',\n cols: 12,\n },\n {\n name: 'use',\n type: 'text',\n label: 'Use',\n placeholder: 'Enter User or Leave Blank',\n cols: 12,\n },\n {\n name: 'backend_url',\n type: 'text',\n label: 'Backend URL',\n required: true,\n value: \"s3://BUCKET_NAME/OPTION_FOLDER\",\n placeholder: 's3://bucket_name/optional folder',\n help: 'Format: service://path. Valid services: s3',\n cols: 12,\n },\n {\n name: 'aws_region',\n type: 'select',\n label: 'AWS Region (optional)',\n value: 'us-east-1',\n options: [\n { value: '', text: 'System Default' },\n { value: 'us-east-1', text: 'US East (N. Virginia)' },\n { value: 'us-east-2', text: 'US East (Ohio)' },\n { value: 'us-west-1', text: 'US West (N. California)' },\n { value: 'us-west-2', text: 'US West (Oregon)' },\n { value: 'ca-central-1', text: 'Canada (Central)' },\n { value: 'eu-west-1', text: 'Europe (Ireland)' },\n { value: 'eu-west-2', text: 'Europe (London)' },\n { value: 'eu-west-3', text: 'Europe (Paris)' },\n { value: 'eu-central-1', text: 'Europe (Frankfurt)' },\n { value: 'eu-north-1', text: 'Europe (Stockholm)' },\n { value: 'eu-south-1', text: 'Europe (Milan)' },\n { value: 'ap-southeast-2', text: 'Asia Pacific (Sydney)' }\n ],\n columns: 12,\n help: 'Optional. Defaults to project AWS_REGION if omitted.'\n },\n {\n name: 'aws_key',\n type: 'text',\n label: 'AWS Key (optional)',\n placeholder: 'enter your AWS Key with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Key with S3 permissions'\n },\n {\n name: 'aws_secret',\n type: 'text',\n label: 'AWS Secret (optional)',\n placeholder: 'enter your AWS Secret with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Secret with S3 permissions'\n },\n {\n name: 'is_default',\n type: 'switch',\n label: 'Is Default',\n cols: 6,\n },\n {\n name: 'is_active',\n type: 'switch',\n label: 'Is Active',\n default: true,\n cols: 6,\n },\n ],\n },\n\n edit: {\n title: 'Edit Storage Backend',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Display Name',\n placeholder: 'Enter Display Name',\n cols: 12,\n },\n {\n name: 'use',\n type: 'text',\n label: 'Use',\n placeholder: 'Enter User or Leave Blank',\n cols: 12,\n },\n {\n name: 'backend_url',\n type: 'text',\n label: 'Backend URL',\n required: true,\n placeholder: 's3://bucket_name/optional folder',\n help: 'Format: service://path. Valid services: s3',\n cols: 12,\n },\n {\n name: 'allowed_origins',\n type: 'text',\n label: 'Domains Who Can Upload',\n cols: 12,\n },\n {\n name: 'is_default',\n type: 'switch',\n label: 'Is Default',\n cols: 6,\n },\n {\n name: 'is_active',\n type: 'switch',\n label: 'Is Active',\n default: true,\n cols: 6,\n },\n {\n name: 'is_public',\n type: 'switch',\n label: 'Is Public',\n default: true,\n cols: 6,\n }\n ],\n },\n\n owners: {\n fields: [\n {\n type: 'collection',\n name: 'group',\n label: 'Group (Owner)',\n Collection: GroupList, // Collection class\n labelField: 'name', // Field to display in dropdown\n valueField: 'id', // Field to use as value\n maxItems: 10, // Max items to show in dropdown\n placeholder: 'Search groups...',\n emptyFetch: false,\n debounceMs: 300, // Search debounce delay\n },\n {\n type: 'collection',\n name: 'user',\n label: 'User (Owner)',\n Collection: UserList, // Collection class\n labelField: 'display_name', // Field to display in dropdown\n valueField: 'id', // Field to use as value\n maxItems: 10, // Max items to show in dropdown\n placeholder: 'Search users...',\n emptyFetch: false,\n debounceMs: 300, // Search debounce delay\n },\n ],\n },\n\n credentials: {\n fields: [\n {\n name: 'aws_region',\n type: 'select',\n label: 'AWS Region (optional)',\n value: 'us-east-1',\n options: [\n { value: '', text: 'System Default' },\n { value: 'us-east-1', text: 'US East (N. Virginia)' },\n { value: 'us-east-2', text: 'US East (Ohio)' },\n { value: 'us-west-1', text: 'US West (N. California)' },\n { value: 'us-west-2', text: 'US West (Oregon)' },\n { value: 'ca-central-1', text: 'Canada (Central)' },\n { value: 'eu-west-1', text: 'Europe (Ireland)' },\n { value: 'eu-west-2', text: 'Europe (London)' },\n { value: 'eu-west-3', text: 'Europe (Paris)' },\n { value: 'eu-central-1', text: 'Europe (Frankfurt)' },\n { value: 'eu-north-1', text: 'Europe (Stockholm)' },\n { value: 'eu-south-1', text: 'Europe (Milan)' },\n { value: 'ap-southeast-2', text: 'Asia Pacific (Sydney)' }\n ],\n columns: 12,\n help: 'Optional. Defaults to project AWS_REGION if omitted.'\n },\n {\n name: 'aws_key',\n type: 'text',\n label: 'AWS Key (optional)',\n placeholder: 'enter your AWS Key with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Key with S3 permissions'\n },\n {\n name: 'aws_secret',\n type: 'text',\n label: 'AWS Secret (optional)',\n placeholder: 'enter your AWS Secret with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Secret with S3 permissions'\n },\n ]\n }\n};\n\n/* =========================\n * File\n * ========================= */\nclass File extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/fileman/file',\n });\n }\n\n isImage() {\n return this.get(\"category\") === 'image';\n }\n\n /**\n * Upload file with progress tracking and UI integration\n * Returns a FileUpload instance with promise interface and cancellation support\n *\n * @param {object} options - Upload configuration\n * @param {File} options.file - File object to upload\n * @param {string} options.name - Custom filename (optional)\n * @param {string} options.group - File group/category (optional)\n * @param {string} options.description - File description (optional)\n * @param {function} options.onProgress - Progress callback ({ progress, loaded, total, percentage })\n * @param {function} options.onComplete - Success callback\n * @param {function} options.onError - Error callback\n * @param {boolean} options.showToast - Show progress toast (default: true)\n * @returns {FileUpload} Upload instance with promise interface\n */\n upload(options = {}) {\n return new FileUpload(this, options);\n }\n}\n\nclass FileList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: File,\n endpoint: '/api/fileman/file',\n size: 10,\n ...options,\n });\n }\n}\n\nconst FileForms = {\n create: {\n title: 'Add File',\n fields: [\n\n ],\n },\n\n edit: {\n title: 'Edit File Backend',\n fields: [\n\n ],\n },\n};\n\nexport {\n FileManager,\n FileManagerList,\n FileManagerForms,\n File,\n FileList,\n FileForms,\n};\n"],"names":["ProgressView","View","constructor","options","super","template","this","filename","filesize","filesizeFormatted","dataFormatter","pipe","progress","percentage","loaded","total","loadedFormatted","totalFormatted","status","showCancel","onCancel","cancelled","completed","getTemplate","updateProgress","progressInfo","render","markCompleted","message","markFailed","markCancelled","onActionCancel","action","event","element","disabled","emit","error","console","setFilename","setFilesize","size","getPercentage","isCompleted","isCancelled","getStats","FileUpload","fileModel","file","name","group","description","onProgress","onComplete","onError","showToast","File","Error","uploadRequest","progressToast","progressView","toastService","ToastService","promise","_startUpload","uploadData","uploadConfig","result","_showProgressToast","_initiateUpload","upload_url","url","method","fields","headers","JSON","stringify","_performUpload","_completeUpload","warn","_onComplete","_onError","payload","file_size","content_type","type","response","rest","POST","data","errorMessage","id","set","Promise","resolve","reject","useFormData","xhr","XMLHttpRequest","upload","onprogress","_onProgress","Math","round","onload","statusText","onerror","ontimeout","onabort","apiUrl","startsWith","resolvedUrl","buildUrl","open","timeout","key","value","Object","entries","toLowerCase","setRequestHeader","formData","FormData","append","send","save","setTimeout","hide","cancel","showView","title","autohide","dismissible","abort","then","onSuccess","catch","onFinally","finally","FileManager","Model","endpoint","FileManagerList","Collection","ModelClass","FileManagerForms","create","label","placeholder","cols","required","help","text","columns","default","edit","owners","GroupList","labelField","valueField","maxItems","emptyFetch","debounceMs","UserList","credentials","isImage","get","FileList","FileForms"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Files-Dh_5PFBn.js","sources":["../../src/core/views/feedback/ProgressView.js","../../src/core/services/FileUpload.js","../../src/core/models/Files.js"],"sourcesContent":["/**\n * ProgressView - File upload progress component\n * \n * Shows upload progress with progress bar, filename, and cancellation option\n * Integrates with FileUpload service for real-time progress updates\n * \n * Features:\n * - Bootstrap progress bar with percentage\n * - File information (name, size)\n * - Bytes uploaded/total display\n * - Cancel button with confirmation\n * - Responsive design\n * \n * Events:\n * - 'cancel' - Emitted when user cancels upload\n * \n * @example\n * const progressView = new ProgressView({\n * filename: 'document.pdf',\n * filesize: 1024000,\n * onCancel: () => fileUpload.cancel()\n * });\n * \n * // Update progress\n * progressView.updateProgress({ progress: 0.5, loaded: 512000, total: 1024000 });\n */\n\nimport View from '@core/View.js';\nimport dataFormatter from '@core/utils/DataFormatter.js';\n\nclass ProgressView extends View {\n constructor(options = {}) {\n super({\n template: 'progress-view-template',\n ...options\n });\n\n // Initialize progress data\n this.filename = options.filename || 'Unknown file';\n this.filesize = options.filesize || 0;\n this.filesizeFormatted = dataFormatter.pipe(this.filesize, 'filesize');\n \n // Progress state\n this.progress = 0;\n this.percentage = 0;\n this.loaded = 0;\n this.total = this.filesize;\n this.loadedFormatted = '0 B';\n this.totalFormatted = this.filesizeFormatted;\n this.status = 'Starting upload...';\n \n // Options\n this.showCancel = options.showCancel !== false;\n this.onCancel = options.onCancel || null;\n \n // State\n this.cancelled = false;\n this.completed = false;\n }\n\n /**\n * Get template for the progress view\n */\n getTemplate() {\n return `\n <div class=\"progress-view\">\n <div class=\"d-flex justify-content-between align-items-start mb-2\">\n <div class=\"flex-grow-1 min-width-0\">\n <div class=\"fw-medium text-truncate\" title=\"{{filename}}\">\n <i class=\"bi bi-file-earmark me-1\"></i>\n {{filename}}\n </div>\n <small class=\"text-muted\">{{status}}</small>\n </div>\n {{#showCancel}}\n <button type=\"button\" \n class=\"btn btn-sm btn-outline-secondary ms-2\" \n data-action=\"cancel\"\n {{#cancelled}}disabled{{/cancelled}}>\n <i class=\"bi bi-x\"></i>\n </button>\n {{/showCancel}}\n </div>\n \n <div class=\"progress mb-2\" style=\"height: 8px;\">\n <div class=\"progress-bar\" \n role=\"progressbar\" \n style=\"width: {{percentage}}%\"\n aria-valuenow=\"{{percentage}}\" \n aria-valuemin=\"0\" \n aria-valuemax=\"100\">\n </div>\n </div>\n \n <div class=\"d-flex justify-content-between\">\n <small class=\"text-muted\">\n {{loadedFormatted}} / {{totalFormatted}}\n </small>\n <small class=\"text-muted\">\n {{percentage}}%\n </small>\n </div>\n </div>\n `;\n }\n\n /**\n * Update progress information\n * @param {Object} progressInfo - Progress data\n * @param {number} progressInfo.progress - Progress as decimal (0-1)\n * @param {number} progressInfo.loaded - Bytes loaded\n * @param {number} progressInfo.total - Total bytes\n * @param {number} progressInfo.percentage - Progress as percentage (0-100)\n */\n updateProgress(progressInfo) {\n if (this.cancelled || this.completed) {\n return;\n }\n\n this.progress = progressInfo.progress;\n this.percentage = progressInfo.percentage;\n this.loaded = progressInfo.loaded;\n this.total = progressInfo.total || this.filesize;\n \n // Format bytes for display\n this.loadedFormatted = dataFormatter.pipe(this.loaded, 'filesize');\n this.totalFormatted = dataFormatter.pipe(this.total, 'filesize');\n \n // Update status message\n if (this.percentage < 100) {\n this.status = `Uploading... ${this.percentage}%`;\n } else {\n this.status = 'Finalizing upload...';\n }\n\n // Re-render to show updated progress\n this.render();\n }\n\n /**\n * Mark upload as completed\n * @param {string} message - Success message\n */\n markCompleted(message = 'Upload completed!') {\n this.completed = true;\n this.progress = 1;\n this.percentage = 100;\n this.status = message;\n this.render();\n }\n\n /**\n * Mark upload as failed\n * @param {string} message - Error message\n */\n markFailed(message = 'Upload failed') {\n this.status = message;\n this.render();\n }\n\n /**\n * Mark upload as cancelled\n */\n markCancelled() {\n this.cancelled = true;\n this.status = 'Upload cancelled';\n this.render();\n }\n\n /**\n * Handle cancel button click\n * @param {string} action - Action name\n * @param {Event} event - Click event\n * @param {Element} element - Button element\n */\n async onActionCancel(action, event, element) {\n if (this.cancelled || this.completed) {\n return;\n }\n\n // Disable button immediately\n element.disabled = true;\n \n // Mark as cancelled\n this.markCancelled();\n \n // Emit cancel event\n this.emit('cancel');\n \n // Call cancel callback if provided\n if (typeof this.onCancel === 'function') {\n try {\n await this.onCancel();\n } catch (error) {\n console.error('Error in cancel callback:', error);\n }\n }\n }\n\n /**\n * Set filename\n * @param {string} filename - New filename\n */\n setFilename(filename) {\n this.filename = filename;\n this.render();\n }\n\n /**\n * Set file size\n * @param {number} size - File size in bytes\n */\n setFilesize(size) {\n this.filesize = size;\n this.filesizeFormatted = dataFormatter.pipe(size, 'filesize');\n this.total = size;\n this.totalFormatted = this.filesizeFormatted;\n this.render();\n }\n\n /**\n * Get current progress as percentage\n * @returns {number} Progress percentage (0-100)\n */\n getPercentage() {\n return this.percentage;\n }\n\n /**\n * Check if upload is completed\n * @returns {boolean} True if completed\n */\n isCompleted() {\n return this.completed;\n }\n\n /**\n * Check if upload is cancelled\n * @returns {boolean} True if cancelled\n */\n isCancelled() {\n return this.cancelled;\n }\n\n /**\n * Get upload statistics\n * @returns {Object} Upload stats\n */\n getStats() {\n return {\n filename: this.filename,\n filesize: this.filesize,\n progress: this.progress,\n percentage: this.percentage,\n loaded: this.loaded,\n total: this.total,\n cancelled: this.cancelled,\n completed: this.completed,\n status: this.status\n };\n }\n}\n\nexport default ProgressView;","/**\n * FileUpload - File upload service with progress tracking and UI integration\n *\n * Features:\n * - Auto-start upload process\n * - Progress tracking with detailed information\n * - Promise interface with cancellation support\n * - Toast integration for user feedback\n * - Three-stage upload process (initiate → upload → complete)\n *\n * @example\n * const file = new File();\n * const upload = file.upload({\n * file: fileObject,\n * name: 'avatar.jpg',\n * group: 'profile-pics',\n * description: 'User avatar',\n * onProgress: ({ progress, loaded, total, percentage }) => {\n * console.log(`${percentage}% complete`);\n * }\n * });\n *\n * upload.then(result => console.log('Success!'))\n * .catch(error => console.error('Failed:', error));\n */\n\nimport ToastService from './ToastService.js';\nimport ProgressView from '../views/feedback/ProgressView.js';\n\nclass FileUpload {\n constructor(fileModel, options = {}) {\n this.fileModel = fileModel;\n this.options = {\n file: null,\n name: null,\n group: null,\n description: null,\n onProgress: null,\n onComplete: null,\n onError: null,\n showToast: true,\n ...options\n };\n\n // Validation\n if (!this.options.file || !(this.options.file instanceof File)) {\n throw new Error('FileUpload requires a valid File object');\n }\n\n // State management\n this.cancelled = false;\n this.uploadRequest = null;\n this.progressToast = null;\n this.progressView = null;\n this.toastService = null;\n\n // Initialize toast service if needed\n if (this.options.showToast) {\n this.toastService = new ToastService();\n }\n\n // Auto-start upload (Option 3 behavior)\n this.promise = this._startUpload();\n }\n\n /**\n * Main upload orchestration\n * @returns {Promise} Upload promise\n * @private\n */\n async _startUpload() {\n try {\n if (this.options.showToast) {\n this._showProgressToast();\n }\n\n // Stage 1: Initiate upload and get signed URL\n let uploadData;\n try {\n uploadData = await this._initiateUpload();\n } catch (error) {\n throw new Error(`Failed to initiate upload: ${error.message}`);\n }\n\n if (this.cancelled) {\n throw new Error('Upload cancelled');\n }\n\n // Validate upload data\n if (!uploadData || !uploadData.upload_url) {\n throw new Error('Invalid upload response: missing upload URL');\n }\n\n // Normalise upload_url — the backend can return either:\n // • a plain string → direct PUT with raw bytes (e.g. S3 signed URL)\n // • a config object → POST multipart/form-data (e.g. filesystem backend)\n // { upload_url: string, method: string, fields: object, headers: object }\n let uploadConfig;\n if (typeof uploadData.upload_url === 'string') {\n uploadConfig = {\n url: uploadData.upload_url,\n method: 'PUT',\n fields: null,\n headers: {}\n };\n } else if (uploadData.upload_url && typeof uploadData.upload_url === 'object'\n && uploadData.upload_url.upload_url) {\n uploadConfig = {\n url: uploadData.upload_url.upload_url,\n method: uploadData.upload_url.method || 'POST',\n fields: uploadData.upload_url.fields || null,\n headers: uploadData.upload_url.headers || {}\n };\n } else {\n throw new Error(\n `Invalid upload response: unrecognised upload_url format. ` +\n `Server returned: ${JSON.stringify(uploadData.upload_url)}`\n );\n }\n\n // Stage 2: Upload file to signed URL / endpoint\n let result;\n try {\n result = await this._performUpload(uploadConfig);\n } catch (error) {\n throw new Error(`File upload failed: ${error.message}`);\n }\n\n if (this.cancelled) {\n throw new Error('Upload cancelled');\n }\n\n // Stage 3: Mark upload as completed\n try {\n await this._completeUpload();\n } catch (error) {\n console.warn('Failed to mark upload as completed:', error);\n // Don't fail the entire upload for completion marking errors\n // The file was successfully uploaded\n }\n\n // Handle success\n this._onComplete(this.fileModel);\n return this.fileModel;\n\n } catch (error) {\n if (error.message !== 'Upload cancelled') {\n this._onError(error);\n }\n throw error;\n }\n }\n\n /**\n * Initiate upload by calling the API to get signed URL\n * @returns {Promise<Object>} Upload initiation data\n * @private\n */\n async _initiateUpload() {\n try {\n const payload = {\n filename: this.options.name || this.options.file.name,\n file_size: this.options.file.size,\n content_type: this.options.file.type,\n };\n\n if (this.options.group) payload.group = this.options.group;\n if (this.options.description) payload.description = this.options.description;\n\n const response = await this.fileModel.rest.POST('/api/fileman/upload/initiate', payload);\n\n if (!response) {\n throw new Error('No response from upload initiation API');\n }\n\n if (!response.data) {\n throw new Error('Upload initiation response missing data');\n }\n\n // Check server response first (prefer server error messages)\n if (!response.data.status) {\n const errorMessage = response.data.error || 'Upload initiation failed';\n throw new Error(errorMessage);\n }\n\n if (!response.data.data) {\n throw new Error('Upload initiation response missing data payload');\n }\n\n // Set model ID for completion step\n if (response.data.data.id) {\n this.fileModel.set('id', response.data.data.id);\n }\n\n return response.data.data; // { id, upload_url }\n\n } catch (error) {\n // Re-throw with more context if it's a generic error\n if (error.message === 'Network Error' || error.name === 'TypeError') {\n throw new Error('Network error during upload initiation. Please check your connection.');\n }\n throw error;\n }\n }\n\n /**\n * Upload file using the normalised upload config from _startUpload.\n *\n * Two dispatch paths:\n * - PUT + raw bytes → legacy plain-string upload_url, or any config with method PUT.\n * Used by backends that issue a direct signed PUT URL (e.g. S3 signed URL).\n * - POST + FormData → config object with method POST and optional fields dict.\n * Used by the local filesystem backend and S3 presigned POST.\n * fields are appended first, then the file as the \"file\" key,\n * matching Django's request.FILES['file'] convention.\n * Content-Type is intentionally NOT set manually so the browser\n * can write the correct multipart boundary.\n *\n * @param {{ url: string, method: string, fields: object|null, headers: object }} uploadConfig\n * @returns {Promise} Upload result\n * @private\n */\n async _performUpload(uploadConfig) {\n return new Promise((resolve, reject) => {\n if (!(this.options.file instanceof File)) {\n reject(new Error('Only single File objects are supported'));\n return;\n }\n\n const { url, method, fields, headers } = uploadConfig;\n const useFormData = method === 'POST' && fields !== null;\n\n const xhr = new XMLHttpRequest();\n this.uploadRequest = xhr;\n\n // Progress tracking\n xhr.upload.onprogress = (event) => {\n if (this.cancelled) return;\n this._onProgress({\n progress: event.loaded / event.total,\n loaded: event.loaded,\n total: event.total,\n percentage: Math.round((event.loaded / event.total) * 100)\n });\n };\n\n xhr.onload = () => {\n if (xhr.status >= 200 && xhr.status < 300) {\n resolve({ data: xhr.response, status: xhr.status, statusText: xhr.statusText, xhr });\n } else {\n reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));\n }\n };\n\n xhr.onerror = () => reject(new Error('Upload failed: Network error'));\n xhr.ontimeout = () => reject(new Error('Upload timed out — file may be too large or connection too slow'));\n xhr.onabort = () => reject(new Error('Upload cancelled'));\n\n // Relative URLs must be prefixed with /api/ so Django's URL router picks them\n // up correctly, then resolved through the REST service so they target the\n // configured API host rather than the browser's current web host.\n // Absolute URLs (http/https) are returned unchanged — needed for S3 etc.\n let apiUrl = url;\n if (url.startsWith('/') && !url.startsWith('/api/')) {\n apiUrl = '/api' + url;\n }\n const resolvedUrl = this.fileModel.rest.buildUrl(apiUrl);\n xhr.open(method, resolvedUrl);\n xhr.timeout = 30000;\n\n if (useFormData) {\n // POST multipart/form-data — filesystem backend and S3 presigned POST.\n // Any extra headers from the config (e.g. x-amz-* for S3) are set here,\n // but Content-Type is deliberately skipped so the browser sets the boundary.\n for (const [key, value] of Object.entries(headers || {})) {\n if (key.toLowerCase() !== 'content-type') {\n xhr.setRequestHeader(key, value);\n }\n }\n\n const formData = new FormData();\n for (const [key, value] of Object.entries(fields)) {\n formData.append(key, value);\n }\n // File must be last — required by S3 presigned POST policy; harmless for Django.\n formData.append('file', this.options.file);\n xhr.send(formData);\n\n } else {\n // PUT raw bytes — legacy / direct signed URL path.\n xhr.setRequestHeader('Content-Type', this.options.file.type);\n for (const [key, value] of Object.entries(headers || {})) {\n if (key.toLowerCase() !== 'content-type') {\n xhr.setRequestHeader(key, value);\n }\n }\n xhr.send(this.options.file);\n }\n });\n }\n\n /**\n * Mark upload as completed in the API\n * @returns {Promise} Completion result\n * @private\n */\n async _completeUpload() {\n try {\n const response = await this.fileModel.save({ action: 'mark_as_completed' });\n\n if (!response) {\n throw new Error('No response from upload completion API');\n }\n\n // Check server response format (prefer server errors over HTTP errors)\n if (response.data && !response.data.status) {\n const errorMessage = response.data.error || 'Failed to mark upload as completed';\n throw new Error(errorMessage);\n }\n\n return response;\n } catch (error) {\n // Re-throw with more context\n if (error.message === 'Network Error' || error.name === 'TypeError') {\n throw new Error('Network error during upload completion. The file may have uploaded successfully.');\n }\n throw error;\n }\n }\n\n /**\n * Handle progress updates\n * @param {Object} progressInfo - Progress information\n * @private\n */\n _onProgress(progressInfo) {\n // Update progress toast if shown\n if (this.progressToast && this.progressToast.updateProgress) {\n this.progressToast.updateProgress(progressInfo);\n }\n\n // Call user-provided progress callback\n if (typeof this.options.onProgress === 'function') {\n this.options.onProgress(progressInfo);\n }\n }\n\n /**\n * Handle successful upload completion\n * @param {Object} result - Upload result\n * @private\n */\n _onComplete(result) {\n // Mark progress view as completed\n if (this.progressView) {\n this.progressView.markCompleted('Upload completed successfully!');\n }\n\n // Auto-hide progress toast after a delay\n if (this.progressToast) {\n setTimeout(() => {\n try {\n if (this.progressToast && typeof this.progressToast.hide === 'function') {\n this.progressToast.hide();\n }\n } catch (error) {\n console.warn('Error hiding progress toast:', error);\n }\n }, 2000);\n }\n\n // Call user-provided completion callback\n if (typeof this.options.onComplete === 'function') {\n this.options.onComplete(result);\n }\n }\n\n /**\n * Handle upload errors\n * @param {Error} error - Error object\n * @private\n */\n _onError(error) {\n // Hide progress toast immediately and show error toast\n if (this.progressToast) {\n try {\n this.progressToast.hide();\n } catch (error) {\n console.warn('Error hiding progress toast on error:', error);\n }\n }\n\n // Show error toast with immediate feedback\n if (this.toastService) {\n this.toastService.error(`Upload failed: ${error.message}`);\n }\n\n // Call user-provided error callback\n if (typeof this.options.onError === 'function') {\n this.options.onError(error);\n }\n }\n\n /**\n * Show progress toast with ProgressView component\n * @private\n */\n _showProgressToast() {\n // Create progress view with file information\n this.progressView = new ProgressView({\n filename: this.options.name || this.options.file.name,\n filesize: this.options.file.size,\n showCancel: true,\n onCancel: () => this.cancel()\n });\n\n // Show progress view in toast\n this.progressToast = this.toastService.showView(this.progressView, 'info', {\n title: 'File Upload',\n autohide: false,\n dismissible: false\n });\n }\n\n /**\n * Cancel the upload\n * @returns {boolean} True if cancelled, false if already completed\n */\n cancel() {\n if (this.cancelled) {\n return false;\n }\n\n this.cancelled = true;\n\n // Cancel the upload request if in progress\n if (this.uploadRequest && typeof this.uploadRequest.abort === 'function') {\n this.uploadRequest.abort();\n }\n\n // Mark progress view as cancelled\n if (this.progressView) {\n this.progressView.markCancelled();\n }\n\n // Hide progress toast after a delay\n if (this.progressToast) {\n setTimeout(() => {\n try {\n if (this.progressToast && typeof this.progressToast.hide === 'function') {\n this.progressToast.hide();\n }\n } catch (error) {\n console.warn('Error hiding progress toast on cancel:', error);\n }\n }, 1500);\n }\n\n return true;\n }\n\n /**\n * Check if upload is cancelled\n * @returns {boolean} True if cancelled\n */\n isCancelled() {\n return this.cancelled;\n }\n\n /**\n * Promise interface - then\n * @param {function} onSuccess - Success handler\n * @param {function} onError - Error handler\n * @returns {Promise} Promise chain\n */\n then(onSuccess, onError) {\n return this.promise.then(onSuccess, onError);\n }\n\n /**\n * Promise interface - catch\n * @param {function} onError - Error handler\n * @returns {Promise} Promise chain\n */\n catch(onError) {\n return this.promise.catch(onError);\n }\n\n /**\n * Promise interface - finally\n * @param {function} onFinally - Finally handler\n * @returns {Promise} Promise chain\n */\n finally(onFinally) {\n return this.promise.finally(onFinally);\n }\n\n /**\n * Get upload statistics\n * @returns {Object} Upload stats\n */\n getStats() {\n return {\n filename: this.options.file.name,\n size: this.options.file.size,\n type: this.options.file.type,\n cancelled: this.cancelled,\n group: this.options.group,\n description: this.options.description\n };\n }\n}\n\nexport default FileUpload;\n","\nimport Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\nimport FileUpload from '@core/services/FileUpload.js';\nimport {UserList} from '@core/models/User.js';\nimport {GroupList} from '@core/models/Group.js';\n/* =========================\n * FileManager\n * ========================= */\nclass FileManager extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/fileman/manager',\n });\n }\n}\n\nclass FileManagerList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: FileManager,\n endpoint: '/api/fileman/manager',\n size: 10,\n ...options,\n });\n }\n}\n\nconst FileManagerForms = {\n create: {\n title: 'Add Storage Backend',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Display Name',\n placeholder: 'Enter Display Name',\n cols: 12,\n },\n {\n name: 'use',\n type: 'text',\n label: 'Use',\n placeholder: 'Enter User or Leave Blank',\n cols: 12,\n },\n {\n name: 'backend_url',\n type: 'text',\n label: 'Backend URL',\n required: true,\n value: \"s3://BUCKET_NAME/OPTION_FOLDER\",\n placeholder: 's3://bucket_name/optional folder',\n help: 'Format: service://path. Valid services: s3',\n cols: 12,\n },\n {\n name: 'aws_region',\n type: 'select',\n label: 'AWS Region (optional)',\n value: 'us-east-1',\n options: [\n { value: '', text: 'System Default' },\n { value: 'us-east-1', text: 'US East (N. Virginia)' },\n { value: 'us-east-2', text: 'US East (Ohio)' },\n { value: 'us-west-1', text: 'US West (N. California)' },\n { value: 'us-west-2', text: 'US West (Oregon)' },\n { value: 'ca-central-1', text: 'Canada (Central)' },\n { value: 'eu-west-1', text: 'Europe (Ireland)' },\n { value: 'eu-west-2', text: 'Europe (London)' },\n { value: 'eu-west-3', text: 'Europe (Paris)' },\n { value: 'eu-central-1', text: 'Europe (Frankfurt)' },\n { value: 'eu-north-1', text: 'Europe (Stockholm)' },\n { value: 'eu-south-1', text: 'Europe (Milan)' },\n { value: 'ap-southeast-2', text: 'Asia Pacific (Sydney)' }\n ],\n columns: 12,\n help: 'Optional. Defaults to project AWS_REGION if omitted.'\n },\n {\n name: 'aws_key',\n type: 'text',\n label: 'AWS Key (optional)',\n placeholder: 'enter your AWS Key with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Key with S3 permissions'\n },\n {\n name: 'aws_secret',\n type: 'text',\n label: 'AWS Secret (optional)',\n placeholder: 'enter your AWS Secret with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Secret with S3 permissions'\n },\n {\n name: 'is_default',\n type: 'switch',\n label: 'Is Default',\n cols: 6,\n },\n {\n name: 'is_active',\n type: 'switch',\n label: 'Is Active',\n default: true,\n cols: 6,\n },\n ],\n },\n\n edit: {\n title: 'Edit Storage Backend',\n fields: [\n {\n name: 'name',\n type: 'text',\n label: 'Display Name',\n placeholder: 'Enter Display Name',\n cols: 12,\n },\n {\n name: 'use',\n type: 'text',\n label: 'Use',\n placeholder: 'Enter User or Leave Blank',\n cols: 12,\n },\n {\n name: 'backend_url',\n type: 'text',\n label: 'Backend URL',\n required: true,\n placeholder: 's3://bucket_name/optional folder',\n help: 'Format: service://path. Valid services: s3',\n cols: 12,\n },\n {\n name: 'allowed_origins',\n type: 'text',\n label: 'Domains Who Can Upload',\n cols: 12,\n },\n {\n name: 'is_default',\n type: 'switch',\n label: 'Is Default',\n cols: 6,\n },\n {\n name: 'is_active',\n type: 'switch',\n label: 'Is Active',\n default: true,\n cols: 6,\n },\n {\n name: 'is_public',\n type: 'switch',\n label: 'Is Public',\n default: true,\n cols: 6,\n }\n ],\n },\n\n owners: {\n fields: [\n {\n type: 'collection',\n name: 'group',\n label: 'Group (Owner)',\n Collection: GroupList, // Collection class\n labelField: 'name', // Field to display in dropdown\n valueField: 'id', // Field to use as value\n maxItems: 10, // Max items to show in dropdown\n placeholder: 'Search groups...',\n emptyFetch: false,\n debounceMs: 300, // Search debounce delay\n },\n {\n type: 'collection',\n name: 'user',\n label: 'User (Owner)',\n Collection: UserList, // Collection class\n labelField: 'display_name', // Field to display in dropdown\n valueField: 'id', // Field to use as value\n maxItems: 10, // Max items to show in dropdown\n placeholder: 'Search users...',\n emptyFetch: false,\n debounceMs: 300, // Search debounce delay\n },\n ],\n },\n\n credentials: {\n fields: [\n {\n name: 'aws_region',\n type: 'select',\n label: 'AWS Region (optional)',\n value: 'us-east-1',\n options: [\n { value: '', text: 'System Default' },\n { value: 'us-east-1', text: 'US East (N. Virginia)' },\n { value: 'us-east-2', text: 'US East (Ohio)' },\n { value: 'us-west-1', text: 'US West (N. California)' },\n { value: 'us-west-2', text: 'US West (Oregon)' },\n { value: 'ca-central-1', text: 'Canada (Central)' },\n { value: 'eu-west-1', text: 'Europe (Ireland)' },\n { value: 'eu-west-2', text: 'Europe (London)' },\n { value: 'eu-west-3', text: 'Europe (Paris)' },\n { value: 'eu-central-1', text: 'Europe (Frankfurt)' },\n { value: 'eu-north-1', text: 'Europe (Stockholm)' },\n { value: 'eu-south-1', text: 'Europe (Milan)' },\n { value: 'ap-southeast-2', text: 'Asia Pacific (Sydney)' }\n ],\n columns: 12,\n help: 'Optional. Defaults to project AWS_REGION if omitted.'\n },\n {\n name: 'aws_key',\n type: 'text',\n label: 'AWS Key (optional)',\n placeholder: 'enter your AWS Key with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Key with S3 permissions'\n },\n {\n name: 'aws_secret',\n type: 'text',\n label: 'AWS Secret (optional)',\n placeholder: 'enter your AWS Secret with S3 permissions',\n columns: 12,\n help: 'Optional, AWS Secret with S3 permissions'\n },\n ]\n }\n};\n\n/* =========================\n * File\n * ========================= */\nclass File extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/fileman/file',\n });\n }\n\n isImage() {\n return this.get(\"category\") === 'image';\n }\n\n /**\n * Upload file with progress tracking and UI integration\n * Returns a FileUpload instance with promise interface and cancellation support\n *\n * @param {object} options - Upload configuration\n * @param {File} options.file - File object to upload\n * @param {string} options.name - Custom filename (optional)\n * @param {string} options.group - File group/category (optional)\n * @param {string} options.description - File description (optional)\n * @param {function} options.onProgress - Progress callback ({ progress, loaded, total, percentage })\n * @param {function} options.onComplete - Success callback\n * @param {function} options.onError - Error callback\n * @param {boolean} options.showToast - Show progress toast (default: true)\n * @returns {FileUpload} Upload instance with promise interface\n */\n upload(options = {}) {\n return new FileUpload(this, options);\n }\n}\n\nclass FileList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: File,\n endpoint: '/api/fileman/file',\n size: 10,\n ...options,\n });\n }\n}\n\nconst FileForms = {\n create: {\n title: 'Add File',\n fields: [\n\n ],\n },\n\n edit: {\n title: 'Edit File Backend',\n fields: [\n\n ],\n },\n};\n\nexport {\n FileManager,\n FileManagerList,\n FileManagerForms,\n File,\n FileList,\n FileForms,\n};\n"],"names":["ProgressView","View","constructor","options","super","template","this","filename","filesize","filesizeFormatted","dataFormatter","pipe","progress","percentage","loaded","total","loadedFormatted","totalFormatted","status","showCancel","onCancel","cancelled","completed","getTemplate","updateProgress","progressInfo","render","markCompleted","message","markFailed","markCancelled","onActionCancel","action","event","element","disabled","emit","error","console","setFilename","setFilesize","size","getPercentage","isCompleted","isCancelled","getStats","FileUpload","fileModel","file","name","group","description","onProgress","onComplete","onError","showToast","File","Error","uploadRequest","progressToast","progressView","toastService","ToastService","promise","_startUpload","uploadData","uploadConfig","result","_showProgressToast","_initiateUpload","upload_url","url","method","fields","headers","JSON","stringify","_performUpload","_completeUpload","warn","_onComplete","_onError","payload","file_size","content_type","type","response","rest","POST","data","errorMessage","id","set","Promise","resolve","reject","useFormData","xhr","XMLHttpRequest","upload","onprogress","_onProgress","Math","round","onload","statusText","onerror","ontimeout","onabort","apiUrl","startsWith","resolvedUrl","buildUrl","open","timeout","key","value","Object","entries","toLowerCase","setRequestHeader","formData","FormData","append","send","save","setTimeout","hide","cancel","showView","title","autohide","dismissible","abort","then","onSuccess","catch","onFinally","finally","FileManager","Model","endpoint","FileManagerList","Collection","ModelClass","FileManagerForms","create","label","placeholder","cols","required","help","text","columns","default","edit","owners","GroupList","labelField","valueField","maxItems","emptyFetch","debounceMs","UserList","credentials","isImage","get","FileList","FileForms"],"mappings":"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"}
|