diff --git a/addons/web/static/src/views/basic_relational_model.js b/addons/web/static/src/views/basic_relational_model.js index 4ba0d9397c5..c6c7d15dc73 100644 --- a/addons/web/static/src/views/basic_relational_model.js +++ b/addons/web/static/src/views/basic_relational_model.js @@ -335,7 +335,12 @@ export class Record extends DataPoint { // Getters // ------------------------------------------------------------------------- - checkValidity() { + async checkValidity(urgent) { + if (!urgent) { + const proms = []; + this.model.env.bus.trigger("RELATIONAL_MODEL:NEED_LOCAL_CHANGES", { proms }); + await Promise.all([...proms, this._updatePromise]); + } for (const fieldName in this.activeFields) { const fieldType = this.fields[fieldName].type; if (fieldName in this._requiredFields) { @@ -352,7 +357,7 @@ export class Record extends DataPoint { continue; case "one2many": case "many2many": - if (!this.checkX2ManyValidity(fieldName)) { + if (!(await this.checkX2ManyValidity(fieldName))) { this.setInvalidField(fieldName); } break; @@ -414,10 +419,10 @@ export class Record extends DataPoint { return evalDomain(required, this.evalContext); } - checkX2ManyValidity(fieldName) { + async checkX2ManyValidity(fieldName) { const list = this.data[fieldName]; const record = list.editedRecord; - if (record && record.isNew && !record.checkValidity()) { + if (record && record.isNew && !(await record.checkValidity())) { if (record.canBeAbandoned) { list.abandonRecord(record.id); } else { @@ -603,7 +608,9 @@ export class Record extends DataPoint { throw new Error("couldn't find x2many field name"); } const operation = { operation: "UPDATE", id: this.__bm_handle__, data }; - await this.__syncParent(operation); + const prom = this.__syncParent(operation); + prom.catch(resolveUpdatePromise); + await prom; } else { const prom = this.model.__bm__.notifyChanges(this.__bm_handle__, data, { viewType: this.__viewType, @@ -631,11 +638,8 @@ export class Record extends DataPoint { this._savePromise = new Promise((r) => { resolveSavePromise = r; }); - const proms = []; - this.model.env.bus.trigger("RELATIONAL_MODEL:NEED_LOCAL_CHANGES", { proms }); - await Promise.all([...proms, this._updatePromise]); this._closeInvalidFieldsNotification(); - if (!this.checkValidity()) { + if (!(await this.checkValidity())) { const invalidFields = [...this._invalidFields].map((fieldName) => { return `
  • ${escape(this.fields[fieldName].string || fieldName)}
  • `; }, this); @@ -702,7 +706,7 @@ export class Record extends DataPoint { this.model.env.bus.trigger("RELATIONAL_MODEL:WILL_SAVE_URGENTLY"); await Promise.resolve(); this.__syncData(); - if (this.isDirty && this.checkValidity()) { + if (this.isDirty && (await this.checkValidity(true))) { this.model.__bm__.save(this.__bm_handle__, { reload: false }); } this.model.__bm__.bypassMutex = false; @@ -782,14 +786,14 @@ export class StaticList extends DataPoint { const editedRecord = this.editedRecord; if (editedRecord && editedRecord.id === record.id && mode === "readonly") { - const valid = record.checkValidity(); + const valid = await record.checkValidity(); if (valid) { this.editedRecord = null; } return valid; } if (editedRecord) { - const isValid = editedRecord.checkValidity(); + const isValid = await editedRecord.checkValidity(); if (!isValid) { if (editedRecord.canBeAbandoned) { this.abandonRecord(editedRecord.id); @@ -1091,7 +1095,7 @@ export class StaticList extends DataPoint { this.__syncData(); this.editedRecord = null; await editedRecord.switchMode("readonly"); - } else if (editedRecord.checkValidity()) { + } else if (await editedRecord.checkValidity()) { await editedRecord.switchMode("readonly"); } else { if (!editedRecord.isDirty) { diff --git a/addons/web/static/src/views/fields/relational_utils.js b/addons/web/static/src/views/fields/relational_utils.js index f7fc9e143e3..d114231aef8 100644 --- a/addons/web/static/src/views/fields/relational_utils.js +++ b/addons/web/static/src/views/fields/relational_utils.js @@ -477,7 +477,7 @@ class X2ManyFieldDialog extends owl.Component { } async save({ saveAndNew }) { - if (this.record.checkValidity()) { + if (await this.record.checkValidity()) { this.record = (await this.props.save(this.record, { saveAndNew })) || this.record; } else { return false; diff --git a/addons/web/static/src/views/relational_model.js b/addons/web/static/src/views/relational_model.js index 84d558ce7ae..c3ebbdfd2bd 100644 --- a/addons/web/static/src/views/relational_model.js +++ b/addons/web/static/src/views/relational_model.js @@ -101,8 +101,9 @@ async function toggleArchive(model, resModel, resIds, doArchive, context) { async function unselectRecord(editedRecord, abandonRecord) { if (editedRecord) { + const isValid = await editedRecord.checkValidity(); const canBeAbandoned = editedRecord.canBeAbandoned; - if (!canBeAbandoned && editedRecord.checkValidity()) { + if (isValid && !canBeAbandoned) { return editedRecord.switchMode("readonly"); } else if (canBeAbandoned) { return abandonRecord(editedRecord.id); @@ -552,7 +553,16 @@ export class Record extends DataPoint { this.invalidateCache(); } - checkValidity() { + async checkValidity() { + if (!this._urgentSave) { + const proms = []; + this.model.env.bus.trigger("RELATIONAL_MODEL:NEED_LOCAL_CHANGES", { proms }); + await Promise.all([...proms, this.model.mutex.getUnlockedDef()]); + } + return this._checkValidity(); + } + + _checkValidity() { for (const fieldName in this._requiredFields) { const fieldType = this.fields[fieldName].type; if (!evalDomain(this._requiredFields[fieldName], this.evalContext)) { @@ -737,7 +747,7 @@ export class Record extends DataPoint { isX2ManyValid(fieldName) { const value = this.data[fieldName]; - return value.records.every((r) => r.checkValidity()); + return value.records.every(async (r) => await r.checkValidity()); } async load(params = {}) { @@ -1214,7 +1224,7 @@ export class Record extends DataPoint { */ async _save(options = { stayInEdition: false, noReload: false }) { this.model.env.bus.trigger("RELATIONAL_MODEL:NEED_LOCAL_CHANGES"); - if (!this.checkValidity()) { + if (!(await this._checkValidity())) { const invalidFields = [...this._invalidFields].map((fieldName) => { return `
  • ${escape(this.fields[fieldName].string || fieldName)}
  • `; }, this); @@ -2580,7 +2590,7 @@ export class StaticList extends DataPoint { this.onRecordWillSwitchMode = async (record, mode) => { const editedRecord = this.editedRecord; if (editedRecord && editedRecord.id === record.id && mode === "readonly") { - const valid = record.checkValidity(); + const valid = await record.checkValidity(); if (valid) { this.editedRecord = null; } 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 4c6415cca1c..41e24be93ea 100644 --- a/addons/web/static/tests/views/fields/one2many_field_tests.js +++ b/addons/web/static/tests/views/fields/one2many_field_tests.js @@ -9559,6 +9559,63 @@ QUnit.module("Fields", (hooks) => { assert.verifySteps(["onchange"]); }); + QUnit.test("edit a field with a slow onchange in one2many", async function (assert) { + serverData.models.turtle.onchanges = { + turtle_foo: function () {}, + }; + + let def; + await makeView({ + type: "form", + resModel: "partner", + serverData, + arch: ` +
    + + + + + +
    `, + async mockRPC(route, args) { + assert.step(args.method); + if (args.method === "onchange") { + await Promise.resolve(def); + } + }, + }); + + assert.verifySteps(["get_views", "onchange"]); + + const value = "hello"; + + // add a new line + await addRow(target); + + assert.verifySteps(["onchange"]); + + // we want to add a delay to simulate an onchange + def = makeDeferred(); + + // write something in the field + await editInput(target, "[name=turtle_foo] input", value); + assert.strictEqual(target.querySelector("[name=turtle_foo] input").value, value); + + await click(target, ".o_form_view"); + + // check that nothing changed before the onchange finished + assert.strictEqual(target.querySelector("[name=turtle_foo] input").value, value); + + assert.verifySteps(["onchange"]); + + // unlock onchange + def.resolve(); + await nextTick(); + + // check the current line is added with the correct content + assert.strictEqual(target.querySelector(".o_data_row [name=turtle_foo]").innerText, value); + }); + QUnit.test( "no deadlock when leaving a one2many line with uncommitted changes", async function (assert) { diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index 478204d7488..2c35930d2a7 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -14250,4 +14250,57 @@ QUnit.module("Views", (hooks) => { assert.verifySteps(["write"]); }); + + QUnit.test("edit a field with a slow onchange in a new row", async function (assert) { + serverData.models.foo.onchanges = { + int_field: function () {}, + }; + serverData.models.foo.records = []; + + let def; + await makeView({ + type: "list", + resModel: "foo", + serverData, + arch: ` + + + `, + async mockRPC(route, args) { + assert.step(args.method); + if (args.method === "onchange") { + await Promise.resolve(def); + } + }, + }); + + assert.verifySteps(["get_views", "web_search_read"]); + + const value = "14"; + // add a new line + await click(target.querySelector(".o_list_button_add")); + + assert.verifySteps(["onchange"]); + + // we want to add a delay to simulate an onchange + def = makeDeferred(); + + // write something in the field + await editInput(target, "[name=int_field] input", value); + assert.strictEqual(target.querySelector("[name=int_field] input").value, value); + + await click(target, ".o_list_view"); + + // check that nothing changed before the onchange finished + assert.strictEqual(target.querySelector("[name=int_field] input").value, value); + assert.verifySteps(["onchange"]); + + // unlock onchange + def.resolve(); + await nextTick(); + + // check the current line is added with the correct content + assert.strictEqual(target.querySelector(".o_data_row [name=int_field]").innerText, value); + assert.verifySteps(["create", "read"]); + }); });