[IMP] web: expression editor: model field selector
The component ExpressionEditorFieldSelector used by the expression editor to allow the selection of a field has several drawbacks: - the fields are not alphabetically ordered - there is no possibility to (fuzzy search) a field - the field types are not indicated in debug mode. - the style is different from the classical style to be found in the domain selector. Here we make the expression editor use a version of the ModelFieldSelector component where the debug input is not shown. Indeed, this would require some validation of the input value in order to prevent complex path like user_id.company_id that are forbidden in studio for instance. Anyway, there is not real need for such an input since it is easy to find/select a simple path with the fuzzy search. Task ID: 3599699 Part-of: odoo/odoo#142483
This commit is contained in:
@@ -11,6 +11,8 @@ import { TreeEditor } from "@web/core/tree_editor/tree_editor";
|
||||
import { getOperatorEditorInfo } from "@web/core/tree_editor/tree_editor_operator_editor";
|
||||
import { getDefaultValue } from "@web/core/tree_editor/tree_editor_value_editors";
|
||||
import { getDefaultPath } from "@web/core/tree_editor/utils";
|
||||
import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
function getDefaultCondition(fieldDefs) {
|
||||
const defaultPath = getDefaultPath(fieldDefs);
|
||||
@@ -20,15 +22,6 @@ function getDefaultCondition(fieldDefs) {
|
||||
return condition(fieldDef.name, operator, value);
|
||||
}
|
||||
|
||||
class ExpressionEditorFieldSelector extends Component {
|
||||
static template = "web.ExpressionEditorFieldSelector";
|
||||
static props = {
|
||||
value: [String, { value: 1 }, { value: 0 }],
|
||||
update: Function,
|
||||
fields: Object,
|
||||
};
|
||||
}
|
||||
|
||||
export class ExpressionEditor extends Component {
|
||||
static template = "web.ExpressionEditor";
|
||||
static components = { TreeEditor };
|
||||
@@ -45,7 +38,10 @@ export class ExpressionEditor extends Component {
|
||||
}
|
||||
|
||||
async onPropsUpdated(props) {
|
||||
this.defaultCondition = getDefaultCondition(props.fields);
|
||||
this.filteredFields = Object.fromEntries(
|
||||
Object.entries(props.fields).filter(([_, fieldDef]) => fieldDef.type !== "properties")
|
||||
);
|
||||
this.defaultCondition = getDefaultCondition(this.filteredFields);
|
||||
try {
|
||||
this.tree = treeFromExpression(props.expression, {
|
||||
getFieldDef: (name) => this.getFieldDef(name, props),
|
||||
@@ -75,14 +71,23 @@ export class ExpressionEditor extends Component {
|
||||
|
||||
getPathEditorInfo() {
|
||||
return {
|
||||
component: ExpressionEditorFieldSelector,
|
||||
component: ModelFieldSelector,
|
||||
extractProps: ({ value, update }) => ({
|
||||
value,
|
||||
path: value,
|
||||
update,
|
||||
fields: this.props.fields,
|
||||
resModel: this.props.resModel,
|
||||
readonly: false,
|
||||
filter: (fieldDef) => fieldDef.name in this.filteredFields,
|
||||
showDebugInput: false,
|
||||
followRelations: false,
|
||||
isDebugMode: this.isDebugMode,
|
||||
}),
|
||||
isSupported: (value) => [0, 1].includes(value) || this.getFieldDef(value),
|
||||
// by construction, all values received by the path editor will be supported.
|
||||
isSupported: (value) => [0, 1].includes(value) || value in this.filteredFields,
|
||||
// by construction, all values received by the path editor are O/1 or a field (name) in this.props.fields.
|
||||
// (see _leafFromAST in condition_tree.js)
|
||||
stringify: (value) => this.props.fields[value].string,
|
||||
defaultValue: () => this.defaultCondition.path,
|
||||
message: _t("Field properties not supported"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.ExpressionEditorFieldSelector">
|
||||
<select class="o_input pe-3 text-truncate" t-on-change="(ev) => props.update(ev.target.value)">
|
||||
<t t-if="[0, 1].includes(props.value)">
|
||||
<option t-att-value="props.value" t-esc="props.value" selected="true" hidden="true" />
|
||||
</t>
|
||||
<t t-foreach="Object.values(props.fields)" t-as="field" t-key="field.name">
|
||||
<option t-if="field.type !== 'properties' and !field.is_property"
|
||||
t-att-value="field.name"
|
||||
t-esc="field.string"
|
||||
t-att-selected="field.name === props.value"
|
||||
/>
|
||||
</t>
|
||||
</select>
|
||||
</t>
|
||||
|
||||
<t t-name="web.ExpressionEditor">
|
||||
<div class="o_expression_editor w-100" aria-atomic="true">
|
||||
<t t-if="tree">
|
||||
|
||||
@@ -21,13 +21,14 @@ export class ModelFieldSelector extends Component {
|
||||
update: { type: Function, optional: true },
|
||||
filter: { type: Function, optional: true },
|
||||
followRelations: { type: Boolean, optional: true },
|
||||
showDebugInput: { type: Boolean, optional: true },
|
||||
};
|
||||
static defaultProps = {
|
||||
readonly: true,
|
||||
allowEmpty: false,
|
||||
isDebugMode: false,
|
||||
showSearchInput: true,
|
||||
update: () => { },
|
||||
update: () => {},
|
||||
followRelations: true,
|
||||
};
|
||||
|
||||
@@ -67,6 +68,7 @@ export class ModelFieldSelector extends Component {
|
||||
isDebugMode: this.props.isDebugMode,
|
||||
filter: this.props.filter,
|
||||
followRelations: this.props.followRelations,
|
||||
showDebugInput: this.props.showDebugInput,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ export class ModelFieldSelectorPopover extends Component {
|
||||
close: Function,
|
||||
filter: { type: Function, optional: true },
|
||||
followRelations: { type: Boolean, optional: true },
|
||||
showDebugInput: { type: Boolean, optional: true },
|
||||
isDebugMode: { type: Boolean, optional: true },
|
||||
path: { optional: true },
|
||||
resModel: String,
|
||||
@@ -139,6 +140,10 @@ export class ModelFieldSelectorPopover extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
get showDebugInput() {
|
||||
return this.props.showDebugInput ?? this.props.isDebugMode;
|
||||
}
|
||||
|
||||
filter(fieldDefs, path) {
|
||||
const filteredKeys = Object.keys(fieldDefs).filter((k) =>
|
||||
this.props.filter(fieldDefs[k], path)
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<t t-if="props.isDebugMode">
|
||||
<t t-if="showDebugInput">
|
||||
<div class="o_model_field_selector_popover_footer">
|
||||
<input type="text" class="o_model_field_selector_debug o_input" t-att-value="state.page.path" t-on-change="(ev) => this.loadNewPath(ev.target.value)" t-on-keydown="onDebugInputKeydown" t-on-input="(ev) => this.props.update(ev.target.value, true)"/>
|
||||
</div>
|
||||
|
||||
@@ -153,10 +153,6 @@ export function getCurrentPath(target, index = 0) {
|
||||
if (pathEditor) {
|
||||
if (pathEditor.querySelector(".o_model_field_selector")) {
|
||||
return getModelFieldSelectorValues(pathEditor).join(" > ");
|
||||
} else if (pathEditor.querySelector("select")) {
|
||||
const select = pathEditor.querySelector("select");
|
||||
const option = [...select.children].find((child) => child.selected);
|
||||
return option.textContent;
|
||||
}
|
||||
return pathEditor.textContent;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
editSelect,
|
||||
getFixture,
|
||||
getNodesTextContent,
|
||||
mount,
|
||||
@@ -27,7 +26,10 @@ import {
|
||||
setupConditionTreeEditorServices,
|
||||
getOperatorOptions,
|
||||
getValueOptions,
|
||||
isNotSupportedPath,
|
||||
clearNotSupported,
|
||||
} from "./condition_tree_editor_helpers";
|
||||
import { openModelFieldSelectorPopover } from "./model_field_selector_tests";
|
||||
|
||||
export {
|
||||
clickOnButtonAddBranch,
|
||||
@@ -39,6 +41,8 @@ export {
|
||||
makeServerData,
|
||||
selectOperator,
|
||||
setupConditionTreeEditorServices,
|
||||
isNotSupportedPath,
|
||||
clearNotSupported,
|
||||
} from "./condition_tree_editor_helpers";
|
||||
|
||||
let serverData;
|
||||
@@ -49,11 +53,6 @@ export const SELECTORS = {
|
||||
debugArea: ".o_expression_editor_debug_container textarea",
|
||||
};
|
||||
|
||||
export async function selectPath(target, path, index = 0) {
|
||||
const el = get(target, SELECTORS.pathEditor, index);
|
||||
await editSelect(el, "select", path);
|
||||
}
|
||||
|
||||
async function editExpression(target, value, index = 0) {
|
||||
const input = get(target, SELECTORS.complexConditionInput, index);
|
||||
await editInput(input, null, value);
|
||||
@@ -208,7 +207,8 @@ QUnit.module("Components", (hooks) => {
|
||||
{ level: 1, value: ["Bar", "is not", "blabla"] },
|
||||
]);
|
||||
const tree = getTreeEditorContent(target, { node: true });
|
||||
await selectPath(tree[1].node, "foo");
|
||||
await openModelFieldSelectorPopover(target);
|
||||
await click(target.querySelectorAll(".o_model_field_selector_popover_item_name")[4]);
|
||||
await selectOperator(tree[1].node, "not in");
|
||||
await editValue(target, ["Doku", "Lukaku", "KDB"]);
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
@@ -384,13 +384,8 @@ QUnit.module("Components", (hooks) => {
|
||||
|
||||
QUnit.test("check condition by default when creating a new rule", async (assert) => {
|
||||
patchWithCleanup(odoo, { debug: false });
|
||||
await makeExpressionEditor({
|
||||
fields: {
|
||||
...serverData.fields,
|
||||
country_id: { string: "Country ID", type: "char" },
|
||||
},
|
||||
expression: "expr",
|
||||
});
|
||||
serverData.models.partner.fields.country_id = { string: "Country ID", type: "char" };
|
||||
await makeExpressionEditor({ expression: "expr" });
|
||||
await click(target, "a[role='button']");
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ level: 0, value: "all" },
|
||||
@@ -405,7 +400,8 @@ QUnit.module("Components", (hooks) => {
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["ID", "is set"] },
|
||||
]);
|
||||
await selectPath(target, "bar");
|
||||
await openModelFieldSelectorPopover(target);
|
||||
await click(target.querySelector(".o_model_field_selector_popover_item_name"));
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{ level: 0, value: "all" },
|
||||
{ level: 1, value: ["Bar", "is", "set"] },
|
||||
@@ -419,4 +415,53 @@ QUnit.module("Components", (hooks) => {
|
||||
assert.deepEqual(getOperatorOptions(target, -1), ["="]);
|
||||
assert.deepEqual(getValueOptions(target, -1), ["1"]);
|
||||
});
|
||||
|
||||
QUnit.test("no field of type properties in model field selector", async (assert) => {
|
||||
patchWithCleanup(odoo, { debug: false });
|
||||
serverData.models.partner.fields.properties = {
|
||||
string: "Properties",
|
||||
type: "properties",
|
||||
definition_record: "product_id",
|
||||
definition_record_field: "definitions",
|
||||
searchable: true,
|
||||
};
|
||||
serverData.models.product.fields.definitions = {
|
||||
string: "Definitions",
|
||||
type: "properties_definition",
|
||||
};
|
||||
|
||||
await makeExpressionEditor({
|
||||
expression: `properties`,
|
||||
fields: Object.fromEntries(
|
||||
Object.entries(serverData.models.partner.fields).filter(([name]) =>
|
||||
["bar", "foo", "properties"].includes(name)
|
||||
)
|
||||
),
|
||||
update(expression) {
|
||||
assert.step(expression);
|
||||
},
|
||||
});
|
||||
assert.deepEqual(getTreeEditorContent(target), [
|
||||
{
|
||||
level: 0,
|
||||
value: "all",
|
||||
},
|
||||
{
|
||||
level: 1,
|
||||
value: ["Properties", "is set"],
|
||||
},
|
||||
]);
|
||||
assert.ok(isNotSupportedPath(target));
|
||||
|
||||
await clearNotSupported(target);
|
||||
assert.verifySteps([`foo == ""`]);
|
||||
|
||||
await openModelFieldSelectorPopover(target);
|
||||
assert.deepEqual(
|
||||
getNodesTextContent([
|
||||
...target.querySelectorAll(".o_model_field_selector_popover_item_name"),
|
||||
]),
|
||||
["Bar", "Foo"]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -775,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"/>`;
|
||||
@@ -1001,4 +1001,19 @@ QUnit.module("Components", (hooks) => {
|
||||
assert.deepEqual(getModelFieldSelectorValues(target), ["foooooo"]);
|
||||
assert.verifySteps(["foooooo"]);
|
||||
});
|
||||
|
||||
QUnit.test("showDebugInput = false", async (assert) => {
|
||||
await mountComponent(ModelFieldSelector, {
|
||||
props: {
|
||||
readonly: false,
|
||||
path: "product_id",
|
||||
resModel: "partner",
|
||||
isDebugMode: true,
|
||||
showDebugInput: false,
|
||||
},
|
||||
});
|
||||
|
||||
await openModelFieldSelectorPopover(target);
|
||||
assert.containsNone(target, ".o_model_field_selector_debug");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user