[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:
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user