diff options
| author | Thanh Dodeur <tso@odoo.com> | 2019-10-10 09:01:58 +0000 |
|---|---|---|
| committer | Thanh Dodeur <tso@odoo.com> | 2019-12-05 14:38:26 +0000 |
| commit | 68ecfe1357437f1e9d6a82a2d3640c895afa3781 (patch) | |
| tree | eaa6a9f02929ea5dceec9883c2fd09b96dd89858 /addons/web/static/src/js | |
| parent | aa6474801c055e3030253fa89f6c15b6a28fecc6 (diff) | |
[ADD] web: adds file upload mixin
This commit adds mixin to manage file uploads in view controllers and to
display progress bars.
The first motivation and use case is to allow MRP to manage and display
uploads like in Documents.
closes odoo/odoo#38397
Task: #2074822
Related: odoo/enterprise#5887
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Diffstat (limited to 'addons/web/static/src/js')
3 files changed, 362 insertions, 0 deletions
diff --git a/addons/web/static/src/js/views/file_upload_mixin.js b/addons/web/static/src/js/views/file_upload_mixin.js new file mode 100644 index 00000000000..84dddcd904d --- /dev/null +++ b/addons/web/static/src/js/views/file_upload_mixin.js @@ -0,0 +1,234 @@ +odoo.define('web.fileUploadMixin', function (require) { +'use strict'; + +/** + * Mixin to be used in view Controllers to manage uploads and generate progress bars. + * supported views: kanban, list + */ + +const { csrf_token, _t } = require('web.core'); +const ProgressBar = require('web.ProgressBar'); +const ProgressCard = require('web.ProgressCard'); + +const ProgressBarMixin = { + + custom_events: { + progress_bar_abort: '_onProgressBarAbort', + }, + + init() { + /** + * Contains the uploads currently happening, used to attach progress bars. + * e.g: {'fileUploadId45': {progressBar, progressCard, ...params}} + */ + this._fileUploads = {}; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * used to use a mocked version of Xhr in the tests. + * + * @private + * @returns {XMLHttpRequest} + */ + _createXhr() { + return new window.XMLHttpRequest(); + }, + /** + * @private + */ + _getFileUploadRenderOptions() { + return { + predicate: () => true, + targetCallback: undefined, + }; + }, + /** + * @private + * @returns {string} upload route + */ + _getFileUploadRoute() { + return '/web/binary/upload_attachment'; + }, + /** + * @private + * @param {Object} params + * @param {Object[]} params.files + * @param {XMLHttpRequest} params.xhr + */ + _makeFileUpload(params) { + const { files, xhr } = params; + const fileUploadId = _.uniqueId('fileUploadId'); + const formData = new FormData(); + const formDataKeys = this._makeFileUploadFormDataKeys(Object.assign({ fileUploadId }, params)); + + formData.append('csrf_token', csrf_token); + for (const key in formDataKeys) { + if (formDataKeys[key] !== undefined) { + formData.append(key, formDataKeys[key]); + } + } + for (const file of files) { + formData.append('ufile', file); + } + + return { + fileUploadId, + xhr, + title: files.length === 1 + ? files[0].name + : _.str.sprintf(_t("%s Files"), files.length), + type: files.length === 1 ? files[0].type : undefined, + formData, + }; + }, + /** + * @private + * @param {Object} param0 + * @param {string} param0.fileUploadId + * @returns {Object} the list of the form entries of a file upload. + */ + _makeFileUploadFormDataKeys({ fileUploadId }) { + return { + callback: fileUploadId, + }; + }, + /** + * @private + * @param {integer} fileUploadId + */ + async _removeFileUpload(fileUploadId) { + const upload = this._fileUploads[fileUploadId]; + upload.progressCard && upload.progressCard.destroy(); + upload.progressBar && upload.progressBar.destroy(); + delete this._fileUploads[fileUploadId]; + await this.reload(); + }, + /** + * @private + */ + async _renderFileUploads() { + const { predicate, targetCallback } = this._getFileUploadRenderOptions(); + + for (const fileUploadId in this._fileUploads) { + const upload = this._fileUploads[fileUploadId]; + if (!predicate(upload)) { + continue; + } + + if (!upload.progressBar) { + if (!upload.recordId || this.viewType !== 'kanban') { + upload.progressCard = new ProgressCard(this, { + title: upload.title, + type: upload.type, + viewType: this.viewType, + }); + } + upload.progressBar = new ProgressBar(this, { + xhr: upload.xhr, + title: upload.title, + fileUploadId, + }); + } + + let $targetCard; + if (upload.progressCard) { + await upload.progressCard.prependTo(this.renderer.$el); + $targetCard = upload.progressCard.$el; + } else if (targetCallback) { + $targetCard = targetCallback(upload); + } + await upload.progressBar.appendTo($targetCard); + } + }, + /** + * @private + * @param {Object[]} files + * @param {Object} [params] optional additional data + */ + async _uploadFiles(files, params={}) { + if (!files || !files.length) { return; } + + await new Promise(resolve => { + const xhr = this._createXhr(); + xhr.open('POST', this._getFileUploadRoute()); + const fileUploadData = this._makeFileUpload(Object.assign({ files, xhr }, params)); + const { fileUploadId, formData } = fileUploadData; + this._fileUploads[fileUploadId] = fileUploadData; + xhr.upload.addEventListener("progress", ev => { + this._updateFileUploadProgress(fileUploadId, ev); + }); + const progressPromise = this._onBeforeUpload(); + xhr.onload = async () => { + await progressPromise; + resolve(); + this._onUploadLoad({ fileUploadId, xhr }); + }; + xhr.onerror = async () => { + await progressPromise; + resolve(); + this._onUploadError({ fileUploadId, xhr }); + }; + xhr.send(formData); + }); + }, + /** + * @private + * @param {string} fileUploadId + * @param {ProgressEvent} ev + */ + _updateFileUploadProgress(fileUploadId, ev) { + const { progressCard, progressBar } = this._fileUploads[fileUploadId]; + progressCard && progressCard.update(ev.loaded, ev.total); + progressBar && progressBar.update(ev.loaded, ev.total); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Hook to customize the behaviour of _uploadFiles() before an upload is made. + * + * @private + */ + async _onBeforeUpload() { + await this._renderFileUploads(); + }, + /** + * @private + * @param {OdooEvent} ev + * @param {integer} ev.data.fileUploadId + */ + _onProgressBarAbort(ev) { + this._removeFileUpload(ev.data.fileUploadId); + }, + /** + * Hook to customize the behaviour of the xhr.onload of an upload. + * + * @private + * @param {string} param0.fileUploadId + */ + _onUploadLoad({ fileUploadId }) { + this._removeFileUpload(fileUploadId); + }, + /** + * Hook to customize the behaviour of the xhr.onerror of an upload. + * + * @private + * @param {string} param1.fileUploadId + * @param {XMLHttpRequest} param0.xhr + */ + _onUploadError({ fileUploadId, xhr }) { + this.do_notify(xhr.status, _.str.sprintf(_t('message: %s'), xhr.reponseText), true); + this._removeFileUpload(fileUploadId); + }, + +}; + +return ProgressBarMixin; + +}); diff --git a/addons/web/static/src/js/views/file_upload_progress_bar.js b/addons/web/static/src/js/views/file_upload_progress_bar.js new file mode 100644 index 00000000000..95b99a831c0 --- /dev/null +++ b/addons/web/static/src/js/views/file_upload_progress_bar.js @@ -0,0 +1,76 @@ +odoo.define('web.ProgressBar', function (require) { +'use strict'; + +const { _t } = require('web.core'); +const Dialog = require('web.Dialog'); +const Widget = require('web.Widget'); + +const ProgressBar = Widget.extend({ + template: 'web.ProgressBar', + + events: { + 'click .o_upload_cross': '_onClickCross', + }, + + /** + * @override + * @param {Object} param1 + * @param {String} param1.title + * @param {String} param1.fileUploadId + * @param {XMLHttpRequest} param2.xhr + */ + init(parent, { title, fileUploadId, xhr }) { + this._super(...arguments); + this.title = title; + this.fileUploadId = fileUploadId; + this.xhr = xhr; + }, + + /** + * @override + * @return {Promise} + */ + start() { + this.xhr.onabort = () => this.do_notify(_t("Upload canceled")); + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @param {integer} loaded + * @param {integer} total + */ + update(loaded, total) { + if (!this.$el) { + return; + } + const percent = Math.round((loaded / total) * 100); + this.$('.o_file_upload_progress_bar_value').css("width", percent + "%"); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickCross(ev) { + ev.stopPropagation(); + const promptText = _.str.sprintf(_t("Do you really want to cancel the upload of %s?"), _.escape(this.title)); + Dialog.confirm(this, promptText, { + confirm_callback: () => { + this.xhr.abort(); + this.trigger_up('progress_bar_abort', { fileUploadId: this.fileUploadId }); + } + }); + }, +}); + +return ProgressBar; + +}); diff --git a/addons/web/static/src/js/views/file_upload_progress_card.js b/addons/web/static/src/js/views/file_upload_progress_card.js new file mode 100644 index 00000000000..1f1db52e53f --- /dev/null +++ b/addons/web/static/src/js/views/file_upload_progress_card.js @@ -0,0 +1,52 @@ +odoo.define('web.ProgressCard', function (require) { +'use strict'; + +const { _t } = require('web.core'); +const Widget = require('web.Widget'); + +const ProgressCard = Widget.extend({ + template: 'web.ProgressCard', + + /** + * @override + * @param {Object} param1 + * @param {String} param1.title + * @param {String} param1.type file mimetype + * @param {String} param1.viewType + */ + init(parent, { title, type, viewType }) { + this._super(...arguments); + this.title = title; + this.type = type; + this.viewType = viewType; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @param {integer} loaded + * @param {integer} total + */ + update(loaded, total) { + if (!this.$el) { + return; + } + const percent = Math.round((loaded / total) * 100); + const $textDivLeft = this.$('.o_file_upload_progress_text_left'); + const $textDivRight = this.$('.o_file_upload_progress_text_right'); + if (percent === 100) { + $textDivLeft.text(_t('Processing...')); + } else { + const mbLoaded = Math.round(loaded/1000000); + const mbTotal = Math.round(total/1000000); + $textDivLeft.text(_.str.sprintf(_t("Uploading... (%s%%)"), percent)); + $textDivRight.text(_.str.sprintf(_t("(%s/%sMb)"), mbLoaded, mbTotal)); + } + }, +}); + +return ProgressCard; + +}); |
