From ec132b530ff517a324fa56f41acd0afa7ff7f26c Mon Sep 17 00:00:00 2001 From: Romain Derie Date: Tue, 8 Dec 2020 18:33:07 +0000 Subject: [PATCH] [FIX] web_editor, website: prevent multiple click on save Clicking on save on html editor is taking quite some time. The user might think something is not happening and need to click again. If he does click again, 2 write RPC will be sent, making the COW business code being triggered twice, ultimately leading to create 2 COW. Instead of A / \ B B' It will be A / | \ B B' B' With as many B' than there is click on save. This commit adds a generic helper to make a button load/spin. closes odoo/odoo#64366 X-original-commit: 80592abda266ef3f9f463854c3101eb3f728087b Signed-off-by: Quentin Smetz (qsm) Signed-off-by: Romain Derie --- addons/web/static/src/js/core/dom.js | 32 +++++++++++++++---- addons/web_editor/static/src/js/common/ace.js | 6 ++-- .../static/src/js/editor/editor_menu.js | 6 ++-- addons/website/static/src/js/menu/navbar.js | 8 ++--- addons/website/static/src/scss/website.scss | 8 +++++ 5 files changed, 43 insertions(+), 17 deletions(-) diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js index 4af811d41bc..b14684e4acd 100644 --- a/addons/web/static/src/js/core/dom.js +++ b/addons/web/static/src/js/core/dom.js @@ -343,19 +343,37 @@ var dom = { // part, the button is disabled without any visual effect. $button.addClass('o_debounce_disabled'); Promise.resolve(dom.DEBOUNCE && concurrency.delay(dom.DEBOUNCE)).then(function () { - $button.addClass('disabled').prop('disabled', true); $button.removeClass('o_debounce_disabled'); - - return Promise.resolve(result).then(function () { - $button.removeClass('disabled').prop('disabled', false); - }).guardedCatch(function () { - $button.removeClass('disabled').prop('disabled', false); - }); + const restore = dom.addButtonLoadingEffect($button[0]); + return Promise.resolve(result).then(restore).guardedCatch(restore); }); return result; }; }, + /** + * Gives the button a loading effect by disabling it and adding a `fa` + * spinner icon. + * The existing button `fa` icons will be hidden through css. + * + * @param {HTMLElement} btn - the button to disable/load + * @return {function} a callback function that will restore the button + * initial state + */ + addButtonLoadingEffect: function (btn) { + const $btn = $(btn); + $btn.addClass('o_website_btn_loading disabled'); + $btn.prop('disabled', true); + const $loader = $('', { + class: 'fa fa-refresh fa-spin mr-2', + }); + $btn.prepend($loader); + return () => { + $btn.removeClass('o_website_btn_loading disabled'); + $btn.prop('disabled', false); + $loader.remove(); + }; + }, /** * Prepends content in a jQuery object and optionnally triggers an event * diff --git a/addons/web_editor/static/src/js/common/ace.js b/addons/web_editor/static/src/js/common/ace.js index 4bbaf5f8fdd..769b4f33023 100644 --- a/addons/web_editor/static/src/js/common/ace.js +++ b/addons/web_editor/static/src/js/common/ace.js @@ -5,6 +5,7 @@ var ajax = require('web.ajax'); var config = require('web.config'); var concurrency = require('web.concurrency'); var core = require('web.core'); +var dom = require('web.dom'); var Dialog = require('web.Dialog'); var Widget = require('web.Widget'); var localStorage = require('web.local_storage'); @@ -911,8 +912,9 @@ var ViewEditor = Widget.extend({ * * @private */ - _onSaveClick: function () { - this._saveResources(); + _onSaveClick: function (ev) { + const restore = dom.addButtonLoadingEffect(ev.currentTarget); + this._saveResources().guardedCatch(restore); }, /** * Called when the user wants to switch from xml to scss or vice-versa -> diff --git a/addons/website/static/src/js/editor/editor_menu.js b/addons/website/static/src/js/editor/editor_menu.js index 7249e0d3741..e330df97001 100644 --- a/addons/website/static/src/js/editor/editor_menu.js +++ b/addons/website/static/src/js/editor/editor_menu.js @@ -2,6 +2,7 @@ odoo.define('website.editor.menu', function (require) { 'use strict'; var Dialog = require('web.Dialog'); +var dom = require('web.dom'); var Widget = require('web.Widget'); var core = require('web.core'); var Wysiwyg = require('web_editor.wysiwyg.root'); @@ -233,8 +234,9 @@ var EditorMenu = Widget.extend({ * * @private */ - _onSaveClick: function () { - this.save(); + _onSaveClick: function (ev) { + const restore = dom.addButtonLoadingEffect(ev.currentTarget); + this.save().then(restore).guardedCatch(restore); }, /** * @private diff --git a/addons/website/static/src/js/menu/navbar.js b/addons/website/static/src/js/menu/navbar.js index da86853f4e1..937392f88ec 100644 --- a/addons/website/static/src/js/menu/navbar.js +++ b/addons/website/static/src/js/menu/navbar.js @@ -180,12 +180,8 @@ var WebsiteNavbar = publicWidget.RootWidget.extend({ * @param {Event} ev */ _onActionMenuClick: function (ev) { - var $button = $(ev.currentTarget); - $button.prop('disabled', true); - var always = function () { - $button.prop('disabled', false); - }; - this._handleAction($button.data('action')).then(always).guardedCatch(always); + const restore = dom.addButtonLoadingEffect(ev.currentTarget); + this._handleAction($(ev.currentTarget).data('action')).then(restore).guardedCatch(restore); }, /** * Called when an action is asked to be executed from a child widget -> diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index 2dad9f8f87f..c14c72efe7f 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -1405,3 +1405,11 @@ $ribbon-padding: 100px; bottom: 0; } } + +.o_website_btn_loading { + cursor: wait; + opacity: $btn-disabled-opacity; + .fa:not(.fa-spin) { + display: none; + } +}