diff --git a/addons/web/static/src/core/network/rpc_service.js b/addons/web/static/src/core/network/rpc_service.js index 4859c82b72d..9ab20fc4f72 100644 --- a/addons/web/static/src/core/network/rpc_service.js +++ b/addons/web/static/src/core/network/rpc_service.js @@ -50,7 +50,7 @@ function jsonrpc(env, rpcId, url, params, settings = {}) { method: "call", params: params, }; - const request = new XHR(); + const request = settings.xhr || new XHR(); let rejectFn; const promise = new Promise((resolve, reject) => { rejectFn = reject; diff --git a/addons/web/static/src/legacy/utils.js b/addons/web/static/src/legacy/utils.js index 76987f79352..76ebaa6b57d 100644 --- a/addons/web/static/src/legacy/utils.js +++ b/addons/web/static/src/legacy/utils.js @@ -143,7 +143,10 @@ export function mapLegacyEnvToWowlEnv(legacyEnv, wowlEnv) { let rejection; const prom = new Promise((resolve, reject) => { const [route, params, settings = {}] = args; - const jsonrpc = wowlEnv.services.rpc(route, params, { silent: settings.shadow }); + const jsonrpc = wowlEnv.services.rpc(route, params, { + silent: settings.shadow, + xhr: settings.xhr, + }); rejection = () => { jsonrpc.abort(); }; diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js index ab9622a73b1..008b913e479 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js @@ -11,6 +11,7 @@ var Widget = require('web.Widget'); var session = require('web.session'); const {removeOnImageChangeAttrs} = require('web_editor.image_processing'); const {getCSSVariableValue, DEFAULT_PALETTE} = require('web_editor.utils'); +const { UploadProgressToast } = require('@web_editor/js/wysiwyg/widgets/upload_progress_toast'); var QWeb = core.qweb; var _t = core._t; @@ -126,6 +127,10 @@ var FileWidget = SearchableMediaWidget.extend({ 'click .o_existing_attachment_remove': '_onRemoveClick', 'click .o_load_more': '_onLoadMoreClick', }), + custom_events: Object.assign({}, SearchableMediaWidget.prototype.events || {}, { + 'file_complete': '_onAttachmentUploaded', + 'upload_complete': '_onUploadCompleted', + }), existingAttachmentsTemplate: undefined, IMAGE_MIMETYPES: ['image/jpg', 'image/jpeg', 'image/jpe', 'image/png', 'image/svg+xml', 'image/gif'], @@ -193,6 +198,18 @@ var FileWidget = SearchableMediaWidget.extend({ return def; }); }, + /** + * @override + */ + destroy() { + if (this.uploader) { + // Prevent uploader from being destroyed with call to super so it can linger + this.uploader.setParent(null); + this.uploader.destroyOnClose = true; + this.uploader.close(2000); + } + this._super(...arguments); + }, //-------------------------------------------------------------------------- // Public @@ -563,52 +580,18 @@ var FileWidget = SearchableMediaWidget.extend({ * @returns {Promise} */ async _addData() { - let files = this.$fileInput[0].files; + const files = this.$fileInput[0].files; if (!files.length) { // Case if the input is emptied, return resolved promise return; } - - var uploadMutex = new concurrency.Mutex(); - - // Upload the smallest file first to block the user the least possible. - files = _.sortBy(files, 'size'); - await this._setUpProgressToast(files); - this.hasError = false; - _.each(files, (file, index) => { - // Upload one file at a time: no need to parallel as upload is - // limited by bandwidth. - uploadMutex.exec(() => { - return utils.getDataURLFromFile(file).then(result => { - return this._rpcShowProgress({ - route: '/web_editor/attachment/add_data', - params: { - 'name': file.name, - 'data': result.split(',')[1], - 'res_id': this.options.res_id, - 'res_model': this.options.res_model, - 'is_image': this.widgetType === 'image', - 'width': 0, - 'quality': 0, - } - }, index).then(attachment => { - if (!attachment.error) { - this._handleNewAttachment(attachment); - } - }); - }); - }); - }); - - return uploadMutex.getUnlockedDef().then(() => { - if (!this.hasError) { - this._closeProgressToast(); - } - if (!this.options.multiImages && !this.noSave) { - this.trigger_up('save_request'); - } - this.noSave = false; + this.uploader = new UploadProgressToast(this, { + files: files, + resModel: this.options.res_model, + resId: this.options.res_id, + isImage: this.widgetType === 'image', }); + this.uploader.appendTo(document.body); }, /** * @private @@ -712,91 +695,22 @@ var FileWidget = SearchableMediaWidget.extend({ this.numberOfAttachmentsToDisplay = this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY; this._super.apply(this, arguments); }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - /** - * Sets up a progress bar for every file being uploaded in a toast. - * * @private - * @param {Object[]} files */ - _setUpProgressToast: async function (files) { - this.$progress = $('
'); - _.each(files, (file, index) => { - let fileSize = file.size; - if (!fileSize) { - fileSize = null; - } else if (fileSize < 1024) { - fileSize = fileSize.toFixed(2) + " bytes"; - } else if (fileSize < 1048576) { - fileSize = (fileSize / 1024).toFixed(2) + " KB"; - } else { - fileSize = (fileSize / 1048576).toFixed(2) + " MB"; - } - - this.$progress.append(QWeb.render('wysiwyg.widgets.upload.progressbar', { - fileId: index, - fileName: file.name, - fileSize: fileSize, - })); - }); - this.connectionNotificationID = this.displayNotification({ - type: 'info', - sticky: true, - contentEl: this.$progress, - }); + _onAttachmentUploaded({ data: attachment }) { + if (!attachment.error) { + this._handleNewAttachment(attachment); + } }, /** - * Closes the toast holding the file(s) progress bar(s). - * * @private */ - _closeProgressToast: function () { - this.call('notification', 'close', this.connectionNotificationID, false, 3000); - }, - /** - * Calls a RPC and shows its progress status. - * - * @private - * @param {Object} params regular `_rpc()` parameters - * @param {integer} index file index to retrieve its related progress bar - * @returns {Promise} - */ - _rpcShowProgress: function (params, index) { - let $progressBar = this.$progress.find(`.js_progressbar_${index}`); - return this._rpc(params, { - xhr: function () { - var xhr = $.ajaxSettings.xhr(); - xhr.upload.onprogress = function (ev) { - var prcComplete = ev.loaded / ev.total * 100; - $progressBar.find('.progress-bar').css({ - width: parseInt(prcComplete) + '%', - }).text(prcComplete.toFixed(2) + '%'); - }; - xhr.upload.onload = function () { - // Don't show yet success as backend code only starts now - $progressBar.find('.progress-bar').css({width: '100%'}).text('100%'); - }; - return xhr; - }, - }).then(attachment => { - $progressBar.find('.fa-spinner, .progress').addClass('d-none'); - if (attachment.error) { - this.hasError = true; - $progressBar.find('.js_progressbar_txt .text-danger').removeClass('d-none'); - $progressBar.find('.js_progressbar_txt .text-danger .o_we_error_text').text(attachment.error); - } else { - $progressBar.find('.js_progressbar_txt .text-success').removeClass('d-none'); - } - return attachment; - }).guardedCatch(() => { - this.hasError = true; - $progressBar.find('.fa-spinner, .progress').addClass('d-none'); - $progressBar.find('.js_progressbar_txt .text-danger').removeClass('d-none'); - }); + _onUploadCompleted() { + if (!this.options.multiImages && !this.noSave) { + this.trigger_up('save_request'); + } + this.noSave = false; }, }); diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/upload_progress_toast.js b/addons/web_editor/static/src/js/wysiwyg/widgets/upload_progress_toast.js new file mode 100644 index 00000000000..069f8cdd94c --- /dev/null +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/upload_progress_toast.js @@ -0,0 +1,153 @@ +/** @odoo-module */ + +import Widget from "web.Widget"; +import concurrency from "web.concurrency"; +import { getDataURLFromFile } from "web.utils"; + +export const UploadProgressToast = Widget.extend({ + xmlDependencies: ['/web_editor/static/src/xml/wysiwyg.xml'], + template: 'wysiwyg.widgets.upload.progress_toast', + events: { + 'click .o_notification_close': '_onCloseClick', + }, + + /** + * @override + */ + init(parent, { files, resModel, resId, isImage }) { + this._super(...arguments); + this.resId = resId; + this.resModel = resModel; + this.isImage = isImage; + + // Upload the smallest file first to block the user the least possible. + this.files = [...files].sort((a, b) => a.size - b.size); + this.files.forEach((file, index) => { + file.id = index; + if (!file.size) { + file.displaySize = null; + } else if (file.size < 1024) { + file.displaySize = file.size.toFixed(2) + " bytes"; + } else if (file.size < 1048576) { + file.displaySize = (file.size / 1024).toFixed(2) + " KB"; + } else { + file.displaySize = (file.size / 1048576).toFixed(2) + " MB"; + } + }); + }, + /** + * @override + */ + start() { + this.hasError = false; + const uploadMutex = new concurrency.Mutex(); + this.files.forEach((file, index) => { + // Upload one file at a time: no need to parallel as upload is + // limited by bandwidth. + uploadMutex.exec(async () => { + const dataURL = await getDataURLFromFile(file); + const attachment = await this._rpcShowProgress({ + route: '/web_editor/attachment/add_data', + params: { + name: file.name, + data: dataURL.split(',')[1], + res_id: this.resId, + res_model: this.resModel, + is_image: this.isImage, + width: 0, + quality: 0, + } + }, this.$el.find(`.js_progressbar_${index}`)); + if (!attachment.error) { + this.trigger_up('file_complete', attachment); + } + }); + }); + + uploadMutex.getUnlockedDef().then(() => { + this.trigger_up('upload_complete'); + this.destroyOnClose = true; + this.close(2000); + }); + this.uploadMutex = uploadMutex; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Calls a RPC and shows its progress status. + * + * @public + * @param {Number} delay time to wait before closing the toast + */ + close(delay = 0) { + window.setTimeout(() => { + if (!this.hasError || delay === 0) { + this.el.querySelector('.fade').classList.remove('show'); + window.setTimeout(() => { + if (this.destroyOnClose) { + this.destroy(); + } else { + this.el.classList.add('d-none'); + } + }, 150); + } + }, delay); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Calls a RPC and shows its progress status. + * + * @private + * @param {Object} params regular `_rpc()` parameters + * @param {jQuery} $progressBar the element holding the progress bar + * @returns {Promise} resolved when the RPC is complete + */ + async _rpcShowProgress(params, $progressBar) { + try { + const xhr = new XMLHttpRequest(); + xhr.upload.addEventListener('progress', ev => { + const prcComplete = ev.loaded / ev.total * 100; + $progressBar.find('.progress-bar').css({ + width: Math.floor(prcComplete) + '%', + }).text(prcComplete.toFixed(2) + '%'); + }); + xhr.upload.addEventListener('load', function () { + // Don't show yet success as backend code only starts now + $progressBar.find('.progress-bar').css({width: '100%'}).text('100%'); + }); + const attachment = await this._rpc(params, { xhr }); + $progressBar.find('.fa-spinner, .progress').addClass('d-none'); + if (attachment.error) { + this.hasError = true; + $progressBar.find('.js_progressbar_txt .text-danger').removeClass('d-none'); + $progressBar.find('.js_progressbar_txt .text-danger .o_we_error_text').text(attachment.error); + } else { + $progressBar.find('.js_progressbar_txt .text-success').removeClass('d-none'); + } + return attachment; + } catch (error) { + this.hasError = true; + $progressBar.find('.fa-spinner, .progress').addClass('d-none'); + $progressBar.find('.js_progressbar_txt .text-danger').removeClass('d-none'); + throw error; + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onCloseClick() { + this.close(); + }, +}); diff --git a/addons/web_editor/static/src/xml/wysiwyg.xml b/addons/web_editor/static/src/xml/wysiwyg.xml index 48e057a8245..e3694e11727 100644 --- a/addons/web_editor/static/src/xml/wysiwyg.xml +++ b/addons/web_editor/static/src/xml/wysiwyg.xml @@ -269,21 +269,35 @@ -