diff --git a/addons/web/static/src/core/model_field_selector/model_field_selector.js b/addons/web/static/src/core/model_field_selector/model_field_selector.js index 4940ffa03c5..bf666287058 100644 --- a/addons/web/static/src/core/model_field_selector/model_field_selector.js +++ b/addons/web/static/src/core/model_field_selector/model_field_selector.js @@ -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 }); + } } diff --git a/addons/web/static/src/core/model_field_selector/model_field_selector.xml b/addons/web/static/src/core/model_field_selector/model_field_selector.xml index 95eccc73f05..febd237b50b 100644 --- a/addons/web/static/src/core/model_field_selector/model_field_selector.xml +++ b/addons/web/static/src/core/model_field_selector/model_field_selector.xml @@ -16,6 +16,9 @@
+
+ +
diff --git a/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js b/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js index 74cff3c6fde..3fbe0da5a6a 100644 --- a/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js +++ b/addons/web/static/src/core/model_field_selector/model_field_selector_popover.js @@ -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, diff --git a/addons/web/static/src/core/model_field_selector/utils.js b/addons/web/static/src/core/model_field_selector/utils.js index c63a364ea70..edc54cbc40c 100644 --- a/addons/web/static/src/core/model_field_selector/utils.js +++ b/addons/web/static/src/core/model_field_selector/utils.js @@ -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()] }; } diff --git a/addons/web/static/tests/core/model_field_selector_tests.js b/addons/web/static/tests/core/model_field_selector_tests.js index 29351444ef7..1c6a4e5c105 100644 --- a/addons/web/static/tests/core/model_field_selector_tests.js +++ b/addons/web/static/tests/core/model_field_selector_tests.js @@ -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``; + Parent.template = xml``; 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``; + + 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``; @@ -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` + + `; + 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 ""`]); + }); });