[FIX] web, web_editor: restore media upload progress toast
In odoo/odoo#72675 the new services were made available in the frontend, and calls to the legacy notification services were redirected to the new notification service. However, some of the behaviour of the legacy notification service was not replicated in the new one, like the ability to pass an HTML element to use inside the notification. While html content can be passed, it is cloned, meaning that the DOM of the element cannot be manipulated by the caller, which is what was being done to show progress during media upload. Although the upload progress toast looks like a notification, it's hardly a standard notification and adds a lot of behaviour, because manipulating DOM directly when it is managed by owl cannot be done safely, it has been decided to simply make it its own widget separate from the notification service, which can manipulate its own DOM freely. task-2607393 closes odoo/odoo#74027 Signed-off-by: Romain Derie <rdeodoo@users.noreply.github.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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 = $('<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,
|
||||
});
|
||||
_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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
@@ -269,21 +269,35 @@
|
||||
</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"/>
|
||||
<t t-name="wysiwyg.widgets.upload.progress_toast">
|
||||
<div class="o_notification_manager">
|
||||
<div t-attf-class="o_notification bg-info fade show" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="o_notification_body">
|
||||
<button type="button" class="close o_notification_close"
|
||||
aria-label="Close">
|
||||
<span class="d-inline" aria-hidden="true">×</span>
|
||||
</button>
|
||||
<div class="mr-auto o_notification_content">
|
||||
<div>
|
||||
<div t-foreach="widget.files" t-as="file" t-attf-class="o_we_progressbar js_progressbar_#{file.id}">
|
||||
<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="file.name"/>
|
||||
<span class="text-nowrap" t-esc="file.displaySize"/>
|
||||
</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 float-left m-1'/><span class="o_we_error_text">File could not be saved</span></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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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 float-left m-1'/> <span class="o_we_error_text">File could not be saved</span></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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user