[FIX] web: SelectMenu value with no corresponding choice should not be displayed

This commit fixes an issue where when a value without a corresponding
choice was given to the SelectMenu, we would display it as a
string. It was missleading as it looked like an option was selected.

Now giving a value with no corresponding choice will result in the same
behaviour as giving no value.

Part-of: odoo/odoo#115799
This commit is contained in:
Bastien Fafchamps (bafa)
2023-04-03 13:55:17 +02:00
committed by Lucas Perais
parent 2d28239f9b
commit eb4a7d18b9
3 changed files with 106 additions and 33 deletions
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { Component, useState, useRef } from "@odoo/owl";
import { Component, useState, useRef, onWillUpdateProps } from "@odoo/owl";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { _lt } from "@web/core/l10n/translation";
@@ -88,20 +88,25 @@ export class SelectMenu extends Component {
() => this.onInput(this.inputRef.el ? this.inputRef.el.value.trim() : ""),
250
);
this.selectedChoice = this.getSelectedChoice(this.props);
onWillUpdateProps((nextProps) => {
if (this.props.value !== nextProps.value) {
this.selectedChoice = this.getSelectedChoice(nextProps);
}
});
}
get displayValue() {
if (this.props.value !== undefined) {
const choices = [...this.props.choices, ...this.props.groups.flatMap((g) => g.choices)];
const value = choices.find((c) => {
if (typeof c.value === "object" && typeof this.props.value === "object") {
return shallowEqual(c.value, this.props.value);
}
return c.value === this.props.value;
});
return value ? value.label : this.props.value;
}
return "";
return this.selectedChoice ? this.selectedChoice.label : "";
}
get canDeselect() {
return (
!this.props.required &&
this.selectedChoice !== undefined &&
this.selectedChoice !== null
);
}
get multiSelectChoices() {
@@ -150,10 +155,6 @@ export class SelectMenu extends Component {
}
}
canClear() {
return !this.props.required && this.props.value != null;
}
onInput(searchString) {
this.filterOptions(searchString);
@@ -164,6 +165,28 @@ export class SelectMenu extends Component {
}
}
getSelectedChoice(props) {
if (props.value) {
const choices = [...props.choices, ...props.groups.flatMap((g) => g.choices)];
return choices.find((c) => {
if (typeof c.value === "object" && typeof props.value === "object") {
return shallowEqual(c.value, props.value);
}
return c.value === props.value;
});
} else {
return undefined;
}
}
onItemSelected(value) {
if (this.props.multiSelect) {
this.props.onSelect([...this.props.value, value]);
} else {
this.props.onSelect(value);
}
}
// ==========================================================================================
// # Search #
// ==========================================================================================
@@ -238,14 +261,6 @@ export class SelectMenu extends Component {
return choices.flatMap((choice, index) => (index === 0 ? 0 : choice.isGroup ? index : []));
}
onItemSelected(value) {
if (this.props.multiSelect) {
this.props.onSelect([...this.props.value, value]);
} else {
this.props.onSelect(value);
}
}
// ==========================================================================================
// # Scroll #
// ==========================================================================================
@@ -22,7 +22,7 @@
<t t-if="!props.slots or !props.slots.default" t-esc="displayValue" />
<t t-else="" t-slot="default" />
</span>
<span t-if="canClear()" t-on-click.stop="() => this.props.onSelect(null)" class="o_select_menu_toggler_clear p-0 m-0">
<span t-if="canDeselect" t-on-click.stop="() => this.props.onSelect(null)" class="o_select_menu_toggler_clear p-0 m-0">
<i class="fa fa-times"></i>
</span>
</t>
@@ -100,16 +100,12 @@ QUnit.module("Web Components", (hooks) => {
class Parent extends Component {
setup() {
this.state = useState({ value: "world" });
this.choices = [
{ label: "Hello", value: "hello" },
];
this.choices = [{ label: "Hello", value: "hello" }];
this.groups = [
{
label: "Group A",
choices: [
{ label: "World", value: "world" },
]
}
choices: [{ label: "World", value: "world" }],
},
];
}
@@ -230,12 +226,74 @@ QUnit.module("Web Components", (hooks) => {
);
});
QUnit.test("Value with no corresponding choices displays as if no choice was selected", async (assert) => {
class Parent extends Component {
static components = { SelectMenu };
static template = xml`
<SelectMenu
choices="this.choices"
value="this.state.value"
/>
`;
setup() {
this.choices = [
{ label: "World", value: "world" },
{ label: "Hello", value: "hello" },
];
this.state = useState({ value: "coucou"});
}
setValue(newValue) {
this.state.value = newValue;
}
}
await mount(Parent, target, { env });
assert.equal(getValue(), "", `The toggler should be empty`);
});
QUnit.test("Changing value props properly updates the selected choice", async (assert) => {
class Parent extends Component {
static components = { SelectMenu };
static template = xml`
<SelectMenu
choices="this.choices"
value="this.state.value"
/>
`;
setup() {
this.choices = [
{ label: "Z", value: { hello: "world" } },
{ label: "A", value: { paper: "company" } },
];
this.state = useState({ value: { paper: "company" }});
}
setValue(newValue) {
this.state.value = newValue;
}
}
const comp = await mount(Parent, target, { env });
assert.equal(
getValue(),
"A",
`The select value shoud be "A"`
);
comp.setValue({ hello: "world" });
await nextTick();
assert.equal(
getValue(),
"Z",
`After changing the value props, the select value shoud be "Z"`
);
});
QUnit.test(
"Clear button calls 'onSelect' with null value and appears only when value is not null",
async (assert) => {
class Parent extends Component {
setup() {
this.state = useState({ value: "Hello" });
this.state = useState({ value: "hello" });
this.choices = [
{ label: "Hello", value: "hello" },
{ label: "World", value: "world" },