From 0d6cdeef2f0c676fb04d9b2ef7e69a750111463b Mon Sep 17 00:00:00 2001 From: Romain Derie Date: Wed, 3 Feb 2021 17:01:56 +0000 Subject: [PATCH] [IMP] web_editor, *: show a progress bar on image upload *: web, web_unsplash Without this commit, the user had no hint to know something was actually being performed. It would be worst when uploading multiple image at once, eg in image wall, as it could take some times for the images to be processed and uploaded. The user now has a visual hint about the upload progress through a toaster and some progress bars. For unsplash, note that as the actual image download is performed S2S, the progress bar will automatically jump to 100%, then will wait for the RPC success once the server is done to show a success progress bar. Part of https://github.com/odoo/odoo/pull/65828 task-2345082 --- .../src/js/services/notification_service.js | 11 +- .../web/static/src/js/widgets/notification.js | 8 +- .../static/src/js/wysiwyg/widgets/media.js | 114 +++++++++++++++--- .../web_editor/static/src/scss/wysiwyg.scss | 6 + addons/web_editor/static/src/xml/wysiwyg.xml | 18 +++ .../static/src/js/unsplash_image_widget.js | 18 ++- 6 files changed, 154 insertions(+), 21 deletions(-) diff --git a/addons/web/static/src/js/services/notification_service.js b/addons/web/static/src/js/services/notification_service.js index 295b49d760c..8b66076a4fb 100644 --- a/addons/web/static/src/js/services/notification_service.js +++ b/addons/web/static/src/js/services/notification_service.js @@ -42,13 +42,20 @@ var NotificationService = AbstractService.extend({ * @param {number} notificationId * @param {boolean} [silent=false] if true, the notification does not call * onClose callback + * @param {integer} [wait=0] seconds to wait before closing the notification */ - close: function (notificationId, silent) { + close: function (notificationId, silent, wait = 0) { var notification = this.notifications[notificationId]; if (!notification) { return; } - notification.close(silent); + if (wait) { + setTimeout(() => { + notification.close(silent); + }, wait); + } else { + notification.close(silent); + } }, /** * Display a notification at the appropriate location, and returns the diff --git a/addons/web/static/src/js/widgets/notification.js b/addons/web/static/src/js/widgets/notification.js index 59044a19198..ff75eb2e172 100644 --- a/addons/web/static/src/js/widgets/notification.js +++ b/addons/web/static/src/js/widgets/notification.js @@ -29,6 +29,8 @@ var Notification = Widget.extend({ * @param {string} params.title * @param {string} params.subtitle * @param {string} [params.message] + * @param {DOMElement|jQuery} [params.contentEl] element to insert inside the + * notification, useful to keep a reference to the notification content * @param {string} [params.type='warning'] 'info', 'success', 'warning', 'danger' or '' * @param {boolean} [params.sticky=false] if true, the notification will * stay visible until the user clicks on it. @@ -45,7 +47,8 @@ var Notification = Widget.extend({ this._super.apply(this, arguments); this.title = params.title; this.subtitle = params.subtitle; - this.message = params.message; + this.contentEl = params.contentEl; + this.message = params.message || this.contentEl && true; this.buttons = params.buttons || []; this.sticky = !!this.buttons.length || !!params.sticky; this.type = params.type === undefined ? 'warning' : params.type; @@ -66,6 +69,9 @@ var Notification = Widget.extend({ * @override */ start: function () { + if (this.contentEl) { + this.$('.o_notification_content').empty().append(this.contentEl); + } this.$el.toast({ animation: this._animation, autohide: false, 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 95170d10e38..28b8839ad4a 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js @@ -565,38 +565,44 @@ var FileWidget = SearchableMediaWidget.extend({ return; } - var self = this; var uploadMutex = new concurrency.Mutex(); // Upload the smallest file first to block the user the least possible. files = _.sortBy(files, 'size'); - _.each(files, function (file) { + 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(function () { - return utils.getDataURLFromFile(file).then(function (result) { - return self._rpc({ + 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': self.options.res_id, - 'res_model': self.options.res_model, + 'res_id': this.options.res_id, + 'res_model': this.options.res_model, 'width': 0, 'quality': 0, - }, - }).then(function (attachment) { - self._handleNewAttachment(attachment); + } + }, index).then(attachment => { + if (!attachment.error) { + this._handleNewAttachment(attachment); + } }); }); }); }); - return uploadMutex.getUnlockedDef().then(function () { - if (!self.options.multiImages && !self.noSave) { - self.trigger_up('save_request'); + return uploadMutex.getUnlockedDef().then(() => { + if (!this.hasError) { + this._closeProgressToast(); } - self.noSave = false; + if (!this.options.multiImages && !this.noSave) { + this.trigger_up('save_request'); + } + this.noSave = false; }); }, /** @@ -701,6 +707,86 @@ 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, + }); + }, + /** + * 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'); + $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'); + }); + }, }); /** diff --git a/addons/web_editor/static/src/scss/wysiwyg.scss b/addons/web_editor/static/src/scss/wysiwyg.scss index af604453a75..29094c6a1b1 100644 --- a/addons/web_editor/static/src/scss/wysiwyg.scss +++ b/addons/web_editor/static/src/scss/wysiwyg.scss @@ -384,6 +384,12 @@ body .modal { } } +.o_we_progressbar:last-child { + hr { + display: none; + } +} + // Highlight selected image/icon %o-we-selected-image { outline: 3px solid rgba(150, 150, 220, 0.3); diff --git a/addons/web_editor/static/src/xml/wysiwyg.xml b/addons/web_editor/static/src/xml/wysiwyg.xml index 9c1a362089b..e0cf05ae981 100644 --- a/addons/web_editor/static/src/xml/wysiwyg.xml +++ b/addons/web_editor/static/src/xml/wysiwyg.xml @@ -280,6 +280,24 @@
+ +
+
+ + + +
+
+ File has been uploaded + File could not be saved +
+
+
0%
+
+
+
+
+
diff --git a/addons/web_unsplash/static/src/js/unsplash_image_widget.js b/addons/web_unsplash/static/src/js/unsplash_image_widget.js index e638806dc1c..41484e9864b 100644 --- a/addons/web_unsplash/static/src/js/unsplash_image_widget.js +++ b/addons/web_unsplash/static/src/js/unsplash_image_widget.js @@ -4,6 +4,7 @@ odoo.define('web_unsplash.image_widgets', function (require) { var core = require('web.core'); var UnsplashAPI = require('unsplash.api'); var widgetsMedia = require('wysiwyg.widgets.media'); +const {_t} = require('web.core'); var unsplashAPI = null; @@ -80,17 +81,26 @@ widgetsMedia.ImageWidget.include({ */ _save: async function () { const _super = this._super; - if (Object.keys(this._unsplash.selectedImages).length) { + const selectedImages = this._unsplash.selectedImages; + const imagesCount = Object.keys(selectedImages).length; + if (imagesCount) { this.saved = true; - const images = await this._rpc({ + await this._setUpProgressToast([{ + name: imagesCount > 1 ? + _.str.sprintf(_t("Uploading %s '%s' images."), imagesCount, this._unsplash.query) : + _.str.sprintf(_t("Uploading '%s' image."), this._unsplash.query), + size: null, + }]); + const images = await this._rpcShowProgress({ route: '/web_unsplash/attachment/add', params: { - unsplashurls: this._unsplash.selectedImages, + unsplashurls: selectedImages, res_model: this.options.res_model, res_id: this.options.res_id, query: this._unsplash.query, }, - }); + }, 0); + this._closeProgressToast(); this.attachments.push(...images); this.selectedAttachments.push(...images); }