[IMP] web: mark field as invalid if onchange returns error

Before this commit, if an onchange returned an error, the faulty
value was kept in the UI and there was no feedback given to the
user besides the error being displayed in a dialog. However, the
former value was still the one stored in the model, which produced
a discrepancy between model and UI.

This commit tackles the issue by marking the field as invalid in
this case (like when you input letters inside a numeric field).
That way, the value in the model is still different from the one
in the UI, but the field is stored in the list of invalid fields,
it is displayed in red, and the view can't be saved (it can be
discarded though).

Task~3498849 (master version, which differs from 16.0, see [1])

[1] odoo/odoo#134792

closes odoo/odoo#135017

Signed-off-by: Pierre Rousseau (pro) <pro@odoo.com>
This commit is contained in:
Aaron Bohy
2023-09-11 22:41:09 +00:00
parent b9391de7a4
commit 2582c488b3
4 changed files with 74 additions and 7 deletions
@@ -1022,6 +1022,12 @@ export class Record extends DataPoint {
changes: localChanges,
fieldNames: onChangeFields,
evalContext: this.evalContext,
onError: (e) => {
for (const fieldName in localChanges) {
this._setInvalidField(fieldName);
}
throw e;
},
});
Object.assign(changes, this._parseServerValues(otherChanges, this.data));
}
@@ -575,7 +575,10 @@ export class RelationalModel extends Model {
* @param {Object} [param.evalContext=config.context]
* @returns Promise<Object>
*/
async _onchange(config, { changes = {}, fieldNames = [], evalContext = config.context }) {
async _onchange(
config,
{ changes = {}, fieldNames = [], evalContext = config.context, onError }
) {
const { fields, activeFields, resModel, resId } = config;
let context = config.context;
if (fieldNames.length === 1) {
@@ -584,7 +587,15 @@ export class RelationalModel extends Model {
}
const spec = getFieldsSpec(activeFields, fields, evalContext, { withInvisible: true });
const args = [resId ? [resId] : [], changes, fieldNames, spec];
const response = await this.orm.call(resModel, "onchange", args, { context });
let response;
try {
response = await this.orm.call(resModel, "onchange", args, { context });
} catch (e) {
if (onError) {
return onError(e);
}
throw e;
}
if (response.warning) {
const { type, title, message, className, sticky } = response.warning;
if (type === "dialog") {
@@ -8923,7 +8923,7 @@ QUnit.module("Fields", (hooks) => {
);
QUnit.test("display correct value after validation error", async function (assert) {
assert.expect(4);
assert.expect(5);
/*
* By-pass QUnit's and test's error handling because the error service needs to be active
@@ -9002,10 +9002,8 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, ".o_field_widget[name=turtle_foo] input", "pinky");
await click(target, ".o_form_view");
assert.strictEqual(target.querySelector(".o_data_row .o_data_cell").textContent, "foo");
// we make sure here that when we save, the values are the current
// values displayed in the field.
await clickSave(target);
assert.hasClass(target.querySelector(".o_field_widget[name=turtles]"), "o_field_invalid");
assert.ok(target.querySelector(".o_form_button_save").disabled);
});
QUnit.test("propagate context to sub views without default_* keys", async function (assert) {
@@ -6284,6 +6284,58 @@ QUnit.module("Views", (hooks) => {
assert.containsOnce(document.body, ".o_notification");
});
QUnit.test("onchange returns an error", async function (assert) {
registry.category("services").add("error", errorService);
registry.category("error_dialogs").add("odoo.exceptions.UserError", WarningDialog);
// remove the override in qunit.js that swallows unhandledrejection errors
// s.t. we let the error service handle them
const originalOnUnhandledRejection = window.onunhandledrejection;
window.onunhandledrejection = () => {};
registerCleanup(() => {
window.onunhandledrejection = originalOnUnhandledRejection;
});
serverData.models.partner.onchanges = { int_field: () => {} };
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `<form><field name="int_field"/></form>`,
resId: 2,
mockRPC(route, args) {
if (args.method === "onchange" && args.args[1].int_field === 64) {
throw makeServerError({ message: "Some business message" });
}
},
});
assert.strictEqual(
target.querySelector(".o_field_widget[name=int_field] input").value,
"9"
);
await editInput(target, ".o_field_widget[name=int_field] input", 64);
assert.containsOnce(document.body, ".modal");
assert.strictEqual(
document.body.querySelector(".modal-body").textContent,
"Some business message"
);
assert.hasClass(
target.querySelector('.o_field_widget[name="int_field"]'),
"o_field_invalid"
);
await click(target.querySelector(".modal .btn-close"));
assert.containsNone(document.body, ".modal");
await editInput(target, ".o_field_widget[name=int_field] input", 32);
assert.containsNone(document.body, ".modal");
assert.containsNone(target, ".o_field_invalid");
});
QUnit.test("button box is rendered in create mode", async function (assert) {
await makeView({
type: "form",