[FIX] web: model field selector: falsy paths

Since the refactoring of the model selector [https://github.com/odoo/odoo/commit/ebf646b44f747567ff8788c884f7f18dffd453e0], it is now more possible
to clear a selected field name (i.e. reset the path to "").

This has lead to an unsatisfactory situation when creating filters in a
spreadsheed. Indeed, the proper functioning of the interface was heavily
relying on that possibility.

In this fix, we introduce a new prop "allowEmpty" that if set to true
allows to clear the selected path and improve the display of falsy paths:
for a falsy path, the model field selector is empty and does not have a
warning message.

X-original-commit: 0244b0a7092b4c68e9f5cd4926b3017ea32b91de
Part-of: odoo/odoo#141362
This commit is contained in:
Mathieu Duckerts-Antoine
2023-11-07 21:02:33 +00:00
committed by Rémi Rahir (rar)
parent 8b7b4f7fb4
commit c4b8e05009
5 changed files with 131 additions and 12 deletions
@@ -14,6 +14,7 @@ export class ModelFieldSelector extends Component {
static props = {
resModel: String,
path: { optional: true },
allowEmpty: { type: Boolean, optional: true },
readonly: { type: Boolean, optional: true },
showSearchInput: { type: Boolean, optional: true },
isDebugMode: { type: Boolean, optional: true },
@@ -23,9 +24,10 @@ export class ModelFieldSelector extends Component {
};
static defaultProps = {
readonly: true,
allowEmpty: false,
isDebugMode: false,
showSearchInput: true,
update: () => {},
update: () => { },
followRelations: true,
};
@@ -35,7 +37,7 @@ export class ModelFieldSelector extends Component {
this.popover = usePopover(this.constructor.components.Popover, {
popoverClass: "o_popover_field_selector",
onClose: async () => {
if (this.newPath) {
if (this.newPath !== null) {
const fieldInfo = await loadFieldInfo(this.props.resModel, this.newPath);
this.props.update(this.newPath, fieldInfo);
}
@@ -43,8 +45,8 @@ export class ModelFieldSelector extends Component {
});
this.keepLast = new KeepLast();
this.state = useState({ isInvalid: false, displayNames: [] });
onWillStart(() => this.updateState(this.props.resModel, this.props.path));
onWillUpdateProps((nextProps) => this.updateState(nextProps.resModel, nextProps.path));
onWillStart(() => this.updateState(this.props));
onWillUpdateProps((nextProps) => this.updateState(nextProps));
}
openPopover(currentTarget) {
@@ -57,7 +59,7 @@ export class ModelFieldSelector extends Component {
path: this.props.path,
update: (path) => {
this.newPath = path;
this.updateState(this.props.resModel, path, true);
this.updateState({ ...this.props, path }, true);
},
showSearchInput: this.props.showSearchInput,
isDebugMode: this.props.isDebugMode,
@@ -66,12 +68,22 @@ export class ModelFieldSelector extends Component {
});
}
async updateState(resModel, path, isConcurrent) {
let prom = this.loadPathDescription(resModel, path);
async updateState(params, isConcurrent) {
const { resModel, path, allowEmpty } = params;
let prom = this.loadPathDescription(resModel, path, allowEmpty);
if (isConcurrent) {
prom = this.keepLast.add(prom);
}
const state = await prom;
Object.assign(this.state, state);
}
clear() {
if (this.popover.isOpen) {
this.newPath = "";
this.popover.close();
return;
}
this.props.update("", { resModel: this.props.resModel, fieodDef: null });
}
}
@@ -16,6 +16,9 @@
<div t-if="!props.readonly and state.isInvalid" class="o_model_field_selector_controls ms-2" tabindex="0">
<i class="fa fa-exclamation-triangle text-warning o_model_field_selector_warning" role="alert" aria-label="Invalid field chain" title="Invalid field chain"/>
</div>
<div t-if="!props.readonly and props.allowEmpty and state.displayNames.length" class="o_model_field_selector_controls ms-2" tabindex="0">
<i class="fa fa-times" t-on-click.stop="clear" aria-label="Clear" title="Clear"/>
</div>
</div>
</t>
@@ -95,7 +95,7 @@ export class ModelFieldSelectorPopover extends Component {
filter: { type: Function, optional: true },
followRelations: { type: Boolean, optional: true },
isDebugMode: { type: Boolean, optional: true },
path: [String, { value: 0 }, { value: 1 }],
path: { optional: true },
resModel: String,
showSearchInput: { type: Boolean, optional: true },
update: Function,
@@ -24,10 +24,13 @@ export function useLoadFieldInfo(fieldService) {
export function useLoadPathDescription(fieldService) {
fieldService ||= useService("field");
return async (resModel, path) => {
return async (resModel, path, allowEmpty) => {
if ([0, 1].includes(path)) {
return { isInvalid: false, displayNames: [makeString(path)] };
}
if (allowEmpty && !path) {
return { isInvalid: false, displayNames: [] };
}
if (typeof path !== "string" || !path) {
return { isInvalid: true, displayNames: [makeString()] };
}
@@ -628,14 +628,14 @@ QUnit.module("Components", (hooks) => {
assert.containsNone(target, ".o_model_field_selector_popover_prev_page");
});
QUnit.test("support of invalid paths", async (assert) => {
QUnit.test("support of invalid paths (allowEmpty=false)", async (assert) => {
class Parent extends Component {
setup() {
this.state = useState({ path: `` });
}
}
Parent.components = { ModelFieldSelector };
Parent.template = xml`<ModelFieldSelector resModel="'partner'" readonly="false" path="state.path"/>`;
Parent.template = xml`<ModelFieldSelector resModel="'partner'" readonly="false" path="state.path" />`;
const parent = await mountComponent(Parent);
assert.deepEqual(getModelFieldSelectorValues(target), ["-"]);
@@ -646,6 +646,55 @@ QUnit.module("Components", (hooks) => {
assert.deepEqual(getModelFieldSelectorValues(target), ["-"]);
assert.containsOnce(target, ".o_model_field_selector_warning");
parent.state.path = false;
await nextTick();
assert.deepEqual(getModelFieldSelectorValues(target), ["-"]);
assert.containsOnce(target, ".o_model_field_selector_warning");
parent.state.path = {};
await nextTick();
assert.deepEqual(getModelFieldSelectorValues(target), ["-"]);
assert.containsOnce(target, ".o_model_field_selector_warning");
parent.state.path = `a`;
await nextTick();
assert.deepEqual(getModelFieldSelectorValues(target), ["a"]);
assert.containsOnce(target, ".o_model_field_selector_warning");
parent.state.path = `foo.a`;
await nextTick();
assert.deepEqual(getModelFieldSelectorValues(target), ["Foo", "a"]);
assert.containsOnce(target, ".o_model_field_selector_warning");
parent.state.path = `a.foo`;
await nextTick();
assert.deepEqual(getModelFieldSelectorValues(target), ["a", "foo"]);
assert.containsOnce(target, ".o_model_field_selector_warning");
});
QUnit.test("support of invalid paths (allowEmpty=true)", async (assert) => {
class Parent extends Component {
setup() {
this.state = useState({ path: `` });
}
}
Parent.components = { ModelFieldSelector };
Parent.template = xml`<ModelFieldSelector resModel="'partner'" readonly="false" path="state.path" allowEmpty="true" />`;
const parent = await mountComponent(Parent);
assert.deepEqual(getModelFieldSelectorValues(target), []);
assert.containsNone(target, ".o_model_field_selector_warning");
parent.state.path = undefined;
await nextTick();
assert.deepEqual(getModelFieldSelectorValues(target), []);
assert.containsNone(target, ".o_model_field_selector_warning");
parent.state.path = false;
await nextTick();
assert.deepEqual(getModelFieldSelectorValues(target), []);
assert.containsNone(target, ".o_model_field_selector_warning");
parent.state.path = {};
await nextTick();
assert.deepEqual(getModelFieldSelectorValues(target), ["-"]);
@@ -726,7 +775,7 @@ QUnit.module("Components", (hooks) => {
setup() {
this.state = useState({ path: `foo` });
}
update() {}
update() { }
}
Parent.components = { ModelFieldSelector };
Parent.template = xml`<ModelFieldSelector resModel="'partner'" readonly="false" path="state.path" update.bind="update"/>`;
@@ -862,4 +911,56 @@ QUnit.module("Components", (hooks) => {
"Some stringucit (char)",
]);
});
QUnit.test("clear button (allowEmpty=true)", async (assert) => {
class Parent extends Component {
static components = { ModelFieldSelector };
static template = xml`
<ModelFieldSelector
readonly="false"
resModel="'partner'"
path="path"
allowEmpty="true"
isDebugMode="true"
update="(path, fieldInfo) => this.onUpdate(path)"
/>
`;
setup() {
this.path = "baaarrr";
}
onUpdate(path) {
this.path = path;
assert.step(`path is ${JSON.stringify(path)}`);
this.render();
}
}
await mountComponent(Parent);
assert.deepEqual(getModelFieldSelectorValues(target), ["baaarrr"]);
assert.containsOnce(target, ".o_model_field_selector_warning");
assert.containsOnce(target, ".o_model_field_selector .fa.fa-times");
// clear when popover is not open
await click(target, ".o_model_field_selector .fa.fa-times");
assert.deepEqual(getModelFieldSelectorValues(target), []);
assert.containsNone(target, ".o_model_field_selector_warning");
assert.containsNone(target, ".o_model_field_selector .fa.fa-times");
assert.verifySteps([`path is ""`]);
await openModelFieldSelectorPopover(target);
await click(target.querySelector(".o_model_field_selector_popover_item_name"));
assert.deepEqual(getModelFieldSelectorValues(target), ["Bar"]);
assert.containsNone(target, ".o_model_field_selector_warning");
assert.containsOnce(target, ".o_model_field_selector .fa.fa-times");
assert.verifySteps([`path is "bar"`]);
// clear when popover is open
await openModelFieldSelectorPopover(target);
await click(target, ".o_model_field_selector .fa.fa-times");
assert.deepEqual(getModelFieldSelectorValues(target), []);
assert.containsNone(target, ".o_model_field_selector_warning");
assert.containsNone(target, ".o_model_field_selector .fa.fa-times");
assert.verifySteps([`path is ""`]);
});
});