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); }