From f6a2086375a6831b9ae84a3cf8082a7a02475a89 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Mon, 3 Jul 2023 12:13:59 +0000 Subject: [PATCH] [FIX] web: wait for static list operation before saving Have a required many2manytags field and a slow network. The field is initially empty. Add a tag to the relation and directly save. An invalid field notification is displayed, because the field is still considered empty. This commit ensures that we wait for the operations done in the static list before saving. Part of task~3179751 Part-of: odoo/odoo#114024 --- .../src/model/relational_model/static_list.js | 66 +++++++++++-------- .../fields/many2many_tags_field_tests.js | 49 ++++++++++++++ 2 files changed, 86 insertions(+), 29 deletions(-) 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 5abb03d938a..7f7e98e02f8 100644 --- a/addons/web/static/src/model/relational_model/static_list.js +++ b/addons/web/static/src/model/relational_model/static_list.js @@ -168,9 +168,11 @@ export class StaticList extends DataPoint { }); } - async delete(record) { - await this._applyCommands([[x2ManyCommands.DELETE, record.resId || record._virtualId]]); - this._onUpdate(); + delete(record) { + return this.model.mutex.exec(async () => { + await this._applyCommands([[x2ManyCommands.DELETE, record.resId || record._virtualId]]); + await this._onUpdate(); + }); } async enterEditMode(record) { @@ -279,9 +281,11 @@ export class StaticList extends DataPoint { }); } - async forget(record) { - await this._applyCommands([[x2ManyCommands.FORGET, record.resId]]); - this._onUpdate(); + forget(record) { + return this.model.mutex.exec(async () => { + await this._applyCommands([[x2ManyCommands.FORGET, record.resId]]); + await this._onUpdate(); + }); } leaveEditMode({ discard, canAbandon, validate } = {}) { @@ -312,9 +316,11 @@ export class StaticList extends DataPoint { }); } - async linkTo(resId, serverData) { - await this._applyCommands([[x2ManyCommands.LINK_TO, resId, serverData]]); - this._onUpdate(); + linkTo(resId, serverData) { + return this.model.mutex.exec(async () => { + await this._applyCommands([[x2ManyCommands.LINK_TO, resId, serverData]]); + await this._onUpdate(); + }); } load({ limit, offset, orderBy } = {}) { @@ -338,27 +344,29 @@ export class StaticList extends DataPoint { } async replaceWith(ids, { reload = false, silent = false } = {}) { - const resIds = reload ? ids : ids.filter((id) => !this._cache[id]); - if (resIds.length) { - const records = await this.model._loadRecords({ - ...this.config, - resIds, - context: this.context, - }); - for (const record of records) { - this._createRecordDatapoint(record); + return this.model.mutex.exec(async () => { + const resIds = reload ? ids : ids.filter((id) => !this._cache[id]); + if (resIds.length) { + const records = await this.model._loadRecords({ + ...this.config, + resIds, + context: this.context, + }); + for (const record of records) { + this._createRecordDatapoint(record); + } } - } - this.records = ids.map((id) => this._cache[id]); - const updateCommandsToKeep = this._commands.filter( - (c) => c[0] === x2ManyCommands.UPDATE && ids.includes(c[1]) - ); - this._commands = [x2ManyCommands.replaceWith(ids)].concat(updateCommandsToKeep); - this._currentIds = [...ids]; - this.count = this._currentIds.length; - if (!silent) { - this._onUpdate(); - } + this.records = ids.map((id) => this._cache[id]); + const updateCommandsToKeep = this._commands.filter( + (c) => c[0] === x2ManyCommands.UPDATE && ids.includes(c[1]) + ); + this._commands = [x2ManyCommands.replaceWith(ids)].concat(updateCommandsToKeep); + this._currentIds = [...ids]; + this.count = this._currentIds.length; + if (!silent) { + await this._onUpdate(); + } + }); } async resequence(movedId, targetId) { diff --git a/addons/web/static/tests/views/fields/many2many_tags_field_tests.js b/addons/web/static/tests/views/fields/many2many_tags_field_tests.js index fb0436e776c..89347862c85 100644 --- a/addons/web/static/tests/views/fields/many2many_tags_field_tests.js +++ b/addons/web/static/tests/views/fields/many2many_tags_field_tests.js @@ -1758,6 +1758,55 @@ QUnit.module("Fields", (hooks) => { assert.containsOnce(target, "[name='timmy'].o_field_invalid"); }); + QUnit.test("set a required many2many_tags and save directly", async function (assert) { + let def; + const form = await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: '
', + async mockRPC(route, args) { + assert.step(args.method); + if (args.method === "web_read") { + await def; + } + } + }); + patchWithCleanup(form.env.services.notification, { + add: () => assert.step("notification"), + }); + + assert.verifySteps([ + "get_views", + "onchange2", + ]); + + assert.containsNone(target, ".o_tag"); + + def = makeDeferred(); + await clickDropdown(target, "timmy"); + await clickOpenedDropdownItem(target, "timmy", "gold"); + assert.containsNone(target, ".o_tag"); + + assert.verifySteps([ + "name_search", + "web_read", + ]); + + await clickSave(target); + assert.doesNotHaveClass(target, "[name='timmy']", "o_field_invalid"); + + assert.verifySteps([]); + + def.resolve(); + await nextTick(); + + assert.verifySteps([ + "create", + "web_read", + ]); + }); + QUnit.test("Many2ManyTagsField with option 'no_quick_create' set to true", async (assert) => { serverData.views = { "partner_type,false,form": `
`,