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 | |
| 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')
6 files changed, 468 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; + +}); diff --git a/addons/web/static/src/scss/file_upload.scss b/addons/web/static/src/scss/file_upload.scss new file mode 100644 index 00000000000..155b39ad110 --- /dev/null +++ b/addons/web/static/src/scss/file_upload.scss @@ -0,0 +1,58 @@ +.o_content { + + .o_kanban_progress_card { + + position: relative; + padding: 0; + min-height: 80px; + cursor: pointer; + + .o_kanban_record_bottom { + color: $gray-900; + } + .o_kanban_image_wrapper { + opacity: 0.7; + } + } + + .o_list_progress_card { + height: 25px; + border: 1px solid #dfdfdf; + position: relative; + vertical-align: middle; + padding: 0; + + .o_file_upload_upload_title { + color: $headings-color; + font-size: 13px; + font-weight: 500; + } + } + + .o_file_upload_progress_bar { + @include o-position-absolute($top: 0px, $left: 0px); + height: 100%; + width: 100%; + .o_file_upload_progress_bar_value { + transition: width 0.1s; + border-right: 1px solid darken($o-brand-primary, 7.5%); + height: 100%; + width: 100%; + background-color: $o-brand-primary; + opacity: 0.5; + } + .o_upload_cross { + @include o-position-absolute(0, 0); + padding: 4px; + color: #963535; + cursor: pointer; + font-size: 16px; + &:active { + opacity: 0.7; + } + } + &:not(:hover) .o_upload_cross { + display: none; + } + } +} diff --git a/addons/web/static/src/xml/file_upload_progress_bar.xml b/addons/web/static/src/xml/file_upload_progress_bar.xml new file mode 100644 index 00000000000..8d2c39e13bf --- /dev/null +++ b/addons/web/static/src/xml/file_upload_progress_bar.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates> + +<div t-name="web.ProgressBar" class="o_file_upload_progress_bar"> + <div class="o_file_upload_progress_bar_value"/> + <span class="o_upload_cross fa fa-times-circle" title="Cancel Upload" aria-label="Cancel Upload"/> +</div> + +</templates> diff --git a/addons/web/static/src/xml/file_upload_progress_card.xml b/addons/web/static/src/xml/file_upload_progress_card.xml new file mode 100644 index 00000000000..d60b38cf2d7 --- /dev/null +++ b/addons/web/static/src/xml/file_upload_progress_card.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates> + +<t t-name="web.ProgressCard"> + <t t-if="widget.viewType === 'kanban'"> + <div class="o_kanban_progress_card o_kanban_record o_kanban_attachment"> + <div class="o_kanban_image"> + <div class="o_kanban_image_wrapper"> + <div class="o_image o_image_thumbnail" t-att-data-mimetype="widget.type"/> + </div> + </div> + <div class="o_kanban_details"> + <div class="o_kanban_details_wrapper"> + <div t-att-title="widget.title" t-att-aria-label="widget.title" class="o_kanban_record_title"> + <span t-esc="widget.title"/> + </div> + <div class="o_kanban_record_body"/> + <div class="o_kanban_record_bottom"> + <div class="oe_kanban_bottom_left"> + <div class="o_file_upload_progress_text_left">Processing...</div> + </div> + <div class="oe_kanban_bottom_right"> + <span class="o_file_upload_progress_text_right"/> + </div> + </div> + </div> + </div> + </div> + </t> + <t t-if="widget.viewType === 'list'"> + <div class="o_data_row o_list_progress_card"> + <span class="o_file_upload_upload_title" t-esc="widget.title"/> + <span class="o_file_upload_progress_text_left">Processing...</span> + <span class="o_file_upload_progress_text_right"/> + </div> + </t> +</t> + +</templates> |
