summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js
diff options
context:
space:
mode:
authorThanh Dodeur <tso@odoo.com>2019-10-10 09:01:58 +0000
committerThanh Dodeur <tso@odoo.com>2019-12-05 14:38:26 +0000
commit68ecfe1357437f1e9d6a82a2d3640c895afa3781 (patch)
treeeaa6a9f02929ea5dceec9883c2fd09b96dd89858 /addons/web/static/src/js
parentaa6474801c055e3030253fa89f6c15b6a28fecc6 (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')
-rw-r--r--addons/web/static/src/js/views/file_upload_mixin.js234
-rw-r--r--addons/web/static/src/js/views/file_upload_progress_bar.js76
-rw-r--r--addons/web/static/src/js/views/file_upload_progress_card.js52
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;
+
+});