[REF] web: use pseudo constructors to create (condition) trees

We define some helpers to easily create condition trees and use them
where it is possible.

closes odoo/odoo#139377

Signed-off-by: Michaël Mattiello (mcm) <mcm@odoo.com>
This commit is contained in:
Mathieu Duckerts-Antoine
2023-10-23 13:11:23 +00:00
parent bbf80b16c3
commit 0ff4fa07cf
7 changed files with 165 additions and 411 deletions
@@ -4,7 +4,12 @@ import { extractPathsFromDomain, useGetDefaultCondition } from "@web/core/domain
import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl";
import { Domain } from "@web/core/domain";
import { TreeEditor } from "@web/core/tree_editor/tree_editor";
import { domainFromTree, treeFromDomain, formatValue } from "@web/core/tree_editor/condition_tree";
import {
domainFromTree,
treeFromDomain,
formatValue,
condition,
} from "@web/core/tree_editor/condition_tree";
import { useLoadFieldInfo } from "@web/core/model_field_selector/utils";
import { CheckBox } from "@web/core/checkbox/checkbox";
import { deepEqual } from "@web/core/utils/objects";
@@ -13,13 +18,7 @@ import { getOperatorEditorInfo } from "@web/core/tree_editor/tree_editor_operato
import { _t } from "@web/core/l10n/translation";
import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector";
const ARCHIVED_CONDITION = {
type: "condition",
value: [true, false],
negate: false,
path: "active",
operator: "in",
};
const ARCHIVED_CONDITION = condition("active", "in", [true, false]);
const ARCHIVED_DOMAIN = `[("active", "in", [True, False])]`;
export class DomainSelector extends Component {
@@ -11,6 +11,7 @@ import {
normalizeValue,
formatValue as toString,
createVirtualOperators,
condition,
} from "@web/core/tree_editor/condition_tree";
import { useLoadFieldInfo, useLoadPathDescription } from "@web/core/model_field_selector/utils";
import {
@@ -41,13 +42,7 @@ function getDefaultCondition(fieldDefs) {
const fieldDef = fieldDefs[defaultPath];
const operator = getDomainDisplayedOperators(fieldDef)[0];
const value = getDefaultValue(fieldDef, operator);
return {
type: "condition",
negate: false,
path: fieldDef.name,
operator,
value,
};
return condition(fieldDef.name, operator, value);
}
function getDefaultDomain(fieldDefs) {
@@ -111,13 +106,7 @@ function simplifyTree(tree) {
value.push(...child.value);
}
}
children.push({
type: "condition",
negate: false,
operator: "in",
path: path,
value: normalizeValue(value),
});
children.push(condition(path, "in", normalizeValue(value)));
}
if (children.length === 1) {
return { ...children[0] };
@@ -2,7 +2,11 @@
import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl";
import { getExpressionDisplayedOperators } from "@web/core/expression_editor/expression_editor_operator_editor";
import { expressionFromTree, treeFromExpression } from "@web/core/tree_editor/condition_tree";
import {
condition,
expressionFromTree,
treeFromExpression,
} from "@web/core/tree_editor/condition_tree";
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";
@@ -13,13 +17,7 @@ function getDefaultCondition(fieldDefs) {
const fieldDef = fieldDefs[defaultPath];
const operator = getExpressionDisplayedOperators(fieldDef)[0];
const value = getDefaultValue(fieldDef, operator);
return {
type: "condition",
negate: false,
path: fieldDef.name,
operator,
value,
};
return condition(fieldDef.name, operator, value);
}
class ExpressionEditorFieldSelector extends Component {
@@ -31,27 +31,15 @@ import { toPyValue } from "@web/core/py_js/py_utils";
*/
/**
* @typedef {Object} AND
* @typedef {Object} Connector
* @property {"connector"} type
* @property {boolean} negate
* @property {"&"} value
* @property {(Connector|OR)[]} children
* @property {"|"|"&"} value
* @property {Tree[]} children
*/
/**
* @typedef {Object} OR
* @property {"connector"} type
* @property {boolean} negate
* @property {"|"} value
* @property {(Connector|AND)[]} children
*/
/**
* @typedef {AND|OR|Condition} SimpleTree
*/
/**
* @typedef {AND|OR|Condition|ComplexCondition} Tree
* @typedef {Connector|Condition|ComplexCondition} Tree
*/
/**
@@ -112,6 +100,70 @@ export class Expression {
}
}
/**
* @param {string} expr
* @returns {Expression}
*/
export function expression(expr) {
return new Expression(expr);
}
/**
* @param {"|"|"&"} value
* @param {Tree[]} [children=[]]
* @param {boolean} [negate=false]
* @returns {Connector}
*/
export function connector(value, children = [], negate = false) {
return { type: "connector", value, children, negate };
}
/**
* @param {Value} path
* @param {Value} operator
* @param {Value} value
* @param {boolean} [negate=false]
* @returns {Condition}
*/
export function condition(path, operator, value, negate = false) {
return { type: "condition", path, operator, value, negate };
}
/**
* @param {string} value
* @returns {ComplexCondition}
*/
export function complexCondition(value) {
parseExpr(value);
return { type: "complex_condition", value };
}
/**
* @param {Value} value
* @returns {Value}
*/
function cloneValue(value) {
if (value instanceof Expression) {
return new Expression(value.toAST());
}
if (Array.isArray(value)) {
return value.map(cloneValue);
}
return value;
}
/**
* @param {Tree} tree
* @returns {Tree}
*/
export function cloneTree(tree) {
const clone = {};
for (const key in tree) {
clone[key] = cloneValue(tree[key]);
}
return clone;
}
export function formatValue(value) {
return formatAST(toAST(value));
}
@@ -239,7 +291,7 @@ function _construcTree(ASTs, distributeNot, negate = false) {
*/
function construcTree(initialASTs, options) {
if (!initialASTs.length) {
return { type: "connector", value: "&", negate: false, children: [] };
return connector("&");
}
const { tree } = _construcTree(initialASTs, options.distributeNot);
return tree;
@@ -342,8 +394,6 @@ function _getConditionFromComparator(ast, options) {
return null;
}
const tree = { type: "condition", negate: false };
let operator = ast.op;
if (operator === "==") {
operator = "=";
@@ -364,18 +414,15 @@ function _getConditionFromComparator(ast, options) {
return null;
}
}
tree.path = left.value;
tree.operator = operator;
tree.value = toValue(right);
return tree;
return condition(left.value, operator, toValue(right));
}
/**
* @param {AST} ast
* @param {Options} options
* @param {boolean} [negate=false]
* @returns {import("@web/core/tree_editor/condition_tree").Condition|ComplexCondition}
* @returns {Condition|ComplexCondition}
*/
function _leafFromAST(ast, options, negate = false) {
if (isNot(ast)) {
@@ -383,24 +430,12 @@ function _leafFromAST(ast, options, negate = false) {
}
if (ast.type === 5 /** name */ && isValidPath(ast, options)) {
return {
type: "condition",
negate: false,
path: ast.value,
operator: negate ? "=" : "!=",
value: false,
};
return condition(ast.value, negate ? "=" : "!=", false);
}
const astValue = toValue(ast);
if (["boolean", "number", "string"].includes(typeof astValue)) {
return {
negate: false,
type: "condition",
path: astValue ? 1 : 0,
operator: "=",
value: 1,
};
return condition(astValue ? 1 : 0, "=", 1);
}
if (ast.type === 7 && COMPARATORS.includes(ast.op)) {
@@ -414,10 +449,7 @@ function _leafFromAST(ast, options, negate = false) {
}
// no conclusive way to transform ast in a condition
return {
type: "complex_condition",
value: formatAST(negate ? not(ast) : ast),
};
return complexCondition(formatAST(negate ? not(ast) : ast));
}
/**
@@ -432,14 +464,11 @@ function _treeFromAST(ast, options, negate = false) {
}
if (ast.type === 14) {
const tree = {
type: "connector",
value: ast.op === "and" ? "&" : "|", // and/or are the only ops that are given type 14 (for now)
children: [],
};
const tree = connector(
ast.op === "and" ? "&" : "|" // and/or are the only ops that are given type 14 (for now)
);
if (options.distributeNot && negate) {
tree.value = tree.value === "&" ? "|" : "&";
tree.negate = false;
} else {
tree.negate = negate;
}
@@ -574,13 +603,9 @@ function createBetweenOperators(tree) {
child1.operator === ">=" &&
child2.operator === "<="
) {
children.push({
type: "condition",
negate: false,
path: child1.path,
operator: "between",
value: normalizeValue([child1.value, child2.value]),
});
children.push(
condition(child1.path, "between", normalizeValue([child1.value, child2.value]))
);
i += 1;
} else {
children.push(child1);
@@ -605,27 +630,11 @@ export function removeBetweenOperators(tree) {
return tree;
}
const { negate, path, value } = tree;
return {
type: "connector",
negate,
value: "&",
children: [
{
type: "condition",
negate: false,
path,
operator: ">=",
value: value[0],
},
{
type: "condition",
negate: false,
path,
operator: "<=",
value: value[1],
},
],
};
return connector(
"&",
[condition(path, ">=", value[0]), condition(path, "<=", value[1])],
negate
);
}
const processedChildren = tree.children.map(removeBetweenOperators);
if (tree.value === "|") {
@@ -701,15 +710,12 @@ function createComplexConditions(tree) {
if (tree.path instanceof Expression && tree.operator === "=" && tree.value === 1) {
// not sure about this one -> we should maybe evaluate the condition and check
// if it does not become something e.g. the name of a integer field?
return {
type: "complex_condition",
value: String(tree.path),
};
return complexCondition(String(tree.path));
}
return { ...tree };
return cloneTree(tree);
}
if (tree.type === "complex_condition") {
return { ...tree };
return cloneTree(tree);
}
return {
...tree,
@@ -723,16 +729,11 @@ function createComplexConditions(tree) {
*/
function removeComplexConditions(tree) {
if (tree.type === "condition") {
return { ...tree };
return cloneTree(tree);
}
if (tree.type === "complex_condition") {
const ast = parseExpr(tree.value);
return {
type: "condition",
path: new Expression(bool(ast)),
operator: "=",
value: 1,
};
return condition(new Expression(bool(ast)), "=", 1);
}
return {
...tree,
@@ -1,7 +1,6 @@
/** @odoo-module **/
import {
cloneTree,
leafToString,
useLoadDisplayNames,
extractIdsFromTree,
@@ -11,7 +10,13 @@ import {
import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl";
import { Dropdown } from "@web/core/dropdown/dropdown";
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
import { formatValue, removeVirtualOperators } from "@web/core/tree_editor/condition_tree";
import {
condition,
cloneTree,
formatValue,
removeVirtualOperators,
connector,
} from "@web/core/tree_editor/condition_tree";
import {
getDefaultValue,
getValueEditorInfo,
@@ -20,21 +25,9 @@ import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_s
import { useLoadFieldInfo } from "@web/core/model_field_selector/utils";
import { deepEqual, shallowEqual } from "@web/core/utils/objects";
const TRUE_TREE = {
type: "condition",
value: 1,
negate: false,
path: 1,
operator: "=",
};
const TRUE_TREE = condition(1, "=", 1);
const DEFAULT_CONDITION = {
type: "condition",
path: "id",
negate: false,
operator: "=",
value: 1,
};
const DEFAULT_CONDITION = condition("id", "=", 1);
function collectDifferences(tree, otherTree) {
// some differences shadow the other differences "below":
@@ -132,19 +125,9 @@ export class TreeEditor extends Component {
async onPropsUpdated(props) {
this.tree = cloneTree(props.tree);
if (shallowEqual(this.tree, TRUE_TREE)) {
this.tree = {
type: "connector",
negate: false,
value: props.defaultConnector,
children: [],
};
this.tree = connector(props.defaultConnector);
} else if (this.tree.type !== "connector") {
this.tree = {
type: "connector",
negate: false,
value: props.defaultConnector,
children: [this.tree],
};
this.tree = connector(props.defaultConnector, [this.tree]);
}
if (this.previousTree) {
@@ -213,13 +196,8 @@ export class TreeEditor extends Component {
return cloneTree(this.props.defaultCondition);
}
createNewBranch(connector) {
return {
type: "connector",
value: connector,
negate: false,
children: [this.createNewLeaf(), this.createNewLeaf()],
};
createNewBranch(value) {
return connector(value, [this.createNewLeaf(), this.createNewLeaf()]);
}
insertRootLeaf(parent) {
@@ -6,32 +6,6 @@ import { Expression } from "@web/core/tree_editor/condition_tree";
import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
/**
* @param {import("@web/core/tree_editor/condition_tree").Value} value
* @returns {import("@web/core/tree_editor/condition_tree").Value}
*/
function cloneValue(value) {
if (value instanceof Expression) {
return new Expression(value.toAST());
}
if (Array.isArray(value)) {
return value.map(cloneValue);
}
return value;
}
/**
* @param {import("@web/core/tree_editor/condition_tree").Tree} tree
* @returns {import("@web/core/tree_editor/condition_tree").Tree}
*/
export function cloneTree(tree) {
const clone = {};
for (const key in tree) {
clone[key] = cloneValue(tree[key]);
}
return clone;
}
/**
* @param {import("@web/core/tree_editor/condition_tree").Value} val
* @param {boolean} disambiguate
@@ -1,8 +1,12 @@
/** @odoo-module **/
import {
complexCondition,
condition,
connector,
domainFromExpression,
domainFromTree,
expression,
Expression,
expressionFromDomain,
expressionFromTree,
@@ -15,39 +19,27 @@ QUnit.module("condition tree", {});
QUnit.test("domainFromTree", function (assert) {
const toTest = [
{
tree: { type: "condition", negate: false, path: "foo", operator: "=", value: false },
tree: condition("foo", "=", false),
result: `[("foo", "=", False)]`,
},
{
tree: { type: "condition", negate: true, path: "foo", operator: "=", value: false },
tree: condition("foo", "=", false, true),
result: `["!", ("foo", "=", False)]`,
},
{
tree: { type: "condition", negate: false, path: "foo", operator: "=?", value: false },
tree: condition("foo", "=?", false),
result: `[("foo", "=?", False)]`,
},
{
tree: { type: "condition", negate: true, path: "foo", operator: "=?", value: false },
tree: condition("foo", "=?", false, true),
result: `["!", ("foo", "=?", False)]`,
},
{
tree: {
type: "condition",
negate: false,
path: "foo",
operator: "between",
value: [1, 3],
},
tree: condition("foo", "between", [1, 3]),
result: `["&", ("foo", ">=", 1), ("foo", "<=", 3)]`,
},
{
tree: {
type: "condition",
negate: true,
path: "foo",
operator: "between",
value: [1, new Expression({ type: 5, value: "uid" })],
},
tree: condition("foo", "between", [1, expression("uid")], true),
result: `["!", "&", ("foo", ">=", 1), ("foo", "<=", uid)]`,
},
];
@@ -255,80 +247,35 @@ QUnit.test("expressionFromTree", function (assert) {
};
const toTest = [
{
expressionTree: {
type: "condition",
path: "x",
negate: false,
operator: "=",
value: false,
},
expressionTree: condition("x", "=", false),
result: `not x`,
},
{
expressionTree: {
type: "condition",
path: "x",
negate: true,
operator: "=",
value: false,
},
expressionTree: condition("x", "=", false, true),
result: `x`,
},
{
expressionTree: {
type: "condition",
path: "x",
negate: false,
operator: "!=",
value: false,
},
expressionTree: condition("x", "!=", false),
result: `x`,
},
{
expressionTree: {
type: "condition",
path: "x",
negate: true,
operator: "!=",
value: false,
},
expressionTree: condition("x", "!=", false, true),
result: `not x`,
},
{
expressionTree: {
type: "condition",
path: "y",
negate: false,
operator: "=",
value: false,
},
expressionTree: condition("y", "=", false),
result: `not "y"`,
},
{
expressionTree: {
type: "condition",
path: "x",
negate: false,
operator: "between",
value: [1, 3],
},
expressionTree: condition("x", "between", [1, 3]),
result: `x >= 1 and x <= 3`,
},
{
expressionTree: {
type: "condition",
path: "x",
negate: true,
operator: "between",
value: [1, new Expression("uid")],
},
expressionTree: condition("x", "between", [1, expression("uid")], true),
result: `not ( x >= 1 and x <= uid )`,
},
{
expressionTree: {
type: "complex_condition",
value: "uid",
},
expressionTree: complexCondition("uid"),
result: `uid`,
},
];
@@ -353,202 +300,70 @@ QUnit.test("treeFromExpression", function (assert) {
const toTest = [
{
expression: `not foo`,
result: {
type: "condition",
path: "foo",
negate: false,
operator: "not_set",
value: false,
},
result: condition("foo", "not_set", false),
},
{
expression: `foo == False`,
result: {
type: "condition",
path: "foo",
operator: "not_set",
negate: false,
value: false,
},
result: condition("foo", "not_set", false),
},
{
expression: `foo`,
result: {
type: "condition",
path: "foo",
negate: false,
operator: "set",
value: false,
},
result: condition("foo", "set", false),
},
{
expression: `foo == True`,
result: {
type: "condition",
path: "foo",
negate: false,
operator: "=",
value: true,
},
result: condition("foo", "=", true),
},
{
expression: `foo is True`,
result: {
type: "complex_condition",
value: `foo is True`,
},
result: complexCondition(`foo is True`),
},
{
expression: `not (foo == False)`,
result: {
type: "condition",
path: "foo",
negate: false,
operator: "set",
value: false,
},
result: condition("foo", "set", false),
},
{
expression: `not (not foo)`,
result: {
type: "condition",
path: "foo",
negate: false,
operator: "set",
value: false,
},
result: condition("foo", "set", false),
},
{
expression: `foo >= 1 and foo <= 3`,
result: {
type: "condition",
negate: false,
operator: "between",
path: "foo",
value: [1, 3],
},
result: condition("foo", "between", [1, 3]),
},
{
expression: `foo >= 1 and foo <= uid`,
result: {
type: "condition",
path: "foo",
negate: false,
operator: "between",
value: [1, new Expression("uid")],
},
result: condition("foo", "between", [1, expression("uid")]),
},
{
expression: `foo >= 1 if bar else foo <= uid`,
result: {
type: "connector",
negate: false,
value: "|",
children: [
{
type: "connector",
negate: false,
value: "&",
children: [
{
type: "condition",
path: "bar",
negate: false,
operator: "set",
value: false,
},
{
type: "condition",
path: "foo",
negate: false,
operator: ">=",
value: 1,
},
],
},
{
type: "connector",
negate: false,
value: "&",
children: [
{
type: "condition",
path: "bar",
negate: false,
operator: "not_set",
value: false,
},
{
type: "condition",
path: "foo",
negate: false,
operator: "<=",
value: new Expression("uid"),
},
],
},
],
},
result: connector("|", [
connector("&", [condition("bar", "set", false), condition("foo", ">=", 1)]),
connector("&", [
condition("bar", "not_set", false),
condition("foo", "<=", new Expression("uid")),
]),
]),
},
{
expression: `context.get('toto')`,
result: {
type: "complex_condition",
value: `context.get("toto")`,
},
result: complexCondition(`context.get("toto")`),
},
{
expression: `not context.get('toto')`,
result: {
type: "complex_condition",
value: `not context.get("toto")`,
},
result: complexCondition(`not context.get("toto")`),
},
{
expression: `foo >= 1 if context.get('toto') else bar == 42`,
result: {
type: "connector",
negate: false,
value: "|",
children: [
{
type: "connector",
negate: false,
value: "&",
children: [
{
type: "complex_condition",
value: `context.get("toto")`,
},
{
type: "condition",
path: "foo",
negate: false,
operator: ">=",
value: 1,
},
],
},
{
type: "connector",
negate: false,
value: "&",
children: [
{
type: "complex_condition",
value: `not context.get("toto")`,
},
{
type: "condition",
path: "bar",
negate: false,
operator: "=",
value: 42,
},
],
},
],
},
result: connector("|", [
connector("&", [
complexCondition(`context.get("toto")`),
condition("foo", ">=", 1),
]),
connector("&", [
complexCondition(`not context.get("toto")`),
condition("bar", "=", 42),
]),
]),
},
];
for (const { expression, result, extraOptions } of toTest) {