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 ""`]);
+ });
});