From c2ae235ca50b18ec8d09cdf7cd410b3bc116b7d9 Mon Sep 17 00:00:00 2001 From: FrancoisGe Date: Tue, 19 Jul 2022 11:15:12 +0000 Subject: [PATCH] [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) --- .../src/views/fields/input_field_hook.js | 57 +++++++++++-------- .../web/static/tests/views/list_view_tests.js | 28 +++++++++ 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/addons/web/static/src/views/fields/input_field_hook.js b/addons/web/static/src/views/fields/input_field_hook.js index 44a5aa783b0..f4cacbb76ad 100644 --- a/addons/web/static/src/views/fields/input_field_hook.js +++ b/addons/web/static/src/views/fields/input_field_hook.js @@ -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); + } } } } diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index c135db5266c..478204d7488 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -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: ` + + `, + 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"]); + }); });