[FIX] web: save a x2m with an invalid new record

The purpose of this commit is to prevent saving to a form view
if you have created and edited a new invalid record in an x2many.

How to reproduce
- go to a form view with an x2m
- create a new record with at least one required field empty in the x2m
- edit another field than the required one
- click on the save button

Before this commit:
- The view is switched to readonly mode and the new record is deleted.

After this commit:
- The view stays in edit mode, the new record is not deleted and
a notification indicating invalid fields is displayed.

closes odoo/odoo#99288

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
FrancoisGe
2022-09-01 17:11:08 +02:00
parent 826ac1771d
commit c70d01f429
3 changed files with 64 additions and 48 deletions
@@ -382,18 +382,18 @@ export class Record extends DataPoint {
continue;
case "properties":
if (!this.checkPropertiesValidity(fieldName)) {
this.setInvalidField(fieldName);
this._setInvalidField(fieldName);
}
break;
case "one2many":
case "many2many":
if (!(await this.checkX2ManyValidity(fieldName))) {
this.setInvalidField(fieldName);
this._setInvalidField(fieldName);
}
break;
default:
if (!isSet && this.isRequired(fieldName) && !this.data[fieldName]) {
this.setInvalidField(fieldName);
this._setInvalidField(fieldName);
}
}
}
@@ -454,7 +454,7 @@ export class Record extends DataPoint {
const list = this.data[fieldName];
const record = list.editedRecord;
if (record && record.isNew && !(await record.checkValidity())) {
if (record.canBeAbandoned) {
if (record.canBeAbandoned && !record.isDirty) {
list.abandonRecord(record.id);
} else {
return false;
@@ -481,11 +481,15 @@ export class Record extends DataPoint {
);
}
setInvalidField(fieldName) {
_setInvalidField(fieldName) {
this._invalidFields.add(fieldName);
this.model.notify();
}
setInvalidField(fieldName) {
const bm = this.model.__bm__;
bm.setDirty(this.__bm_handle__);
this.model.notify();
this._setInvalidField(fieldName);
}
isInvalid(fieldName) {
@@ -1134,31 +1138,24 @@ export class StaticList extends DataPoint {
async unselectRecord(canDiscard = false) {
// something seems wrong with switchMode --> review system?
const editedRecord = this.editedRecord;
if (editedRecord) {
const handle = editedRecord.__bm_handle__;
if (canDiscard && editedRecord.isNew && !editedRecord.isDirty) {
this.model.__bm__.discardChanges(handle);
if (editedRecord.canBeAbandoned) {
this.model.__bm__.removeLine(handle);
}
this.__syncData();
this.editedRecord = null;
return await editedRecord.switchMode("readonly");
} else if (await editedRecord.checkValidity()) {
return await editedRecord.switchMode("readonly");
} else {
if (!editedRecord.isDirty) {
this.model.__bm__.discardChanges(handle);
if (editedRecord.canBeAbandoned) {
this.model.__bm__.removeLine(handle);
}
this.editedRecord = null;
return await editedRecord.switchMode("readonly");
}
return false;
}
if (!editedRecord) {
return true;
}
return true;
const isValid = await editedRecord.checkValidity();
const handle = editedRecord.__bm_handle__;
if (!editedRecord.isDirty && ((canDiscard && editedRecord.isNew) || !isValid)) {
this.model.__bm__.discardChanges(handle);
if (editedRecord.canBeAbandoned) {
this.model.__bm__.removeLine(handle);
}
this.__syncData();
this.editedRecord = null;
return await editedRecord.switchMode("readonly");
} else if (isValid) {
return await editedRecord.switchMode("readonly");
}
return false;
}
}
@@ -7,6 +7,7 @@ import { session } from "@web/session";
import { Field } from "@web/views/fields/field";
import { Record } from "@web/views/record";
import {
addRow,
click,
clickDiscard,
clickDropdown,
@@ -2120,24 +2121,7 @@ QUnit.module("Fields", (hooks) => {
});
// Click on "Add an item"
await click(target, ".o_field_x2many_list_row_add a");
target.querySelector(".o_field_widget.o_field_char[name=display_name] input").value =
"some text";
assert.containsOnce(
target,
".o_field_widget.o_field_char[name=display_name]",
"should have a char field 'display_name' on this record"
);
assert.doesNotHaveClass(
target.querySelector(".o_field_widget.o_field_char[name=display_name]"),
"o_required_modifier",
"the char field should not be required on this record"
);
assert.strictEqual(
target.querySelector(".o_field_widget.o_field_char[name=display_name] input").value,
"some text",
"should have entered text in the char field on this record"
);
await addRow(target);
assert.containsOnce(
target,
".o_field_widget.o_required_modifier[name=trululu]",
@@ -12562,4 +12562,39 @@ QUnit.module("Fields", (hooks) => {
await triggerEvents(target, ".o_data_row", ["touchstart", "touchend"]);
assert.containsNone(target, ".o_data_row_selected");
});
QUnit.test(
"save a record after creating and editing a new invalid record in a one2many",
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="p">
<tree editable="bottom">
<field name="display_name" required="1"/>
<field name="int_field"/>
</tree>
</field>
</form>`,
resId: 1,
});
await clickEdit(target);
await addRow(target);
await editInput(target, ".o_field_widget[name=int_field] input", "3");
await clickSave(target);
assert.containsOnce(
target,
".o_data_row.o_selected_row",
"line should not have been removed and should still be in edition"
);
assert.hasClass(
target.querySelector(".o_field_widget[name=display_name]"),
"o_field_invalid"
);
}
);
});