[FIX] web: earlier record update for dirty fields

A field input always asks for a record update when it is the target of
a "change" event but such an event might occur after some keyboard
events (e.g. a "Enter"). In order to make keyboard navigation work, it
is necessary to make a dirty field communicate its new value before a
change event and make sure the record is correctly updated before being
saved.

Before this commit, this was done via a system based on a special event
triggered on the model each time a record is saved but that solution was
not appropriate:

- the updates done in that way were not correctly awaited.
- they would be asked for even if the save would not be the result of
  a keyboard event.

Here we replace the old system by a simpler one that works. When some
specific keyboard events occur, a dirty field ask for an update that is
correctly awaited.

closes odoo/odoo#96101

Related: odoo/enterprise#29873
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
FrancoisGe
2022-07-28 01:29:59 +02:00
parent 2c3b041bb1
commit c2ae235ca5
2 changed files with 61 additions and 24 deletions
@@ -1,5 +1,6 @@
/** @odoo-module **/
import { getActiveHotkey } from "@web/core/hotkeys/hotkey_service";
import { useBus } from "@web/core/utils/hooks";
const { useComponent, useEffect, useRef, useEnv } = owl;
@@ -52,25 +53,33 @@ export function useInputField(params) {
* However, if the field is invalid, the new value will not be committed to the model.
*/
function onChange(ev) {
isDirty = false;
isInvalid = false;
let val = ev.target.value;
if (params.parse) {
try {
val = params.parse(val);
} catch (_e) {
component.props.record.setInvalidField(component.props.name);
isInvalid = true;
if (isDirty) {
isDirty = false;
isInvalid = false;
let val = ev.target.value;
if (params.parse) {
try {
val = params.parse(val);
} catch (_e) {
component.props.record.setInvalidField(component.props.name);
isInvalid = true;
}
}
if (!isInvalid) {
component.props.update(val);
lastSetValue = ev.target.value;
}
if (component.props.setDirty) {
component.props.setDirty(isDirty);
}
}
if (!isInvalid) {
component.props.update(val);
lastSetValue = ev.target.value;
}
if (component.props.setDirty) {
component.props.setDirty(isDirty);
}
function onKeydown(ev) {
const hotkey = getActiveHotkey(ev);
if (["enter", "tab", "shift+tab"].includes(hotkey)) {
commitChanges(false);
}
}
@@ -79,9 +88,11 @@ export function useInputField(params) {
if (inputEl) {
inputEl.addEventListener("input", onInput);
inputEl.addEventListener("change", onChange);
inputEl.addEventListener("keydown", onKeydown);
return () => {
inputEl.removeEventListener("input", onInput);
inputEl.removeEventListener("change", onChange);
inputEl.removeEventListener("keydown", onKeydown);
};
}
},
@@ -102,12 +113,11 @@ export function useInputField(params) {
});
useBus(env.bus, "RELATIONAL_MODEL:WILL_SAVE_URGENTLY", () => commitChanges(true));
useBus(env.bus, "RELATIONAL_MODEL:NEED_LOCAL_CHANGES", () => commitChanges(false));
/**
* Roughly the same as onChange, but called at more specific / critical times. (See bus events)
*/
function commitChanges(urgent) {
async function commitChanges(urgent) {
if (!inputRef.el) {
return;
}
@@ -134,17 +144,16 @@ export function useInputField(params) {
}
}
if (component.props.setDirty) {
component.props.setDirty(isDirty);
}
if (isInvalid) {
return;
}
if (val !== component.props.value) {
component.props.update(val);
await component.props.update(val);
lastSetValue = inputRef.el.value;
if (component.props.setDirty) {
component.props.setDirty(isDirty);
}
}
}
}
@@ -14222,4 +14222,32 @@ QUnit.module("Views", (hooks) => {
assert.verifySteps(["get_views", "web_search_read"]);
}
);
QUnit.test("editable list correctly saves dirty fields ", async (assert) => {
serverData.models.foo.records = [serverData.models.foo.records[0]];
await makeView({
resModel: "foo",
type: "list",
arch: `<list editable="bottom">
<field name="display_name" />
</list>`,
serverData,
mockRPC(route, args) {
if (args.method === "write") {
assert.step("write");
assert.deepEqual(args.args, [[1], { display_name: "test" }]);
}
},
});
await click(target.querySelector(".o_data_cell"));
const input = target.querySelector(".o_data_cell input");
input.value = "test";
await triggerEvent(input, null, "input");
triggerHotkey("Tab");
await nextTick();
assert.verifySteps(["write"]);
});
});