[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:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user