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"]);
+ });
});