[FIX] web: correctly allow to leave form view

Scenario:
Open a record that is invalid in database (a required field is
empty), and click on the pager to go to another record.

Before this commit, we prevented the user from leaving the record,
marked the field as invalid and displayed a notification. This is
a weird UX experiment, since the user changed nothing (he did not
even enter edit mode). In this situation, since the record hasn't
been modified, the user should be allowed to leave. This is how
the legacy form view behaved.

closes odoo/odoo#100442

Signed-off-by: Géry Debongnie <ged@odoo.com>
This commit is contained in:
Aaron Bohy
2022-09-19 15:09:59 +02:00
parent 78daac46e2
commit adcde74532
3 changed files with 86 additions and 10 deletions
@@ -343,15 +343,15 @@ export class Record extends DataPoint {
return this.model.__bm__.localData[this.__bm_handle__].res_ids;
}
// -------------------------------------------------------------------------
// Getters
// -------------------------------------------------------------------------
async askChanges() {
const proms = [];
this.model.env.bus.trigger("RELATIONAL_MODEL:NEED_LOCAL_CHANGES", { proms });
return Promise.all([...proms, this._updatePromise]);
}
async checkValidity(urgent) {
if (!urgent) {
const proms = [];
this.model.env.bus.trigger("RELATIONAL_MODEL:NEED_LOCAL_CHANGES", { proms });
await Promise.all([...proms, this._updatePromise]);
await this.askChanges();
}
for (const fieldName in this.activeFields) {
const fieldType = this.fields[fieldName].type;
@@ -197,9 +197,13 @@ export class FormController extends Component {
limit: 1,
total: resIds.length,
onUpdate: async ({ offset }) => {
const canProceed = await this.model.root.save({ stayInEdition: true });
await this.model.root.askChanges(); // ensures that isDirty is correct
let canProceed = true;
if (this.model.root.isDirty) {
canProceed = await this.model.root.save({ stayInEdition: true });
}
if (canProceed) {
this.model.load({ resId: resIds[offset] });
return this.model.load({ resId: resIds[offset] });
}
},
};
@@ -4468,7 +4468,79 @@ QUnit.module("Views", (hooks) => {
assert.containsOnce(target, ".o_notification_manager .o_notification");
});
QUnit.test("keynav switching to another record from a dirty one", async function (assert) {
QUnit.test("keynav: switching to another record from an invalid one", async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: '<form><field name="foo" required="1"/></form>',
resIds: [1, 2],
resId: 1,
mockRPC(route) {
if (route === "/web/dataset/call_kw/partner/write") {
throw new Error("Shouldn't call write as the record is invalid");
}
},
});
assert.strictEqual(target.querySelector(".breadcrumb").innerText, "first record");
assert.hasClass(target.querySelector(".o_field_widget[name=foo]"), "o_required_modifier");
assert.strictEqual(target.querySelector(".o_pager_value").textContent, "1");
assert.strictEqual(target.querySelector(".o_pager_limit").textContent, "2");
await click(target.querySelector(".o_form_button_edit"));
await editInput(target, ".o_field_widget[name=foo] input", "");
triggerHotkey("alt+n");
await nextTick();
assert.strictEqual(target.querySelector(".breadcrumb").innerText, "first record");
assert.strictEqual(target.querySelector(".o_pager_value").textContent, "1");
assert.strictEqual(target.querySelector(".o_pager_limit").textContent, "2");
assert.hasClass(target.querySelector(".o_field_widget[name=foo]"), "o_field_invalid");
assert.containsOnce(target, ".o_notification_manager .o_notification");
});
QUnit.test("switching to another record from an invalid one (2)", async function (assert) {
// in this scenario, the record is already invalid in db, so we should be allowed to
// leave it
serverData.models.partner.records[0].foo = false;
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="foo" required="1"/>
</form>`,
resIds: [1, 2],
resId: 1,
});
assert.strictEqual(target.querySelector(".breadcrumb").innerText, "first record");
assert.hasClass(target.querySelector(".o_field_widget[name=foo]"), "o_required_modifier");
assert.strictEqual(target.querySelector(".o_pager_value").textContent, "1");
assert.strictEqual(target.querySelector(".o_pager_limit").textContent, "2");
await click(target.querySelector(".o_pager_next"));
assert.strictEqual(target.querySelector(".breadcrumb").innerText, "second record");
assert.strictEqual(target.querySelector(".o_pager_value").textContent, "2");
await click(target.querySelector(".o_pager_previous"));
assert.strictEqual(target.querySelector(".breadcrumb").innerText, "first record");
assert.hasClass(target.querySelector(".o_field_widget[name=foo]"), "o_required_modifier");
assert.strictEqual(target.querySelector(".o_pager_value").textContent, "1");
// same, but in edit mode
await click(target.querySelector(".o_form_button_edit"));
assert.containsOnce(target, ".o_form_editable");
await click(target.querySelector(".o_pager_next"));
assert.containsOnce(target, ".o_form_editable");
assert.strictEqual(target.querySelector(".breadcrumb").innerText, "second record");
assert.strictEqual(target.querySelector(".o_pager_value").textContent, "2");
});
QUnit.test("keynav: switching to another record from a dirty one", async function (assert) {
let nbWrite = 0;
await makeView({
type: "form",
@@ -4508,7 +4580,7 @@ QUnit.module("Views", (hooks) => {
input.value = "new value";
await triggerEvent(input, null, "input");
// click on the pager to switch to the next record (will save record)
// trigger the pager hotkey to switch to the next record (will save record)
triggerHotkey("alt+n");
await nextTick();
assert.containsNone(document.body, ".modal", "no confirm modal should be displayed");