From da24433f65c6d352a688cb8dc7cd0320ad0f5958 Mon Sep 17 00:00:00 2001 From: "Julien Carion (juca)" Date: Fri, 28 Apr 2023 15:30:31 +0000 Subject: [PATCH] [FIX] web: Dialog buttons for form and x2many disabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes an issue with dialog buttons where it is possible to trigger their action multiple times if the network is slow. This is solved in form dialogs and x2many dialogs by disabling the buttons when the user clicks on them. opw-3291929 closes odoo/odoo#120151 Signed-off-by: Michaƫl Mattiello (mcm) --- .../src/views/fields/relational_utils.js | 5 +- .../static/src/views/form/form_controller.js | 21 +++--- .../static/src/views/form/form_controller.xml | 2 +- .../views/fields/one2many_field_tests.js | 67 +++++++++++++++++++ .../view_dialogs/form_view_dialog_tests.js | 46 +++++++++++++ 5 files changed, 126 insertions(+), 15 deletions(-) diff --git a/addons/web/static/src/views/fields/relational_utils.js b/addons/web/static/src/views/fields/relational_utils.js index 2a41ca35c98..56d9701dea0 100644 --- a/addons/web/static/src/views/fields/relational_utils.js +++ b/addons/web/static/src/views/fields/relational_utils.js @@ -571,15 +571,18 @@ export class X2ManyFieldDialog extends Component { } async save({ saveAndNew }) { + const disabledButtons = this.disableButtons(); if (await this.record.checkValidity()) { this.record = (await this.props.save(this.record, { saveAndNew })) || this.record; } else { this.record.openInvalidFieldsNotification(); + this.enableButtons(disabledButtons); return false; } if (!saveAndNew) { this.props.close(); } + this.enableButtons(disabledButtons); return true; } @@ -589,9 +592,7 @@ export class X2ManyFieldDialog extends Component { } async saveAndNew() { - const disabledButtons = this.disableButtons(); const saved = await this.save({ saveAndNew: true }); - this.enableButtons(disabledButtons); if (saved) { if (this.title) { this.title = this.title.replace(this.env._t("Open:"), this.env._t("New:")); diff --git a/addons/web/static/src/views/form/form_controller.js b/addons/web/static/src/views/form/form_controller.js index f32b34892c5..7026cfb04f8 100644 --- a/addons/web/static/src/views/form/form_controller.js +++ b/addons/web/static/src/views/form/form_controller.js @@ -117,8 +117,6 @@ export class FormController extends Component { this.canCreate = create && !this.props.preventCreate; this.canEdit = edit && !this.props.preventEdit; - this.disabledButtons = null; - let mode = this.props.mode || "edit"; if (!this.canEdit) { mode = "readonly"; @@ -413,18 +411,17 @@ export class FormController extends Component { } disableButtons() { - const btns = [...this.rootRef.el.querySelectorAll("button:not([disabled])")]; + const btns = [...this.ui.activeElement.querySelectorAll("button:not([disabled])")]; for (const btn of btns) { - btn.setAttribute("disabled", "1"); + btn.setAttribute("disabled", ""); } - this.disabledButtons = btns; + return btns; } - enableButtons() { - for (const btn of this.disabledButtons) { + enableButtons(btns) { + for (const btn of btns) { btn.removeAttribute("disabled"); } - this.disabledButtons = null; } async beforeExecuteActionButton(clickParams) { @@ -459,14 +456,14 @@ export class FormController extends Component { }); } if (canProceed) { - this.disableButtons(); + const btns = this.disableButtons(); await this.model.load({ resId: null }); - this.enableButtons(); + this.enableButtons(btns); } } async saveButtonClicked(params = {}) { - this.disableButtons(); + const btns = this.disableButtons(); const record = this.model.root; let saved = false; @@ -475,7 +472,7 @@ export class FormController extends Component { } else { saved = await record.save(params); } - this.enableButtons(); + this.enableButtons(btns); if (saved && this.props.onSave) { this.props.onSave(record, params); } diff --git a/addons/web/static/src/views/form/form_controller.xml b/addons/web/static/src/views/form/form_controller.xml index 22a93003f4d..3f4ec888481 100644 --- a/addons/web/static/src/views/form/form_controller.xml +++ b/addons/web/static/src/views/form/form_controller.xml @@ -14,7 +14,7 @@ - + diff --git a/addons/web/static/tests/views/fields/one2many_field_tests.js b/addons/web/static/tests/views/fields/one2many_field_tests.js index 71c11f3b1c4..e630a9d858f 100644 --- a/addons/web/static/tests/views/fields/one2many_field_tests.js +++ b/addons/web/static/tests/views/fields/one2many_field_tests.js @@ -402,6 +402,73 @@ QUnit.module("Fields", (hooks) => { await click(target, ".o_field_many2one input"); }); + QUnit.test("O2M modal buttons are disabled on click", async function (assert) { + // Records in an o2m can have a m2o pointing to themselves. + // In that case, a domain evaluation on that field followed by name_search + // shouldn't send virtual_ids to the server. + + patchWithCleanup(browser, { + setTimeout: (fn) => fn(), + }); + + serverData.models.turtle.fields.parent_id = { + string: "Parent", + type: "many2one", + relation: "turtle", + }; + serverData.views = { + "turtle,false,form": ` +
+ + `, + }; + const def = makeDeferred(); + await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: ` +
+ + + + + + + + + `, + async mockRPC(route, args) { + if (args.method === "create") { + await def; + } + }, + }); + + await addRow(target); + + await clickOpenM2ODropdown(target, "parent_id"); + await editInput(target, ".o_field_widget[name=parent_id] input", "ABC"); + await clickOpenedDropdownItem(target, "parent_id", "Create and edit..."); + await click( + target.querySelector(".modal:not(.o_inactive_modal) .modal-footer .o_form_button_save") + ); + assert.strictEqual( + target + .querySelector(".modal:not(.o_inactive_modal) .modal-footer .o_form_button_save") + .getAttribute("disabled"), + "" + ); + def.resolve(); + await nextTick(); + // close all dialogs + await click( + target.querySelector(".modal:not(.o_inactive_modal) .modal-footer .o_form_button_save") + ); + await nextTick(); + assert.containsNone(target, ".o_dialog .o_form_view"); + }); + QUnit.test("resequence a x2m in a form view dialog from another x2m", async function (assert) { await makeView({ type: "form", diff --git a/addons/web/static/tests/views/view_dialogs/form_view_dialog_tests.js b/addons/web/static/tests/views/view_dialogs/form_view_dialog_tests.js index c1d1eb3ba0c..4a17446afc7 100644 --- a/addons/web/static/tests/views/view_dialogs/form_view_dialog_tests.js +++ b/addons/web/static/tests/views/view_dialogs/form_view_dialog_tests.js @@ -2,10 +2,12 @@ import { click, + editInput, getFixture, nextTick, patchWithCleanup, triggerHotkey, + makeDeferred, } from "@web/../tests/helpers/utils"; import { makeViewInDialog } from "@web/../tests/views/helpers"; import { createWebClient } from "@web/../tests/webclient/helpers"; @@ -341,4 +343,48 @@ QUnit.module("ViewDialogs", (hooks) => { assert.verifySteps(["remove"]); assert.containsNone(target, ".o_dialog .o_form_view"); }); + + QUnit.test("Buttons are set as disabled on click", async function (assert) { + serverData.views = { + "partner,false,form": ` +
+ + + + + +
+ `, + }; + const def = makeDeferred(); + async function mockRPC(route, args) { + if (args.method === "write") { + await def; + } + } + const webClient = await createWebClient({ serverData, mockRPC }); + webClient.env.services.dialog.add(FormViewDialog, { + resModel: "partner", + resId: 1, + }); + + await nextTick(); + await editInput( + target.querySelector(".o_dialog .o_content .o_field_char .o_input"), + "", + "test" + ); + + await click(target.querySelector(".o_dialog .modal-footer .o_form_button_save")); + assert.strictEqual( + target + .querySelector(".o_dialog .modal-footer .o_form_button_save") + .getAttribute("disabled"), + "" + ); + + def.resolve(); + await nextTick(); + assert.containsNone(target, ".o_dialog .o_form_view"); + }); });