[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
This commit is contained in:
Romain Derie
2021-03-31 07:06:35 +00:00
committed by qsm-odoo
parent 185c50a740
commit 0d6cdeef2f
6 changed files with 154 additions and 21 deletions
@@ -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
@@ -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,
@@ -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 = $('<div/>');
_.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');
});
},
});
/**
@@ -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);
@@ -280,6 +280,24 @@
</div>
</t>
<t t-name="wysiwyg.widgets.upload.progressbar">
<div t-attf-class="o_we_progressbar js_progressbar_#{fileId}">
<div class="d-flex text-info">
<span><i class="fa fa-spinner fa-pulse"/></span>
<span class="font-italic font-weight-bold text-truncate flex-grow-1 mx-2" t-esc="fileName"/>
<span class="text-nowrap" t-esc="fileSize"/>
</div>
<div class='js_progressbar_txt'>
<span class="text-success d-none"><i class='fa fa-check'/> File has been uploaded</span>
<span class="text-danger d-none"><i class='fa fa-times'/> File could not be saved</span>
</div>
<div class="progress">
<div class="progress-bar bg-info progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%;">0%</div>
</div>
<hr/>
</div>
</t>
<!-- Icon choosing part of the Media Dialog -->
<t t-name="wysiwyg.widgets.font-icons">
<form action="#">
@@ -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);
}