diff --git a/addons/test_website/__manifest__.py b/addons/test_website/__manifest__.py index e6a4396c7ae..940143a7e37 100644 --- a/addons/test_website/__manifest__.py +++ b/addons/test_website/__manifest__.py @@ -13,6 +13,7 @@ and we don't want to reload the website module every time, including it's possib dependencies. Neither we want to add in website module some routes, views and models which only purpose is to run tests.""", 'depends': [ + 'web_unsplash', 'website', ], 'data': [ diff --git a/addons/test_website/static/tests/tours/image_upload_progress.js b/addons/test_website/static/tests/tours/image_upload_progress.js index aaa1141e14f..4976752aeb9 100644 --- a/addons/test_website/static/tests/tours/image_upload_progress.js +++ b/addons/test_website/static/tests/tours/image_upload_progress.js @@ -3,28 +3,30 @@ odoo.define('test_website.image_upload_progress', function (require) { const tour = require('web_tour.tour'); +const setupSteps = [{ + content: "enter edit mode", + trigger: "a[data-action=edit]" +}, { + content: "drop a snippet", + trigger: "#oe_snippets .oe_snippet[name='Text - Image'] .oe_snippet_thumbnail:not(.o_we_already_dragging)", + extra_trigger: "body.editor_enable.editor_has_snippets", + moveTrigger: ".oe_drop_zone", + run: "drag_and_drop #wrap", +}, { + content: "drop a snippet", + trigger: "#oe_snippets .oe_snippet[name='Image Gallery'] .oe_snippet_thumbnail:not(.o_we_already_dragging)", + extra_trigger: "body.editor_enable.editor_has_snippets", + moveTrigger: ".oe_drop_zone", + run: "drag_and_drop #wrap", +}]; + const formatErrorMsg = "format is not supported. Try with: .gif, .jpe, .jpeg, .jpg, .png, .svg"; tour.register('test_image_upload_progress', { url: '/test_image_progress', test: true }, [ - { - content: "enter edit mode", - trigger: "a[data-action=edit]" - }, { - content: "drop a snippet", - trigger: "#oe_snippets .oe_snippet[name='Text - Image'] .oe_snippet_thumbnail:not(.o_we_already_dragging)", - extra_trigger: "body.editor_enable.editor_has_snippets", - moveTrigger: ".oe_drop_zone", - run: "drag_and_drop #wrap", - }, { - content: "drop a snippet", - trigger: "#oe_snippets .oe_snippet[name='Image Gallery'] .oe_snippet_thumbnail:not(.o_we_already_dragging)", - extra_trigger: "body.editor_enable.editor_has_snippets", - moveTrigger: ".oe_drop_zone", - run: "drag_and_drop #wrap", - }, + ...setupSteps, // 1. Check multi image upload { content: "click on dropped snippet", @@ -158,4 +160,41 @@ tour.register('test_image_upload_progress', { }, ]); + +tour.register('test_image_upload_progress_unsplash', { + url: '/test_image_progress', + test: true +}, [ + ...setupSteps, + // 1. Check multi image upload + { + content: "click on dropped snippet", + trigger: "#wrap .s_image_gallery .img", + }, { + content: "click on replace media to open image dialog", + trigger: 'we-customizeblock-option [data-replace-media]', + }, { + content: "search 'fox' images", + trigger: ".o_we_search", + run: "text fox", + }, { + content: "click on unsplash result", // note that unsplash is mocked + trigger: ".o_unsplash_attachment_cell" + }, { + content: "check that the upload progress bar is correctly shown", + // ensure it is there so we are sure next step actually test something + extra_trigger: '.o_notification_close', + trigger: ".o_we_progressbar:contains('fox'):contains('File has been uploaded')", + in_modal: false, + run: function () {}, // it's a check + }, { + content: "notification should close after 3 seconds", + trigger: 'body:not(:has(.o_notification_close))', + in_modal: false, + }, { + content: "unsplash image (mocked to logo) should have been used", + trigger: "#wrap .s_image_gallery .img[data-original-src^='/unsplash/HQqIOc8oYro/fox']", + }, +]); + }); diff --git a/addons/test_website/tests/test_image_upload_progress.py b/addons/test_website/tests/test_image_upload_progress.py index 2d82762d7ea..60f0c356cd3 100644 --- a/addons/test_website/tests/test_image_upload_progress.py +++ b/addons/test_website/tests/test_image_upload_progress.py @@ -1,11 +1,57 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.addons.web_editor.controllers.main import Web_Editor +from odoo.addons.web_unsplash.controllers.main import Web_Unsplash + import odoo.tests +from odoo import http +from odoo.tools import config + +BASE_URL = "http://127.0.0.1:%s" % (config["http_port"],) + @odoo.tests.common.tagged('post_install', '-at_install') class TestImageUploadProgress(odoo.tests.HttpCase): def test_01_image_upload_progress(self): self.start_tour("/test_image_progress", 'test_image_upload_progress', login="admin") + + def test_02_image_upload_progress_unsplash(self): + def media_library_search(self, **params): + return {"results": 0, "media": []} + + def fetch_unsplash_images(self, **post): + return { + 'total': 1434, + 'total_pages': 48, + 'results': [{ + 'id': 'HQqIOc8oYro', + 'alt_description': 'brown fox sitting on green grass field during daytime', + 'urls': { + # 'regular': 'https://images.unsplash.com/photo-1462953491269-9aff00919695?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwzMDUwOHwwfDF8c2VhcmNofDF8fGZveHxlbnwwfHx8fDE2MzEwMzIzNDE&ixlib=rb-1.2.1&q=80&w=1080', + 'regular': BASE_URL + '/website/static/src/img/phone.png', + }, + 'links': { + # 'download_location': 'https://api.unsplash.com/photos/HQqIOc8oYro/download?ixid=MnwzMDUwOHwwfDF8c2VhcmNofDF8fGZveHxlbnwwfHx8fDE2MzEwMzIzNDE' + 'download_location': BASE_URL + '/website/static/src/img/phone.png', + }, + 'user': { + 'name': 'Mitchell Admin', + 'links': { + 'html': BASE_URL, + }, + }, + }] + } + + # because not preprocessed by ControllerType metaclass + fetch_unsplash_images.routing_type = 'json' + Web_Unsplash.fetch_unsplash_images = http.route("/web_unsplash/fetch_images", type='json', auth="user")(fetch_unsplash_images) + + # disable undraw, no third party should be called in tests + media_library_search.routing_type = 'json' + Web_Editor.media_library_search = http.route(['/web_editor/media_library_search'], type='json', auth="user", website=True)(media_library_search) + + self.start_tour("/", 'test_image_upload_progress_unsplash', login="admin") diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js index 008b913e479..38db3565971 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js @@ -127,10 +127,6 @@ 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'], @@ -205,7 +201,6 @@ var FileWidget = SearchableMediaWidget.extend({ 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); @@ -580,18 +575,49 @@ var FileWidget = SearchableMediaWidget.extend({ * @returns {Promise} */ async _addData() { - const files = this.$fileInput[0].files; + let files = this.$fileInput[0].files; if (!files.length) { // Case if the input is emptied, return resolved promise return; } - this.uploader = new UploadProgressToast(this, { - files: files, - resModel: this.options.res_model, - resId: this.options.res_id, - isImage: this.widgetType === 'image', + + const uploadMutex = new concurrency.Mutex(); + + // Upload the smallest file first to block the user the least possible. + files = _.sortBy(files, 'size'); + await this._setUpProgressToast(files); + _.each(files, (file, index) => { + // Upload one file at a time: no need to parallel as upload is + // limited by bandwidth. + uploadMutex.exec(async () => { + const dataURL = await utils.getDataURLFromFile(file); + const attachment = await this.uploader.rpcShowProgress({ + route: '/web_editor/attachment/add_data', + params: { + 'name': file.name, + 'data': dataURL.split(',')[1], + 'res_id': this.options.res_id, + 'res_model': this.options.res_model, + 'is_image': this.widgetType === 'image', + 'width': 0, + 'quality': 0, + } + }, index); + if (!attachment.error) { + this._handleNewAttachment(attachment); + } + }); + }); + + return uploadMutex.getUnlockedDef().then(() => { + if (!this.uploader.hasError) { + this.uploader.close(3000); + } + if (!this.options.multiImages && !this.noSave) { + this.trigger_up('save_request'); + } + this.noSave = false; }); - this.uploader.appendTo(document.body); }, /** * @private @@ -695,22 +721,20 @@ 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 */ - _onAttachmentUploaded({ data: attachment }) { - if (!attachment.error) { - this._handleNewAttachment(attachment); - } - }, - /** - * @private - */ - _onUploadCompleted() { - if (!this.options.multiImages && !this.noSave) { - this.trigger_up('save_request'); - } - this.noSave = false; + _setUpProgressToast: async function (files) { + this.uploader = new UploadProgressToast(this, files); + await this.uploader.appendTo(document.body); }, }); diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/upload_progress_toast.js b/addons/web_editor/static/src/js/wysiwyg/widgets/upload_progress_toast.js index 069f8cdd94c..2ba80a91840 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/upload_progress_toast.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/upload_progress_toast.js @@ -1,8 +1,6 @@ /** @odoo-module */ - import Widget from "web.Widget"; -import concurrency from "web.concurrency"; -import { getDataURLFromFile } from "web.utils"; +import {qweb} from 'web.core'; export const UploadProgressToast = Widget.extend({ xmlDependencies: ['/web_editor/static/src/xml/wysiwyg.xml'], @@ -14,62 +12,34 @@ export const UploadProgressToast = Widget.extend({ /** * @override */ - init(parent, { files, resModel, resId, isImage }) { + init(parent, files) { 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"; - } - }); + this.files = files; }, /** * @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); - } - }); - }); + this.$progress = $('
'); + _.each(this.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"; + } - uploadMutex.getUnlockedDef().then(() => { - this.trigger_up('upload_complete'); - this.destroyOnClose = true; - this.close(2000); + this.$progress.append(qweb.render('wysiwyg.widgets.upload.progressbar', { + fileId: index, + fileName: file.name, + fileSize: fileSize, + })); }); - this.uploadMutex = uploadMutex; + this.$el.find('.o_notification_content').append(this.$progress); }, //-------------------------------------------------------------------------- @@ -84,32 +54,21 @@ export const UploadProgressToast = Widget.extend({ */ 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); - } + this.el.querySelector('.fade').classList.remove('show'); + window.setTimeout(() => { + this.destroy(); + }, 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 + * @param {integer} index file index to retrieve its related progress bar + * @returns {Promise} */ - async _rpcShowProgress(params, $progressBar) { + async rpcShowProgress(params, index) { + let $progressBar = this.$el.find(`.js_progressbar_${index}`); try { const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', ev => { @@ -122,7 +81,7 @@ export const UploadProgressToast = Widget.extend({ // 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 }); + const attachment = await this._rpc(params, {xhr}); $progressBar.find('.fa-spinner, .progress').addClass('d-none'); if (attachment.error) { this.hasError = true; diff --git a/addons/web_editor/static/src/xml/wysiwyg.xml b/addons/web_editor/static/src/xml/wysiwyg.xml index b19e279f67a..beadeffd48a 100644 --- a/addons/web_editor/static/src/xml/wysiwyg.xml +++ b/addons/web_editor/static/src/xml/wysiwyg.xml @@ -270,6 +270,23 @@ +