From 17de016dc6cfa75cbd219fad631a294e3202be25 Mon Sep 17 00:00:00 2001 From: FrancoisGe Date: Tue, 4 Jul 2023 15:44:01 +0000 Subject: [PATCH] [FIX] web: save a record before the onchange is complete in o2m Before this commit, the save button in the x2m form view dialog does not wait for ongoing updates. Why is this? When you click on the "Save and Close" button of a form view dialog in an x2m, you check the validity of the record to know whether you should save or not. The askChanges function called does not wait for ongoing updates. Solution: Just wait until the model mutex has finished processing all the current functions. This ensures that the record has applied all its updates. How to reproduce: - Go to a form view with a non-editable x2m - Add a line - Edit a required field with a slow onchange - Click "Save and close" before the onchange is complete Before this commit: The form dialog remains open and a notification appears telling us that a required field is invalid. After this commit: The dialog is closed and the change has been applied. Part-of: odoo/odoo#114024 --- .../src/model/relational_model/record.js | 2 +- .../src/model/relational_model/static_list.js | 6 +- .../static/src/views/list/list_renderer.js | 2 +- .../views/fields/one2many_field_tests.js | 58 +++++++++++++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/addons/web/static/src/model/relational_model/record.js b/addons/web/static/src/model/relational_model/record.js index 940a66ea7e8..48fe6b01478 100644 --- a/addons/web/static/src/model/relational_model/record.js +++ b/addons/web/static/src/model/relational_model/record.js @@ -307,7 +307,7 @@ export class Record extends DataPoint { _askChanges() { const proms = []; this.model.bus.trigger("NEED_LOCAL_CHANGES", { proms }); - return Promise.all(proms); + return Promise.all([...proms, this.model.mutex.getUnlockedDef()]); } _checkValidity({ silent } = {}) { diff --git a/addons/web/static/src/model/relational_model/static_list.js b/addons/web/static/src/model/relational_model/static_list.js index 7f7e98e02f8..ad68f56d551 100644 --- a/addons/web/static/src/model/relational_model/static_list.js +++ b/addons/web/static/src/model/relational_model/static_list.js @@ -288,10 +288,12 @@ export class StaticList extends DataPoint { }); } - leaveEditMode({ discard, canAbandon, validate } = {}) { + async leaveEditMode({ discard, canAbandon, validate } = {}) { + if (this.editedRecord) { + await this.editedRecord._askChanges(false); + } return this.model.mutex.exec(async () => { if (this.editedRecord) { - await this.editedRecord._askChanges(); const isValid = this.editedRecord._checkValidity(); if (!isValid && validate) { return false; diff --git a/addons/web/static/src/views/list/list_renderer.js b/addons/web/static/src/views/list/list_renderer.js index df2af077a5e..c698430d469 100644 --- a/addons/web/static/src/views/list/list_renderer.js +++ b/addons/web/static/src/views/list/list_renderer.js @@ -2024,7 +2024,7 @@ export class ListRenderer extends Component { * @param {HTMLElement} [params.previous] */ async sortDrop(dataRowId, { element, previous }) { - this.props.list.leaveEditMode(); + await this.props.list.leaveEditMode(); element.classList.remove("o_row_draggable"); const refId = previous ? previous.dataset.id : null; this.resequencePromise = this.props.list.resequence(dataRowId, refId, { 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 67819ef45db..a8bf7015eb6 100644 --- a/addons/web/static/tests/views/fields/one2many_field_tests.js +++ b/addons/web/static/tests/views/fields/one2many_field_tests.js @@ -13177,4 +13177,62 @@ QUnit.module("Fields", (hooks) => { assert.containsOnce(target, ".o_list_view"); assert.verifySteps([]); }); + + QUnit.test( + "one2many: save a record before the onchange is complete in a form dialog", + async function (assert) { + serverData.models.turtle.onchanges = { + display_name: function () {}, + }; + + serverData.views = { + "turtle,false,form": ` +
+ + `, + }; + + const def = makeDeferred(); + await makeView({ + type: "form", + resModel: "partner", + serverData, + resId: 1, + arch: ` +
+ + + + + +
`, + async mockRPC(route, args) { + if ( + args.method === "onchange2" && + args.args[2].length === 1 && + args.args[2][0] === "display_name" + ) { + await def; + } + }, + }); + await addRow(target); + assert.containsOnce(target, ".modal"); + + await editInput(target, ".o_field_widget[name=display_name] input", "new name"); + await click(target, ".modal .o_form_button_save"); + assert.containsOnce(target, ".modal"); + + def.resolve(); + await nextTick(); + assert.containsNone(target, ".modal"); + assert.containsN(target, ".o_data_row", 2); + assert.deepEqual( + [...target.querySelectorAll(".o_data_row [name='display_name']")].map( + (el) => el.textContent + ), + ["donatello", "new name"] + ); + } + ); });