[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:
committed by
Rémi Rahir (rar)
parent
8b7b4f7fb4
commit
c4b8e05009
@@ -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 ""`]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user