[FIX] web: Dialog buttons for form and x2many disabled

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) <mcm@odoo.com>
This commit is contained in:
Julien Carion (juca)
2023-07-05 11:37:03 +02:00
parent d49bceb161
commit da24433f65
5 changed files with 126 additions and 15 deletions
@@ -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:"));
@@ -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);
}
@@ -14,7 +14,7 @@
<t t-set-slot="layout-buttons">
<t t-if="env.inDialog">
<t t-if="footerArchInfo">
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="footerArchInfo" />
<t t-component="props.Renderer" record="model.root" Compiler="props.Compiler" archInfo="footerArchInfo"/>
</t>
<t t-else="">
<t t-call="{{ props.buttonTemplate }}"/>
@@ -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": `
<form>
<field name="parent_id"/>
</form>`,
};
const def = makeDeferred();
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="turtles">
<tree>
<field name="parent_id"/>
</tree>
<form>
<field name="parent_id"/>
</form>
</field>
</form>`,
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",
@@ -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": `
<form string="Partner">
<sheet>
<group>
<field name="name"/>
</group>
</sheet>
</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");
});
});