[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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user