[FIX] web: autocomplete inEdition

When a user is editing an Autocomplete, we don't want to delete its value
if the props value changes.

For example:
- In a form view with a many2one field and another field with an onchange
- Edit the second field
- Insert a value in the many2one input
- The onchange of the first field returns a value for the many2one

Before this commit:
    The value inserted in the many2one is lost. It is replaced by the
    onchange value

After this commit:
    The value inserted in the many2one is unchanged.

closes odoo/odoo#136654

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
FrancoisGe
2023-09-28 01:51:24 +00:00
parent 24cf7bdc35
commit ba0205afbb
4 changed files with 132 additions and 9 deletions
@@ -12,6 +12,7 @@ export class AutoComplete extends Component {
this.nextSourceId = 0;
this.nextOptionId = 0;
this.sources = [];
this.inEdition = false;
this.state = useState({
navigationRev: 0,
@@ -45,7 +46,6 @@ export class AutoComplete extends Component {
}
}, this.constructor.timeout);
useExternalListener(window, "scroll", this.onWindowScroll, true);
this.hotkey = useService("hotkey");
@@ -54,8 +54,10 @@ export class AutoComplete extends Component {
owl.onWillUpdateProps((nextProps) => {
if (this.props.value !== nextProps.value || this.forceValFromProp) {
this.forceValFromProp = false;
this.state.value = nextProps.value;
this.inputRef.el.value = nextProps.value;
if (!this.inEdition) {
this.state.value = nextProps.value;
this.inputRef.el.value = nextProps.value;
}
this.close();
}
});
@@ -156,6 +158,7 @@ export class AutoComplete extends Component {
}
selectOption(indices, params = {}) {
const option = this.sources[indices[0]].options[indices[1]];
this.inEdition = false;
if (option.unselectable) {
this.inputRef.el.value = "";
this.close();
@@ -234,6 +237,7 @@ export class AutoComplete extends Component {
this.props.onBlur({
inputValue: value,
});
this.inEdition = false;
this.close();
}
}
@@ -250,6 +254,7 @@ export class AutoComplete extends Component {
});
}
async onInput() {
this.inEdition = true;
this.pendingPromise = this.pendingPromise || new Deferred();
this.loadingPromise = this.pendingPromise;
this.debouncedProcessInput();
@@ -397,4 +397,84 @@ QUnit.module("Components", (hooks) => {
assert.strictEqual(target.querySelector(".o-autocomplete input"), document.activeElement);
});
QUnit.test("autocomplete in edition keep edited value before select option", async (assert) => {
class Parent extends Component {
setup() {
this.state = useState({ value: "Hello" });
}
onClick() {
this.state.value = "My Click";
}
onSelect() {
this.state.value = "My Selection";
}
}
Parent.components = { AutoComplete };
Parent.template = xml`
<button class="myButton" t-on-click="onClick" />
<AutoComplete value="this.state.value"
sources="[{ options: [{ label: 'My Selection' }] }]"
onSelect.bind="onSelect"
/>
`;
await mount(Parent, target, { env });
const input = target.querySelector(".o-autocomplete input");
input.value = "Yolo";
await triggerEvent(input, null, "input");
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "Yolo");
await click(target, ".myButton");
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "Yolo");
// Leave inEdition mode when selecting an option
await click(target.querySelector(".o-autocomplete input"));
await click(target.querySelectorAll(".o-autocomplete--dropdown-item")[0]);
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "My Selection");
await click(target, ".myButton");
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "My Click");
});
QUnit.test("autocomplete in edition keep edited value before blur", async (assert) => {
let count = 0;
class Parent extends Component {
setup() {
this.state = useState({ value: "Hello" });
}
onClick() {
this.state.value = `My Click ${count++}`;
}
}
Parent.components = { AutoComplete };
Parent.template = xml`
<button class="myButton" t-on-click="onClick" />
<AutoComplete value="this.state.value"
sources="[]"
onSelect="() => {}"
/>
`;
await mount(Parent, target, { env });
let input = target.querySelector(".o-autocomplete input");
input.value = "";
await triggerEvent(input, null, "input");
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "");
await click(target, ".myButton");
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "");
// Leave inEdition mode when blur the input
input = target.querySelector(".o-autocomplete input");
await triggerEvent(input, null, "blur");
await nextTick();
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "");
await click(target, ".myButton");
assert.strictEqual(target.querySelector(".o-autocomplete input").value, "My Click 1");
});
});
@@ -10,6 +10,7 @@ import {
selectDropdownItem,
triggerEvent,
clickDiscard,
clickOpenedDropdownItem,
} from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { browser } from "@web/core/browser/browser";
@@ -360,8 +361,7 @@ QUnit.module("Fields", (hooks) => {
const input = target.querySelector(".o_field_widget[name=user_id] input");
input.value = "yy";
await triggerEvent(input, null, "input");
await click(target, ".o_field_widget[name=user_id] input");
await selectDropdownItem(target, "user_id", "Create and edit...");
await clickOpenedDropdownItem(target, "user_id", "Create and edit...");
await clickDiscard(target.querySelector(".modal"));
assert.strictEqual(target.querySelector(".o_field_widget[name=user_id] input").value, "");
@@ -692,8 +692,7 @@ QUnit.module("Fields", (hooks) => {
const input = target.querySelector(".o_field_widget[name=trululu] input");
input.value = "yy";
await triggerEvent(input, null, "input");
await click(target, ".o_field_widget[name=trululu] input");
await selectDropdownItem(target, "trululu", "Create and edit...");
await clickOpenedDropdownItem(target, "trululu", "Create and edit...");
await clickSave(target.querySelector(".modal"));
assert.verifySteps([`web_save: [[],{"name":"yy"}]`]);
@@ -742,8 +741,7 @@ QUnit.module("Fields", (hooks) => {
await click(target.querySelectorAll(".o_data_cell")[0]);
const input = target.querySelector(".o_field_widget[name=user_id] input");
await editInput(input, null, "yy");
await click(target, ".o_field_widget[name=user_id] input");
await selectDropdownItem(target, "user_id", 'Create "yy"');
await clickOpenedDropdownItem(target, "user_id", 'Create "yy"');
assert.verifySteps(["name_create"]);
}
@@ -834,6 +832,46 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test(
"edit many2one before onchange is finished should not reset the value",
async function (assert) {
serverData.models.partner.onchanges.display_name = function (obj) {
obj.user_id = 19;
};
const def = makeDeferred();
await makeView({
type: "form",
resModel: "partner",
resId: 1,
serverData,
arch: `
<form>
<field name="display_name"/>
<field name="user_id"/>
</form>`,
async mockRPC(route, { method }) {
if (method === "onchange") {
assert.step("onchange");
await def;
}
},
});
await editInput(target, "[name='display_name'] input", "new name");
const input = target.querySelector("[name='user_id'] input");
input.value = "Plop";
await triggerEvent(input, null, "input");
assert.strictEqual(target.querySelector("[name='user_id'] input").value, "Plop");
def.resolve();
await nextTick();
assert.strictEqual(target.querySelector("[name='user_id'] input").value, "Plop");
assert.verifySteps(["onchange"]);
}
);
QUnit.test("many2one doesn't trigger field_change when being emptied", async function (assert) {
await makeView({
type: "list",