[REF] web: simplify domain selector
This commit is a refactoring of the domain selector to simplify it. The component DomainSelector now uses less sub components. Consequently, the code is centralized in fewer files and more readable. closes odoo/odoo#110978 Task: 3149707 Signed-off-by: Mathieu Duckerts-Antoine <dam@odoo.com>
This commit is contained in:
@@ -1,195 +1,29 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { DomainSelectorRootNode } from "./domain_selector_root_node";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { Component, onWillStart, onWillUpdateProps, useState } from "@odoo/owl";
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { DomainTreeBuilder } from "@web/core/domain_selector/domain_tree_builder";
|
||||
import {
|
||||
getDefaultFieldValue,
|
||||
getEditorInfo,
|
||||
getOperatorsInfo,
|
||||
} from "@web/core/domain_selector/domain_selector_fields";
|
||||
import { BranchDomainNode } from "@web/core/domain_selector/domain_selector_nodes";
|
||||
import { findOperator } from "@web/core/domain_selector/domain_selector_operators";
|
||||
import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class DomainSelector extends Component {
|
||||
setup() {
|
||||
this.nextNodeId = 0;
|
||||
}
|
||||
|
||||
buildTree() {
|
||||
try {
|
||||
const domain = new Domain(this.props.value);
|
||||
const ctx = {
|
||||
parent: null,
|
||||
index: 0,
|
||||
domain: domain.toList(),
|
||||
get currentElement() {
|
||||
return ctx.domain[ctx.index];
|
||||
},
|
||||
next() {
|
||||
ctx.index++;
|
||||
},
|
||||
getFullDomain() {
|
||||
return rootNode.computeDomain().toString();
|
||||
},
|
||||
};
|
||||
|
||||
const rootNode = this.makeRootNode(ctx);
|
||||
ctx.parent = rootNode;
|
||||
this.traverseNode(ctx);
|
||||
|
||||
return ctx.parent;
|
||||
} catch {
|
||||
// WOWL TODO: rethrow error when not the expected type
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
traverseNode(ctx) {
|
||||
if (ctx.index < ctx.domain.length) {
|
||||
if (typeof ctx.currentElement === "string" && ["&", "|"].includes(ctx.currentElement)) {
|
||||
this.traverseBranchNode(ctx);
|
||||
} else {
|
||||
this.traverseLeafNode(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
traverseBranchNode(ctx) {
|
||||
if (ctx.parent.type !== "branch" || ctx.parent.operator !== ctx.currentElement) {
|
||||
const node = this.makeBranchNode(ctx, ctx.currentElement, []);
|
||||
ctx.parent.operands.push(node);
|
||||
ctx = Object.assign(Object.create(ctx), { parent: node });
|
||||
}
|
||||
ctx.next();
|
||||
this.traverseNode(ctx);
|
||||
ctx.next();
|
||||
this.traverseNode(ctx);
|
||||
}
|
||||
traverseLeafNode(ctx) {
|
||||
const condition = ctx.currentElement;
|
||||
const [leftOperand, operator, rightOperand] = condition;
|
||||
const node = this.makeLeafNode(ctx, operator, [leftOperand, rightOperand]);
|
||||
ctx.parent.operands.push(node);
|
||||
}
|
||||
|
||||
makeBranchNode(ctx, operator, operands) {
|
||||
const updateDomain = () => this.props.update(ctx.getFullDomain());
|
||||
const makeFakeNode = this.makeFakeNode.bind(this);
|
||||
|
||||
return {
|
||||
type: "branch",
|
||||
id: this.nextNodeId++,
|
||||
operator,
|
||||
operands,
|
||||
computeDomain() {
|
||||
return Domain.combine(
|
||||
this.operands.map((operand) => operand.computeDomain()),
|
||||
this.operator === "&" ? "AND" : "OR"
|
||||
);
|
||||
},
|
||||
update(operator) {
|
||||
this.operator = operator;
|
||||
updateDomain();
|
||||
},
|
||||
insert(newNodeType) {
|
||||
const newNode = makeFakeNode(ctx, newNodeType);
|
||||
const operands = ctx.parent.operands;
|
||||
operands.splice(operands.indexOf(this) + 1, 0, newNode);
|
||||
updateDomain();
|
||||
},
|
||||
delete() {
|
||||
const operands = ctx.parent.operands;
|
||||
operands.splice(operands.indexOf(this), 1);
|
||||
updateDomain();
|
||||
},
|
||||
};
|
||||
}
|
||||
makeLeafNode(ctx, operator, operands) {
|
||||
const updateDomain = () => this.props.update(ctx.getFullDomain());
|
||||
const makeFakeNode = this.makeFakeNode.bind(this);
|
||||
|
||||
return {
|
||||
type: "leaf",
|
||||
id: this.nextNodeId++,
|
||||
operator,
|
||||
operands,
|
||||
computeDomain() {
|
||||
return new Domain([[this.operands[0], this.operator, this.operands[1]]]);
|
||||
},
|
||||
update(changes) {
|
||||
if ("fieldName" in changes) {
|
||||
this.operands[0] = changes.fieldName;
|
||||
}
|
||||
if ("operator" in changes) {
|
||||
this.operator = changes.operator;
|
||||
}
|
||||
if ("value" in changes) {
|
||||
this.operands[1] = changes.value;
|
||||
}
|
||||
updateDomain();
|
||||
},
|
||||
insert(newNodeType) {
|
||||
const newNode = makeFakeNode(ctx, newNodeType);
|
||||
const operands = ctx.parent.operands;
|
||||
operands.splice(operands.indexOf(this) + 1, 0, newNode);
|
||||
updateDomain();
|
||||
},
|
||||
delete() {
|
||||
const operands = ctx.parent.operands;
|
||||
operands.splice(operands.indexOf(this), 1);
|
||||
updateDomain();
|
||||
},
|
||||
};
|
||||
}
|
||||
makeRootNode(ctx) {
|
||||
const updateDomain = (...args) => this.props.update(...args);
|
||||
const makeFakeNode = this.makeFakeNode.bind(this);
|
||||
|
||||
return {
|
||||
type: "root",
|
||||
id: this.nextNodeId++,
|
||||
operator: "&",
|
||||
operands: [],
|
||||
computeDomain() {
|
||||
return Domain.combine(
|
||||
this.operands.map((operand) => operand.computeDomain()),
|
||||
"AND"
|
||||
);
|
||||
},
|
||||
update(newValue, fromDebug) {
|
||||
if (typeof newValue === "string") {
|
||||
updateDomain(newValue, fromDebug);
|
||||
} else if (this.operands.length) {
|
||||
this.operands[0].update(newValue);
|
||||
}
|
||||
},
|
||||
insert(newNodeType) {
|
||||
const newNode = makeFakeNode(ctx, newNodeType);
|
||||
if (ctx.parent) {
|
||||
const operands = ctx.parent.operands;
|
||||
operands.splice(operands.indexOf(this) + 1, 0, newNode);
|
||||
} else {
|
||||
this.operands.push(newNode);
|
||||
}
|
||||
updateDomain(ctx.getFullDomain());
|
||||
},
|
||||
delete() {},
|
||||
};
|
||||
}
|
||||
|
||||
makeFakeNode(ctx, type) {
|
||||
const [field, op, value] = this.props.defaultLeafValue;
|
||||
if (type === "branch") {
|
||||
return this.makeBranchNode(ctx, ctx.parent.operator === "&" ? "|" : "&", [
|
||||
this.makeLeafNode(ctx, op, [field, value]),
|
||||
this.makeLeafNode(ctx, op, [field, value]),
|
||||
]);
|
||||
} else {
|
||||
return this.makeLeafNode(ctx, op, [field, value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(DomainSelector, {
|
||||
template: "web._DomainSelector",
|
||||
components: {
|
||||
DomainSelectorRootNode,
|
||||
},
|
||||
props: {
|
||||
static template = "web._DomainSelector";
|
||||
static components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
ModelFieldSelector,
|
||||
};
|
||||
static props = {
|
||||
className: { type: String, optional: true },
|
||||
resModel: String,
|
||||
value: String,
|
||||
@@ -198,11 +32,186 @@ Object.assign(DomainSelector, {
|
||||
update: { type: Function, optional: true },
|
||||
isDebugMode: { type: Boolean, optional: true },
|
||||
defaultLeafValue: { type: Array, optional: true },
|
||||
},
|
||||
defaultProps: {
|
||||
};
|
||||
static defaultProps = {
|
||||
readonly: true,
|
||||
update: () => {},
|
||||
isDebugMode: false,
|
||||
defaultLeafValue: ["id", "=", 1],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.view = useService("view");
|
||||
this.treeBuilder = new DomainTreeBuilder();
|
||||
this.tree = useState({
|
||||
isSupported: false,
|
||||
root: null,
|
||||
});
|
||||
onWillStart(() => this.onPropsUpdated(this.props));
|
||||
onWillUpdateProps((np) => this.onPropsUpdated(np));
|
||||
}
|
||||
|
||||
async onPropsUpdated(p) {
|
||||
try {
|
||||
// try to parse and execute the domain, if it fails then the domain is not supported.
|
||||
const domain = new Domain(p.value);
|
||||
domain.toList();
|
||||
this.tree.root = this.treeBuilder.build(
|
||||
domain,
|
||||
await this.loadFieldDefs(p.resModel, this.extractFieldsFromDomain(domain))
|
||||
);
|
||||
this.defaultLeaf = this.treeBuilder.build(
|
||||
new Domain([p.defaultLeafValue]),
|
||||
await this.loadFieldDefs(p.resModel, [p.defaultLeafValue[0]])
|
||||
).children[0];
|
||||
this.tree.isSupported = true;
|
||||
} catch {
|
||||
this.tree.isSupported = false;
|
||||
this.tree.root = null;
|
||||
}
|
||||
}
|
||||
|
||||
extractFieldsFromDomain(domain) {
|
||||
const fields = [];
|
||||
for (const node of domain.ast.value) {
|
||||
if ([4, 10].includes(node.type)) {
|
||||
if (node.value[0].type === 1 || [0, 1].includes(node.value[0].value)) {
|
||||
fields.push(node.value[0].value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
notifyChanges() {
|
||||
this.props.update(this.tree.root.toDomain().toString());
|
||||
}
|
||||
|
||||
async loadFieldDefs(resModel, fields) {
|
||||
const promises = [];
|
||||
const fieldDefs = {};
|
||||
|
||||
for (const field of fields) {
|
||||
promises.push(
|
||||
this.loadFieldDef(resModel, field).then((info) => {
|
||||
fieldDefs[field] = info;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
return fieldDefs;
|
||||
}
|
||||
|
||||
async loadFieldDef(resModel, field) {
|
||||
if ("01".includes(field.toString())) {
|
||||
return { type: "integer" };
|
||||
}
|
||||
|
||||
const names = field.length ? field.split(".") : [];
|
||||
let currentModel = resModel;
|
||||
let fieldDef = null;
|
||||
for (const name of names) {
|
||||
const model = await this.view.loadFields(currentModel);
|
||||
fieldDef = model[name];
|
||||
currentModel = fieldDef.relation;
|
||||
}
|
||||
return fieldDef;
|
||||
}
|
||||
|
||||
createNewLeaf() {
|
||||
return this.defaultLeaf.clone();
|
||||
}
|
||||
|
||||
createNewBranch(operator) {
|
||||
return new BranchDomainNode(operator, [this.createNewLeaf(), this.createNewLeaf()]);
|
||||
}
|
||||
|
||||
insertRootLeaf(parent) {
|
||||
parent.add(this.createNewLeaf());
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
insertLeaf(parent, node) {
|
||||
parent.insertAfter(node.id, this.createNewLeaf());
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
insertBranch(parent, node) {
|
||||
const nextOperator = parent.operator === "AND" ? "OR" : "AND";
|
||||
parent.insertAfter(node.id, this.createNewBranch(nextOperator));
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
delete(parent, node) {
|
||||
parent.delete(node.id);
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
updateBranchOperator(node, operator) {
|
||||
node.operator = operator;
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
async updateField(node, field) {
|
||||
const fieldDef = await this.loadFieldDef(this.props.resModel, field);
|
||||
node.field = { ...fieldDef, name: field };
|
||||
node.operator = getOperatorsInfo(fieldDef.type)[0];
|
||||
node.value = getDefaultFieldValue(fieldDef);
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
updateLeafOperator(node, operator) {
|
||||
const previousOperator = node.operator;
|
||||
node.operator = findOperator(operator);
|
||||
if (previousOperator.valueMode !== node.operator.valueMode) {
|
||||
switch (node.operator.valueMode) {
|
||||
case "none": {
|
||||
node.value = false;
|
||||
break;
|
||||
}
|
||||
case "multiple": {
|
||||
node.value = previousOperator.valueMode === "none" ? [] : [node.value];
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (previousOperator.valueMode === "none") {
|
||||
node.value = getDefaultFieldValue(node.field);
|
||||
} else {
|
||||
node.value = node.value[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
updateLeafValue(node, value) {
|
||||
node.value = value;
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
onDebugValueChange(value) {
|
||||
return this.props.update(value, true);
|
||||
}
|
||||
|
||||
getEditorInfo(node) {
|
||||
return getEditorInfo(node.field.type, node.operator.key);
|
||||
}
|
||||
|
||||
getOperatorsInfo(node) {
|
||||
const operators = getOperatorsInfo(node.field.type);
|
||||
if (!operators.some((op) => op.key === node.operator.key)) {
|
||||
operators.push(node.operator);
|
||||
}
|
||||
return operators;
|
||||
}
|
||||
|
||||
highlightNode(target, toggle, classNames) {
|
||||
const nodeEl = target.closest(".o_domain_node");
|
||||
for (const className of classNames.split(/\s+/i)) {
|
||||
nodeEl.classList.toggle(className, toggle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,21 +2,178 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web._DomainSelector" owl="1">
|
||||
<t t-set="tree" t-value="buildTree()"/>
|
||||
<t t-if="tree">
|
||||
<DomainSelectorRootNode
|
||||
value="props.value"
|
||||
node="tree"
|
||||
readonly="props.readonly"
|
||||
resModel="props.resModel"
|
||||
isDebugMode="props.isDebugMode"
|
||||
debugValue="props.debugValue"
|
||||
className="props.className"
|
||||
/>
|
||||
<t t-if="tree.isSupported">
|
||||
<t t-call="web._DomainSelector.root">
|
||||
<t t-set="node" t-value="tree.root" />
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div t-att-class="props.className">This domain is not supported.</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.root" owl="1">
|
||||
<div class="o_domain_node o_domain_tree o_domain_selector" aria-atomic="true" t-attf-class="{{ props.className }} {{ props.readonly ? 'o_read_mode' : 'o_edit_mode'}}" t-ref="root">
|
||||
<t t-if="node.children.length === 0">
|
||||
<span>Match <strong>all records</strong></span>
|
||||
<t t-if="!props.readonly">
|
||||
<button class="btn btn-sm btn-primary o_domain_add_first_node_button ms-1" t-on-click="() => this.insertRootLeaf(node)">
|
||||
<i class="fa fa-plus"/> Add filter
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
<t t-elif="node.children.length === 1">
|
||||
Match records with the following rule:
|
||||
<t t-call="web._DomainSelector.branch.children" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span>Match records with </span>
|
||||
<t t-call="web._DomainSelector.branch.operator" />
|
||||
<span> of the following rules:</span>
|
||||
<t t-call="web._DomainSelector.branch.children" />
|
||||
</t>
|
||||
<t t-if="props.isDebugMode and !props.readonly">
|
||||
<label class="o_domain_debug_container">
|
||||
<span class="small"># Code editor</span>
|
||||
<textarea type="text" class="o_domain_debug_input" spellcheck="false" t-att-value="props.debugValue or props.value" t-on-change="(ev) => this.onDebugValueChange(ev.target.value)" />
|
||||
</label>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.controls" owl="1">
|
||||
<div class="o_domain_node_control_panel" role="toolbar" aria-label="Domain node">
|
||||
<button
|
||||
class="btn btn-link text-danger o_domain_delete_node_button"
|
||||
title="Delete node"
|
||||
aria-label="Delete node"
|
||||
t-on-click="() => this.delete(parent, node)"
|
||||
t-on-mouseenter="(ev) => this.highlightNode(ev.target, true, 'o_hover_btns')"
|
||||
t-on-mouseleave="(ev) => this.highlightNode(ev.target, false, 'o_hover_btns')"
|
||||
>
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
<button
|
||||
class="btn o_domain_add_node_button"
|
||||
title="Add node"
|
||||
aria-label="Add node"
|
||||
t-on-click="() => this.insertLeaf(parent, node)"
|
||||
t-on-mouseenter="(ev) => this.highlightNode(ev.target, true, 'o_hover_add_node')"
|
||||
t-on-mouseleave="(ev) => this.highlightNode(ev.target, false, 'o_hover_add_node')"
|
||||
>
|
||||
<i class="fa fa-plus-circle"/>
|
||||
</button>
|
||||
<button
|
||||
class="btn o_domain_add_node_button"
|
||||
title="Add branch"
|
||||
aria-label="Add branch"
|
||||
t-on-click="() => this.insertBranch(parent, node)"
|
||||
t-on-mouseenter="(ev) => this.highlightNode(ev.target, true, 'o_hover_add_node o_hover_add_inset_node')"
|
||||
t-on-mouseleave="(ev) => this.highlightNode(ev.target, false, 'o_hover_add_node o_hover_add_inset_node')"
|
||||
>
|
||||
<i class="fa fa-ellipsis-h"/>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.branch" owl="1">
|
||||
<div class="o_domain_node o_domain_tree">
|
||||
<div class="o_domain_tree_header o_domain_selector_row">
|
||||
<t t-if="!props.readonly">
|
||||
<t t-call="web._DomainSelector.controls" />
|
||||
</t>
|
||||
<t t-call="web._DomainSelector.branch.operator" />
|
||||
<span class="ml4">of:</span>
|
||||
</div>
|
||||
<t t-call="web._DomainSelector.branch.children" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.branch.operator" owl="1">
|
||||
<t t-if="props.readonly">
|
||||
<strong>
|
||||
<t t-if="node.operator === 'AND'">all</t>
|
||||
<t t-elif="node.operator === 'OR'">any</t>
|
||||
<t t-elif="node.operator === 'NOT'">none</t>
|
||||
</strong>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="d-inline-flex o_domain_tree_operator_selector" aria-atomic="true">
|
||||
<Dropdown togglerClass="'btn btn-link btn-sm btn-primary py-0 px-1 o_domain_tree_operator_caret'">
|
||||
<t t-set-slot="toggler">
|
||||
<t t-if="node.operator === 'AND'">all</t>
|
||||
<t t-elif="node.operator === 'OR'">any</t>
|
||||
<t t-elif="node.operator === 'NOT'">none</t>
|
||||
</t>
|
||||
<DropdownItem onSelected="() => this.updateBranchOperator(node, 'AND')">all</DropdownItem>
|
||||
<DropdownItem onSelected="() => this.updateBranchOperator(node, 'OR')">any</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.branch.children" owl="1">
|
||||
<div class="o_domain_node_children_container">
|
||||
<t t-foreach="node.children" t-as="child" t-key="child.id">
|
||||
<t t-call="web._DomainSelector.{{ child.type }}">
|
||||
<t t-set="parent" t-value="node" />
|
||||
<t t-set="node" t-value="child" />
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.leaf" owl="1">
|
||||
<div class="o_domain_node o_domain_leaf o_domain_selector_row">
|
||||
<t t-if="props.readonly">
|
||||
<div class="o_domain_leaf_info">
|
||||
<t t-call="web._DomainSelector.leaf:readonly" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="web._DomainSelector.controls" />
|
||||
<div class="o_domain_leaf_edition">
|
||||
<t t-call="web._DomainSelector.leaf:editable" />
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.leaf:readonly" owl="1">
|
||||
<ModelFieldSelector fieldName="node.field.name" resModel="props.resModel" readonly="true" />
|
||||
<t t-if="typeof node.value === 'boolean'">
|
||||
is <t t-if="node.value ? ['is_not', 'not_equal'].includes(node.operator.key) : ['is', 'equal'].includes(node.operator.key)"> not </t> set
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="o_domain_leaf_operator"> <t t-esc="node.operator.label" /></span>
|
||||
<t t-set="values" t-value="Array.isArray(node.value) ? node.value : [node.value]" />
|
||||
<t t-foreach="values" t-as="value" t-key="value_index">
|
||||
<span class="o_domain_leaf_value text-primary">
|
||||
<t t-if="typeof value === 'string'"> "<t t-esc="value"/>"</t>
|
||||
<t t-else=""> <t t-esc="value"/></t>
|
||||
</span>
|
||||
<t t-if="!value_last"> or </t>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.leaf:editable" owl="1">
|
||||
<ModelFieldSelector fieldName="node.field.name" resModel="props.resModel" readonly="false" update="(field) => this.updateField(node, field)" isDebugMode="props.isDebugMode" />
|
||||
|
||||
<div>
|
||||
<select class="o_domain_leaf_operator_select o_input text-truncate pe-1" t-on-change="(ev) => this.updateLeafOperator(node, ev.target.value)">
|
||||
<t t-foreach="getOperatorsInfo(node)" t-as="operator" t-key="operator.key">
|
||||
<option t-att-value="operator.key" t-att-selected="node.operator.key === operator.key" t-esc="operator_value.label" />
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<t t-if="node.operator.valueMode !== 'none'">
|
||||
<div class="o_ds_value_cell">
|
||||
<t t-set="editor" t-value="getEditorInfo(node)" />
|
||||
<t t-component="editor.component" t-props="editor.props({ value: node.value, update: (value) => this.updateLeafValue(node, value), field: node.field })" />
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { DomainSelectorBranchOperator } from "./domain_selector_branch_operator";
|
||||
import { DomainSelectorControlPanel } from "./domain_selector_control_panel";
|
||||
import { DomainSelectorLeafNode } from "./domain_selector_leaf_node";
|
||||
|
||||
import { Component, useRef } from "@odoo/owl";
|
||||
|
||||
export class DomainSelectorBranchNode extends Component {
|
||||
setup() {
|
||||
this.root = useRef("root");
|
||||
}
|
||||
onHoverDeleteNodeBtn(hovering) {
|
||||
this.root.el.classList.toggle("o_hover_btns", hovering);
|
||||
}
|
||||
onHoverInsertLeafNodeBtn(hovering) {
|
||||
this.root.el.classList.toggle("o_hover_add_node", hovering);
|
||||
}
|
||||
onHoverInsertBranchNodeBtn(hovering) {
|
||||
this.root.el.classList.toggle("o_hover_add_node", hovering);
|
||||
this.root.el.classList.toggle("o_hover_add_inset_node", hovering);
|
||||
}
|
||||
}
|
||||
DomainSelectorBranchNode.template = "web.DomainSelectorBranchNode";
|
||||
DomainSelectorBranchNode.components = {
|
||||
DomainSelectorBranchNode,
|
||||
DomainSelectorBranchOperator,
|
||||
DomainSelectorControlPanel,
|
||||
DomainSelectorLeafNode,
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorBranchNode" owl="1">
|
||||
<div class="o_domain_node o_domain_tree" t-ref="root">
|
||||
<div class="o_domain_tree_header o_domain_selector_row">
|
||||
<t t-if="!props.readonly">
|
||||
<DomainSelectorControlPanel
|
||||
node="props.node"
|
||||
onHoverDeleteNodeBtn.bind="onHoverDeleteNodeBtn"
|
||||
onHoverInsertLeafNodeBtn.bind="onHoverInsertLeafNodeBtn"
|
||||
onHoverInsertBranchNodeBtn.bind="onHoverInsertBranchNodeBtn"
|
||||
/>
|
||||
</t>
|
||||
<DomainSelectorBranchOperator node="props.node" readonly="props.readonly" />
|
||||
<span class="ml4">of:</span>
|
||||
</div>
|
||||
<div class="o_domain_node_children_container">
|
||||
<t t-foreach="props.node.operands" t-as="subNode" t-key="subNode.id">
|
||||
<t t-if="subNode.type === 'branch'">
|
||||
<DomainSelectorBranchNode t-props="{ ...props, node: subNode }" />
|
||||
</t>
|
||||
<t t-elif="subNode.type === 'leaf'">
|
||||
<DomainSelectorLeafNode t-props="{ ...props, node: subNode }" />
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,24 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class DomainSelectorBranchOperator extends Component {
|
||||
onOperatorSelected(operator) {
|
||||
this.props.node.update(operator);
|
||||
}
|
||||
}
|
||||
DomainSelectorBranchOperator.components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
};
|
||||
DomainSelectorBranchOperator.template = "web.DomainSelectorBranchOperator";
|
||||
DomainSelectorBranchOperator.props = {
|
||||
node: Object,
|
||||
readonly: Boolean,
|
||||
showCaret: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorBranchOperator" owl="1">
|
||||
<t t-if="!props.readonly">
|
||||
<div class="d-inline-flex o_domain_tree_operator_selector" aria-atomic="true">
|
||||
<Dropdown togglerClass="'btn btn-link btn-sm btn-primary py-0 px-1 o_domain_tree_operator_caret'" showCaret="props.showCaret">
|
||||
<t t-set-slot="toggler">
|
||||
<t t-if="props.node.operator === '&'">all</t>
|
||||
<t t-elif="props.node.operator === '|'">any</t>
|
||||
<t t-elif="props.node.operator === '!'">none</t>
|
||||
</t>
|
||||
<DropdownItem onSelected="() => this.onOperatorSelected('&')">all</DropdownItem>
|
||||
<DropdownItem onSelected="() => this.onOperatorSelected('|')">any</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<strong>
|
||||
<t t-if="props.node.operator === '&'">all</t>
|
||||
<t t-elif="props.node.operator === '|'">any</t>
|
||||
<t t-elif="props.node.operator === '!'">none</t>
|
||||
</strong>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,39 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, toRaw } from "@odoo/owl";
|
||||
|
||||
export class DomainSelectorControlPanel extends Component {
|
||||
deleteNode() {
|
||||
this.props.node.delete();
|
||||
}
|
||||
|
||||
insertNode(newNodeType) {
|
||||
toRaw(this.props.node).insert(newNodeType); // FIXME WOWL reactivity
|
||||
}
|
||||
|
||||
onEnterDeleteNodeBtn() {
|
||||
this.props.onHoverDeleteNodeBtn(true);
|
||||
}
|
||||
onLeaveDeleteNodeBtn() {
|
||||
this.props.onHoverDeleteNodeBtn(false);
|
||||
}
|
||||
onEnterInsertLeafNodeBtn() {
|
||||
this.props.onHoverInsertLeafNodeBtn(true);
|
||||
}
|
||||
onLeaveInsertLeafNodeBtn() {
|
||||
this.props.onHoverInsertLeafNodeBtn(false);
|
||||
}
|
||||
onEnterInsertBranchNodeBtn() {
|
||||
this.props.onHoverInsertBranchNodeBtn(true);
|
||||
}
|
||||
onLeaveInsertBranchNodeBtn() {
|
||||
this.props.onHoverInsertBranchNodeBtn(false);
|
||||
}
|
||||
}
|
||||
DomainSelectorControlPanel.template = "web.DomainSelectorControlPanel";
|
||||
DomainSelectorControlPanel.props = {
|
||||
node: Object,
|
||||
onHoverDeleteNodeBtn: Function,
|
||||
onHoverInsertLeafNodeBtn: Function,
|
||||
onHoverInsertBranchNodeBtn: Function,
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorControlPanel" owl="1">
|
||||
<div class="o_domain_node_control_panel" role="toolbar" aria-label="Domain node">
|
||||
<button
|
||||
class="btn btn-link text-danger o_domain_delete_node_button"
|
||||
title="Delete node"
|
||||
aria-label="Delete node"
|
||||
t-on-click="deleteNode"
|
||||
t-on-mouseenter="onEnterDeleteNodeBtn"
|
||||
t-on-mouseleave="onLeaveDeleteNodeBtn"
|
||||
>
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
<button
|
||||
class="btn o_domain_add_node_button"
|
||||
title="Add node"
|
||||
aria-label="Add node"
|
||||
t-on-click="() => this.insertNode('leaf')"
|
||||
t-on-mouseenter="onEnterInsertLeafNodeBtn"
|
||||
t-on-mouseleave="onLeaveInsertLeafNodeBtn"
|
||||
>
|
||||
<i class="fa fa-plus-circle"/>
|
||||
</button>
|
||||
<button
|
||||
class="btn o_domain_add_node_button"
|
||||
title="Add branch"
|
||||
aria-label="Add branch"
|
||||
data-branch="1"
|
||||
t-on-click="() => this.insertNode('branch')"
|
||||
t-on-mouseenter="onEnterInsertBranchNodeBtn"
|
||||
t-on-mouseleave="onLeaveInsertBranchNodeBtn"
|
||||
>
|
||||
<i class="fa fa-ellipsis-h"/>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,276 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useRef } from "@odoo/owl";
|
||||
import { DatePicker, DateTimePicker } from "@web/core/datepicker/datepicker";
|
||||
import { selectOperators } from "@web/core/domain_selector/domain_selector_operators";
|
||||
import {
|
||||
deserializeDate,
|
||||
deserializeDateTime,
|
||||
serializeDate,
|
||||
serializeDateTime,
|
||||
} from "@web/core/l10n/dates";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { evaluateExpr, formatAST } from "@web/core/py_js/py";
|
||||
import { toPyValue } from "@web/core/py_js/py_utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
// ============================================================================
|
||||
|
||||
class Input extends Component {
|
||||
static props = ["value", "update"];
|
||||
static template = "web.DomainSelector.Input";
|
||||
}
|
||||
|
||||
class Select extends Component {
|
||||
static props = ["value", "update", "options"];
|
||||
static template = "web.DomainSelector.Select";
|
||||
|
||||
deserialize(value) {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
serialize(value) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
class TagInput extends Component {
|
||||
static props = ["value", "update"];
|
||||
static template = "web.DomainSelector.TagInput";
|
||||
|
||||
setup() {
|
||||
this.inputRef = useRef("input");
|
||||
}
|
||||
|
||||
removeTag(tagIndex) {
|
||||
return this.props.update([
|
||||
...this.props.value.slice(0, tagIndex),
|
||||
...this.props.value.slice(tagIndex + 1),
|
||||
]);
|
||||
}
|
||||
|
||||
addTag(value) {
|
||||
return this.props.update([...this.props.value, value]);
|
||||
}
|
||||
|
||||
onBtnClick() {
|
||||
const value = this.inputRef.el.value;
|
||||
this.inputRef.el.value = "";
|
||||
return this.addTag(value);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
const parsers = registry.category("parsers");
|
||||
function parseValue(fieldType, value) {
|
||||
const parser = parsers.get(fieldType, (value) => value);
|
||||
try {
|
||||
return parser(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function makeEditor(component, props) {
|
||||
return {
|
||||
component,
|
||||
props: props || (({ value, update }) => ({ value, update })),
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT = {
|
||||
operators: [
|
||||
"equal",
|
||||
"not_equal",
|
||||
"greater_than",
|
||||
"greater_equal",
|
||||
"less_than",
|
||||
"less_equal",
|
||||
"ilike",
|
||||
"not_ilike",
|
||||
"like",
|
||||
"not_like",
|
||||
"equal_like",
|
||||
"equal_ilike",
|
||||
"child_of",
|
||||
"parent_of",
|
||||
"in",
|
||||
"not_in",
|
||||
"set",
|
||||
"not_set",
|
||||
],
|
||||
editors: { default: makeEditor(Input) },
|
||||
defaultValue: () => "",
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const BOOLEAN = {
|
||||
operators: ["is", "is_not"],
|
||||
editors: {
|
||||
default: makeEditor(Select, ({ value, update }) => ({
|
||||
value,
|
||||
update,
|
||||
options: [
|
||||
[true, _lt("set")],
|
||||
[false, _lt("not set")],
|
||||
],
|
||||
})),
|
||||
},
|
||||
defaultValue: () => true,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const DATE = {
|
||||
operators: [
|
||||
"equal",
|
||||
"not_equal",
|
||||
"greater_than",
|
||||
"greater_equal",
|
||||
"less_than",
|
||||
"less_equal",
|
||||
"set",
|
||||
"not_set",
|
||||
],
|
||||
editors: {
|
||||
default: makeEditor(DatePicker, ({ value, update }) => ({
|
||||
date: deserializeDate(value),
|
||||
onDateTimeChanged: (value) => {
|
||||
if (!value.isValid) {
|
||||
return;
|
||||
}
|
||||
return update(value ? serializeDate(value) : luxon.DateTime.local());
|
||||
},
|
||||
})),
|
||||
},
|
||||
defaultValue: () => serializeDate(luxon.DateTime.local()),
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const DATETIME = {
|
||||
operators: [
|
||||
"equal",
|
||||
"not_equal",
|
||||
"greater_than",
|
||||
"greater_equal",
|
||||
"less_than",
|
||||
"less_equal",
|
||||
"set",
|
||||
"not_set",
|
||||
],
|
||||
editors: {
|
||||
default: makeEditor(DateTimePicker, ({ value, update }) => ({
|
||||
date: deserializeDateTime(value),
|
||||
onDateTimeChanged: (value) => {
|
||||
if (!value.isValid) {
|
||||
return;
|
||||
}
|
||||
return update(value ? serializeDateTime(value) : luxon.DateTime.local());
|
||||
},
|
||||
})),
|
||||
},
|
||||
defaultValue: () => serializeDateTime(luxon.DateTime.local()),
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const TEXT = {
|
||||
operators: ["equal", "not_equal", "ilike", "not_ilike", "set", "not_set", "in", "not_in"],
|
||||
editors: {
|
||||
default: makeEditor(Input),
|
||||
in: makeEditor(TagInput),
|
||||
not_in: makeEditor(TagInput),
|
||||
},
|
||||
defaultValue: () => "",
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const NUMBER = {
|
||||
operators: [
|
||||
"equal",
|
||||
"not_equal",
|
||||
"greater_than",
|
||||
"greater_equal",
|
||||
"less_than",
|
||||
"less_equal",
|
||||
"ilike",
|
||||
"not_ilike",
|
||||
"set",
|
||||
"not_set",
|
||||
],
|
||||
editors: {
|
||||
default: makeEditor(Input, ({ value, update, field }) => ({
|
||||
value: `${value}`,
|
||||
update: (value) => update(parseValue(field.type, value)),
|
||||
})),
|
||||
},
|
||||
defaultValue: () => 1,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const RELATIONAL = {
|
||||
operators: ["equal", "not_equal", "ilike", "not_ilike", "set", "not_set"],
|
||||
editors: { default: makeEditor(Input) },
|
||||
defaultValue: (field) => (field.type === "many2one" ? 1 : []),
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const SELECTION_EDITOR_IN = makeEditor(Input, ({ value, update }) => ({
|
||||
value: formatAST(toPyValue(value)),
|
||||
update: (value) => update(evaluateExpr(value)),
|
||||
}));
|
||||
|
||||
const SELECTION = {
|
||||
operators: ["equal", "not_equal", "in", "not_in", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(Select, ({ value, update, field }) => ({
|
||||
value,
|
||||
update,
|
||||
options: field.selection,
|
||||
})),
|
||||
in: SELECTION_EDITOR_IN,
|
||||
not_in: SELECTION_EDITOR_IN,
|
||||
},
|
||||
defaultValue: (field) => field.selection[0][0],
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
export const FIELD_DESCRIPTIONS = {
|
||||
boolean: BOOLEAN,
|
||||
char: TEXT,
|
||||
date: DATE,
|
||||
datetime: DATETIME,
|
||||
float: NUMBER,
|
||||
html: TEXT,
|
||||
integer: NUMBER,
|
||||
many2many: RELATIONAL,
|
||||
many2one: RELATIONAL,
|
||||
monetary: NUMBER,
|
||||
one2many: RELATIONAL,
|
||||
selection: SELECTION,
|
||||
text: TEXT,
|
||||
};
|
||||
|
||||
export function getFieldInfo(fieldType) {
|
||||
return FIELD_DESCRIPTIONS[fieldType] || DEFAULT;
|
||||
}
|
||||
|
||||
export function getEditorInfo(fieldType, operator) {
|
||||
const fieldInfo = getFieldInfo(fieldType);
|
||||
return fieldInfo.editors[operator] || fieldInfo.editors.default;
|
||||
}
|
||||
|
||||
export function getOperatorsInfo(fieldType) {
|
||||
return selectOperators(getFieldInfo(fieldType).operators);
|
||||
}
|
||||
|
||||
export function getDefaultFieldValue(field) {
|
||||
const desc = getFieldInfo(field.type);
|
||||
return desc.defaultValue(field);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelector.Input" owl="1">
|
||||
<input type="text" class="o_input o_domain_leaf_value_input" t-att-value="props.value" t-on-change="(ev) => props.update(ev.target.value)" />
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.Select" owl="1">
|
||||
<select class="o_input o_domain_leaf_value_input" t-on-change="(ev) => props.update(deserialize(ev.target.value))">
|
||||
<t t-foreach="props.options" t-as="option" t-key="option[0]">
|
||||
<option t-att-value="serialize(option[0])" t-att-selected="option[0] === props.value" t-esc="option[1]" />
|
||||
</t>
|
||||
</select>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.TagInput" owl="1">
|
||||
<div class="o_domain_leaf_value_input">
|
||||
<t t-foreach="props.value" t-as="tag" t-key="tag_index">
|
||||
<span class="badge rounded-pill">
|
||||
<t t-esc="tag" />
|
||||
<i class="o_domain_leaf_value_remove_tag_button fa fa-times" role="img" aria-label="Remove tag" title="Remove tag" t-on-click="() => this.removeTag(tag_index)" />
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_domain_leaf_value_tags">
|
||||
<input type="text" class="o_input" placeholder="Add new value" t-ref="input" />
|
||||
<button class="btn btn-sm btn-primary fa fa-plus o_domain_leaf_value_add_tag_button" aria-label="Add tag" title="Add tag" t-on-click="onBtnClick" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,127 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useModelField } from "@web/core/model_field_selector/model_field_hook";
|
||||
import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { DomainSelectorControlPanel } from "./domain_selector_control_panel";
|
||||
import { DomainSelectorDefaultField } from "./fields/domain_selector_default_field";
|
||||
|
||||
import { Component, onWillStart, onWillUpdateProps, useRef } from "@odoo/owl";
|
||||
|
||||
export class DomainSelectorLeafNode extends Component {
|
||||
setup() {
|
||||
this.root = useRef("root");
|
||||
this.modelField = useModelField();
|
||||
this.fieldInfo = {
|
||||
type: "integer",
|
||||
string: "ID",
|
||||
};
|
||||
onWillStart(async () => {
|
||||
this.fieldInfo = await this.loadField(this.props.resModel, this.props.node.operands[0]);
|
||||
});
|
||||
onWillUpdateProps(async (nextProps) => {
|
||||
this.fieldInfo = await this.loadField(nextProps.resModel, nextProps.node.operands[0]);
|
||||
});
|
||||
}
|
||||
|
||||
get displayedOperator() {
|
||||
const op = this.getOperatorInfo(this.props.node.operator);
|
||||
return op ? op.label : "?";
|
||||
}
|
||||
get isValueHidden() {
|
||||
const op = this.getOperatorInfo(this.props.node.operator);
|
||||
return op ? op.hideValue : false;
|
||||
}
|
||||
|
||||
async loadField(resModel, fieldName) {
|
||||
const chain = await this.modelField.loadChain(resModel, fieldName);
|
||||
if (!chain[chain.length - 1].field && chain.length > 1) {
|
||||
return chain[chain.length - 2].field;
|
||||
}
|
||||
return (
|
||||
chain[chain.length - 1].field || {
|
||||
type: "integer",
|
||||
string: "ID",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
findOperator(operatorList, opToFind) {
|
||||
return operatorList.find((o) =>
|
||||
o.matches({
|
||||
field: this.fieldInfo,
|
||||
value: this.props.node.operands[1],
|
||||
operator: opToFind,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getOperators(field) {
|
||||
const operators = field.getOperators();
|
||||
if (this.findOperator(operators, this.props.node.operator)) {
|
||||
return operators;
|
||||
}
|
||||
return operators.concat(
|
||||
this.findOperator(
|
||||
registry.category("domain_selector/operator").getAll(),
|
||||
this.props.node.operator
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getFieldComponent(type) {
|
||||
return registry.category("domain_selector/fields").get(type, DomainSelectorDefaultField);
|
||||
}
|
||||
getOperatorInfo(operator) {
|
||||
const op = this.findOperator(
|
||||
this.getFieldComponent(this.fieldInfo.type).getOperators(),
|
||||
operator
|
||||
);
|
||||
if (op) {
|
||||
return op;
|
||||
}
|
||||
return this.findOperator(
|
||||
registry.category("domain_selector/operator").getAll(),
|
||||
this.props.node.operator
|
||||
);
|
||||
}
|
||||
|
||||
async onFieldChange(fieldName) {
|
||||
const changes = { fieldName };
|
||||
const fieldInfo = await this.loadField(this.props.resModel, fieldName);
|
||||
const component = this.getFieldComponent(fieldInfo.type);
|
||||
Object.assign(changes, component.onDidTypeChange(fieldInfo));
|
||||
this.props.node.update(changes);
|
||||
}
|
||||
onOperatorChange(ev) {
|
||||
const component = this.getFieldComponent(this.fieldInfo.type);
|
||||
const operatorInfo = component.getOperators()[parseInt(ev.target.value, 10)];
|
||||
const changes = { operator: operatorInfo.value };
|
||||
Object.assign(
|
||||
changes,
|
||||
operatorInfo.onDidChange(this.getOperatorInfo(this.props.node.operator), () =>
|
||||
component.onDidTypeChange(this.fieldInfo)
|
||||
)
|
||||
);
|
||||
this.props.node.update(changes);
|
||||
}
|
||||
|
||||
onHoverDeleteNodeBtn(hovering) {
|
||||
this.root.el.classList.toggle("o_hover_btns", hovering);
|
||||
}
|
||||
onHoverInsertLeafNodeBtn(hovering) {
|
||||
this.root.el.classList.toggle("o_hover_add_node", hovering);
|
||||
}
|
||||
onHoverInsertBranchNodeBtn(hovering) {
|
||||
this.root.el.classList.toggle("o_hover_add_node", hovering);
|
||||
this.root.el.classList.toggle("o_hover_add_inset_node", hovering);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(DomainSelectorLeafNode, {
|
||||
template: "web.DomainSelectorLeafNode",
|
||||
components: {
|
||||
DomainSelectorControlPanel,
|
||||
ModelFieldSelector,
|
||||
},
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorLeafNode" owl="1">
|
||||
<div class="o_domain_node o_domain_leaf o_domain_selector_row" t-ref="root">
|
||||
<t t-if="props.readonly">
|
||||
<div class="o_domain_leaf_info">
|
||||
<ModelFieldSelector fieldName="props.node.operands[0].toString()" resModel="props.resModel" readonly="props.readonly" isDebugMode="props.isDebugMode" />
|
||||
<t t-if="typeof props.node.operands[1] === 'string'">
|
||||
<span class="o_domain_leaf_operator"> <t t-esc="displayedOperator" /></span>
|
||||
<span class="o_domain_leaf_value text-primary"> "<t t-esc="props.node.operands[1]" />"</span>
|
||||
</t>
|
||||
<t t-elif="typeof props.node.operands[1] === 'number'">
|
||||
<span class="o_domain_leaf_operator"> <t t-esc="displayedOperator" /></span>
|
||||
<span class="o_domain_leaf_value text-primary"> <t t-esc="props.node.operands[1]" /></span>
|
||||
</t>
|
||||
<t t-elif="typeof props.node.operands[1] === 'boolean'">
|
||||
is
|
||||
<t t-if="(props.node.operator === '=' and props.node.operands[1] === false) or (props.node.operator === '!=' and props.node.operands[1] === true)">
|
||||
not
|
||||
</t>
|
||||
set
|
||||
</t>
|
||||
<t t-elif="Array.isArray(props.node.operands[1])">
|
||||
<span class="o_domain_leaf_operator"> <t t-esc="displayedOperator" /></span>
|
||||
<t t-foreach="props.node.operands[1]" t-as="value" t-key="value_index">
|
||||
<span class="o_domain_leaf_value text-primary"> "<t t-esc="value"/>"</span>
|
||||
<t t-if="!value_last"> or </t>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<DomainSelectorControlPanel
|
||||
node="props.node"
|
||||
onHoverDeleteNodeBtn.bind="onHoverDeleteNodeBtn"
|
||||
onHoverInsertLeafNodeBtn.bind="onHoverInsertLeafNodeBtn"
|
||||
onHoverInsertBranchNodeBtn.bind="onHoverInsertBranchNodeBtn"
|
||||
/>
|
||||
<div class="o_domain_leaf_edition">
|
||||
<ModelFieldSelector fieldName="props.node.operands[0].toString()" resModel="props.resModel" readonly="props.readonly" update="(name) => this.onFieldChange(name)" isDebugMode="props.isDebugMode" />
|
||||
<t t-set="field" t-value="getFieldComponent(fieldInfo.type)" />
|
||||
<t t-if="field">
|
||||
<div>
|
||||
<select class="o_domain_leaf_operator_select o_input text-truncate pe-1" t-on-change="onOperatorChange">
|
||||
<t t-foreach="getOperators(field)" t-as="operator" t-key="operator.value">
|
||||
<option
|
||||
t-att-value="operator_index"
|
||||
t-att-selected="operator.matches({ fieldInfo, operator: props.node.operator, value: props.node.operands[1] })"
|
||||
t-esc="operator.label"
|
||||
/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<t t-if="!isValueHidden">
|
||||
<t
|
||||
t-component="field"
|
||||
t-key="fieldInfo.type"
|
||||
field="fieldInfo"
|
||||
operator="getOperatorInfo(props.node.operator)"
|
||||
value="props.node.operands[1]"
|
||||
update="(changes) => props.node.update(changes)"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,66 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Domain } from "@web/core/domain";
|
||||
|
||||
class DomainNode {
|
||||
static nextId = 0;
|
||||
|
||||
constructor(type) {
|
||||
this.id = ++DomainNode.nextId;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
toDomain() {
|
||||
return new Domain();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export class BranchDomainNode extends DomainNode {
|
||||
constructor(operator, children = []) {
|
||||
super("branch");
|
||||
this.operator = operator;
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
toDomain() {
|
||||
return Domain.combine(
|
||||
this.children.map((c) => c.toDomain()),
|
||||
this.operator
|
||||
);
|
||||
}
|
||||
|
||||
add(node) {
|
||||
this.children.push(node);
|
||||
}
|
||||
|
||||
insertAfter(id, node) {
|
||||
const childIndex = this.children.findIndex((c) => c.id === id);
|
||||
this.children.splice(childIndex + 1, 0, node);
|
||||
}
|
||||
|
||||
delete(id) {
|
||||
const childIndex = this.children.findIndex((c) => c.id === id);
|
||||
this.children.splice(childIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export class LeafDomainNode extends DomainNode {
|
||||
constructor(field, operator, value) {
|
||||
super("leaf");
|
||||
this.field = field;
|
||||
this.operator = operator;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
toDomain() {
|
||||
return new Domain([[this.field.name, this.operator.symbol, this.value]]);
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new LeafDomainNode(this.field, this.operator, this.value);
|
||||
}
|
||||
}
|
||||
@@ -1,167 +1,41 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
function onDidChange(action) {
|
||||
return function (oldOperator, fieldChange) {
|
||||
if (this.category !== oldOperator.category) {
|
||||
return action(fieldChange);
|
||||
}
|
||||
return {};
|
||||
};
|
||||
export const OPERATOR_DESCRIPTIONS = [
|
||||
{ key: "is", label: _lt("is"), symbol: "=" },
|
||||
{ key: "is_not", label: _lt("is not"), symbol: "!=" },
|
||||
{ key: "equal", label: "=", symbol: "=" },
|
||||
{ key: "not_equal", label: "!=", symbol: "!=" },
|
||||
{ key: "greater_than", label: ">", symbol: ">" },
|
||||
{ key: "greater_equal", label: ">=", symbol: ">=" },
|
||||
{ key: "less_than", label: "<", symbol: "<" },
|
||||
{ key: "less_equal", label: "<=", symbol: "<=" },
|
||||
{ key: "ilike", label: _lt("contains"), symbol: "ilike" },
|
||||
{ key: "not_ilike", label: _lt("does not contain"), symbol: "not ilike" },
|
||||
{ key: "like", label: _lt("like"), symbol: "like" },
|
||||
{ key: "not_like", label: _lt("not like"), symbol: "not like" },
|
||||
{ key: "equal_like", label: _lt("=like"), symbol: "=like" },
|
||||
{ key: "equal_ilike", label: _lt("=ilike"), symbol: "=ilike" },
|
||||
{ key: "child_of", label: _lt("child of"), symbol: "child_of" },
|
||||
{ key: "parent_of", label: _lt("parent of"), symbol: "parent_of" },
|
||||
{ key: "in", label: _lt("in"), symbol: "in", valueMode: "multiple" },
|
||||
{ key: "not_in", label: _lt("not in"), symbol: "not in", valueMode: "multiple" },
|
||||
{ key: "set", label: _lt("is set"), symbol: "!=", valueMode: "none" },
|
||||
{ key: "not_set", label: _lt("is not set"), symbol: "=", valueMode: "none" },
|
||||
];
|
||||
|
||||
export function findOperator(key) {
|
||||
return OPERATOR_DESCRIPTIONS.find((op) => op.key === key);
|
||||
}
|
||||
|
||||
function matchValue() {
|
||||
return function ({ operator }) {
|
||||
return operator === this.value;
|
||||
};
|
||||
export function selectOperators(keys) {
|
||||
const operators = Object.fromEntries(OPERATOR_DESCRIPTIONS.map((op) => [op.key, op]));
|
||||
return keys.map((key) => operators[key]);
|
||||
}
|
||||
|
||||
const dso = registry.category("domain_selector/operator");
|
||||
dso.add("=", {
|
||||
category: "equality",
|
||||
label: "=",
|
||||
value: "=",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches({ operator, value }) {
|
||||
return operator === this.value && typeof value !== "boolean";
|
||||
},
|
||||
});
|
||||
dso.add("!=", {
|
||||
category: "equality",
|
||||
label: _lt("is not ="),
|
||||
value: "!=",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches({ operator, value }) {
|
||||
return operator === this.value && typeof value !== "boolean";
|
||||
},
|
||||
});
|
||||
dso.add(">", {
|
||||
category: "comparison",
|
||||
label: ">",
|
||||
value: ">",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add(">=", {
|
||||
category: "comparison",
|
||||
label: ">=",
|
||||
value: ">=",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("<", {
|
||||
category: "comparison",
|
||||
label: "<",
|
||||
value: "<",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("<=", {
|
||||
category: "comparison",
|
||||
label: "<=",
|
||||
value: "<=",
|
||||
onDidChange: onDidChange((fieldChange) => fieldChange()),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("ilike", {
|
||||
category: "like",
|
||||
label: _lt("contains"),
|
||||
value: "ilike",
|
||||
onDidChange: onDidChange(() => ({ value: "" })),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("not ilike", {
|
||||
category: "like",
|
||||
label: _lt("does not contain"),
|
||||
value: "not ilike",
|
||||
onDidChange: onDidChange(() => ({ value: "" })),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("like", {
|
||||
category: "like",
|
||||
label: _lt("like"),
|
||||
value: "like",
|
||||
onDidChange: onDidChange(() => ({ value: "" })),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("not like", {
|
||||
category: "like",
|
||||
label: _lt("not like"),
|
||||
value: "not like",
|
||||
onDidChange: onDidChange(() => ({ value: "" })),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("=like", {
|
||||
category: "like",
|
||||
label: _lt("=like"),
|
||||
value: "=like",
|
||||
onDidChange: onDidChange(() => ({ value: "" })),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("=ilike", {
|
||||
category: "like",
|
||||
label: _lt("=ilike"),
|
||||
value: "=ilike",
|
||||
onDidChange: onDidChange(() => ({ value: "" })),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("child_of", {
|
||||
category: "relation",
|
||||
label: _lt("child of"),
|
||||
value: "child_of",
|
||||
onDidChange: onDidChange(() => ({ value: 1 })),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("parent_of", {
|
||||
category: "relation",
|
||||
label: _lt("parent of"),
|
||||
value: "parent_of",
|
||||
onDidChange: onDidChange(() => ({ value: 1 })),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("in", {
|
||||
category: "in",
|
||||
label: _lt("in"),
|
||||
value: "in",
|
||||
onDidChange: onDidChange(() => ({ value: [] })),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("not in", {
|
||||
category: "in",
|
||||
label: _lt("not in"),
|
||||
value: "not in",
|
||||
onDidChange: onDidChange(() => ({ value: [] })),
|
||||
matches: matchValue(),
|
||||
});
|
||||
dso.add("set", {
|
||||
category: "set",
|
||||
label: _lt("is set"),
|
||||
value: "set",
|
||||
hideValue: true,
|
||||
onDidChange() {
|
||||
return {
|
||||
operator: "!=",
|
||||
value: false,
|
||||
};
|
||||
},
|
||||
matches({ operator, value }) {
|
||||
return operator === "!=" && typeof value === "boolean";
|
||||
},
|
||||
});
|
||||
dso.add("not set", {
|
||||
category: "set",
|
||||
label: _lt("is not set"),
|
||||
value: "not set",
|
||||
hideValue: true,
|
||||
onDidChange() {
|
||||
return {
|
||||
operator: "=",
|
||||
value: false,
|
||||
};
|
||||
},
|
||||
matches({ operator, value }) {
|
||||
return operator === "=" && typeof value === "boolean";
|
||||
},
|
||||
});
|
||||
export function parseOperator(symbol) {
|
||||
return OPERATOR_DESCRIPTIONS.filter(
|
||||
(op) => !["is", "is_not", "set", "not_set"].includes(op.key)
|
||||
).find((op) => op.symbol === symbol);
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { DomainSelectorBranchNode } from "./domain_selector_branch_node";
|
||||
import { DomainSelectorBranchOperator } from "./domain_selector_branch_operator";
|
||||
import { DomainSelectorLeafNode } from "./domain_selector_leaf_node";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class DomainSelectorRootNode extends Component {
|
||||
get hasNode() {
|
||||
return this.props.node.operands.length > 0;
|
||||
}
|
||||
get node() {
|
||||
return this.props.node.operands[0];
|
||||
}
|
||||
|
||||
insertNode(newNodeType) {
|
||||
this.props.node.insert(newNodeType);
|
||||
}
|
||||
|
||||
onOperatorSelected(ev) {
|
||||
this.props.node.update(ev.detail.payload.operator);
|
||||
}
|
||||
onChange(ev) {
|
||||
this.props.node.update(ev.target.value, true);
|
||||
}
|
||||
}
|
||||
DomainSelectorRootNode.template = "web.DomainSelectorRootNode";
|
||||
DomainSelectorRootNode.components = {
|
||||
DomainSelectorBranchNode,
|
||||
DomainSelectorBranchOperator,
|
||||
DomainSelectorLeafNode,
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorRootNode" owl="1">
|
||||
<div
|
||||
class="o_domain_node o_domain_tree o_domain_selector"
|
||||
aria-atomic="true"
|
||||
t-attf-class="{{ props.className }} {{ props.readonly ? 'o_read_mode' : 'o_edit_mode'}}"
|
||||
>
|
||||
<t t-if="!hasNode">
|
||||
<span>Match <strong>all records</strong></span>
|
||||
<t t-if="!props.readonly">
|
||||
<button
|
||||
class="btn btn-sm btn-primary o_domain_add_first_node_button ms-1"
|
||||
t-on-click="() => this.insertNode('leaf')"
|
||||
>
|
||||
<i class="fa fa-plus"/> Add filter
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-if="node.type === 'leaf'">
|
||||
Match records with the following rule:
|
||||
<div class="o_domain_node_children_container">
|
||||
<DomainSelectorLeafNode t-props="{ ...props, node }" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span>Match records with </span>
|
||||
<DomainSelectorBranchOperator node="node" readonly="props.readonly" showCaret="true" />
|
||||
<span> of the following rules:</span>
|
||||
<div class="o_domain_node_children_container">
|
||||
<t t-foreach="node.operands" t-as="subNode" t-key="subNode.id">
|
||||
<t t-if="subNode.type === 'branch'">
|
||||
<DomainSelectorBranchNode t-props="{ ...props, node: subNode }" />
|
||||
</t>
|
||||
<t t-elif="subNode.type === 'leaf'">
|
||||
<DomainSelectorLeafNode t-props="{ ...props, node: subNode }" />
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="props.isDebugMode and !props.readonly">
|
||||
<label class="o_domain_debug_container">
|
||||
<span class="small"># Code editor</span>
|
||||
<textarea type="text" class="o_domain_debug_input" t-att-value="props.debugValue or props.value" t-on-change="onChange" />
|
||||
</label>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,102 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { BranchDomainNode, LeafDomainNode } from "@web/core/domain_selector/domain_selector_nodes";
|
||||
import { findOperator, parseOperator } from "@web/core/domain_selector/domain_selector_operators";
|
||||
|
||||
export class DomainTreeBuilder {
|
||||
build(domain, fieldDefs) {
|
||||
const nodeIterator = domain.ast.value.values();
|
||||
const nextNode = () => {
|
||||
const it = nodeIterator.next();
|
||||
return it.done ? null : it.value;
|
||||
};
|
||||
|
||||
const rawNode = nextNode();
|
||||
const node =
|
||||
rawNode && this.isBranch(rawNode)
|
||||
? new BranchDomainNode(this.getBranchOperator(rawNode))
|
||||
: new BranchDomainNode("AND");
|
||||
if (rawNode) {
|
||||
this.buildNode(node, rawNode, nextNode, fieldDefs);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
buildNode(parent, rawNode, nextNode, fieldDefs) {
|
||||
if (this.isBranch(rawNode)) {
|
||||
this.buildBranch(parent, rawNode, nextNode, fieldDefs);
|
||||
} else {
|
||||
this.buildLeaf(parent, rawNode, fieldDefs);
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
buildLeaf(parent, rawNode, fieldDefs) {
|
||||
const field = this.getLeafFieldName(rawNode);
|
||||
const operatorInfo = this.getLeafOperatorInfo(rawNode, fieldDefs[field].type);
|
||||
const value = this.isLeafValueArray(rawNode)
|
||||
? this.getLeafValue(rawNode).map((v) => v.value)
|
||||
: this.getLeafValue(rawNode);
|
||||
parent.add(
|
||||
new LeafDomainNode({ ...fieldDefs[field], name: field.toString() }, operatorInfo, value)
|
||||
);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
buildBranch(parent, rawNode, nextNode, fieldDefs) {
|
||||
let newParent = parent;
|
||||
const currentOperator = this.getBranchOperator(rawNode);
|
||||
if (currentOperator !== parent.operator) {
|
||||
newParent = new BranchDomainNode(currentOperator);
|
||||
parent.add(newParent);
|
||||
}
|
||||
this.buildNode(newParent, nextNode(), nextNode, fieldDefs);
|
||||
this.buildNode(newParent, nextNode(), nextNode, fieldDefs);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
isBranch(rawNode) {
|
||||
return rawNode.type === 1 && ["&", "|", "!"].includes(rawNode.value);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
getBranchOperator(rawNode) {
|
||||
switch (rawNode.value) {
|
||||
case "&":
|
||||
return "AND";
|
||||
case "|":
|
||||
return "OR";
|
||||
case "!":
|
||||
return "NOT";
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
getLeafFieldName(rawNode) {
|
||||
return rawNode.value[0].value;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
getLeafOperatorInfo(rawNode, fieldType) {
|
||||
const rawOperator = rawNode.value[1].value;
|
||||
if (fieldType === "boolean") {
|
||||
return findOperator(rawOperator === "=" ? "is" : "is_not");
|
||||
} else if (rawNode.value[2].type === 2) {
|
||||
return findOperator(rawOperator === "!=" ? "set" : "not_set");
|
||||
} else {
|
||||
return parseOperator(rawOperator);
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
getLeafValue(rawNode) {
|
||||
return rawNode.value[2].value;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
isLeafValueArray(rawNode) {
|
||||
return [4, 10].includes(rawNode.value[2].type);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
const dsf = registry.category("domain_selector/fields");
|
||||
|
||||
export class DomainSelectorBooleanField extends Component {
|
||||
onChange(ev) {
|
||||
this.props.update({
|
||||
value: ev.target.value === "1",
|
||||
});
|
||||
}
|
||||
}
|
||||
Object.assign(DomainSelectorBooleanField, {
|
||||
template: "web.DomainSelectorBooleanField",
|
||||
|
||||
onDidTypeChange() {
|
||||
return { value: true };
|
||||
},
|
||||
getOperators() {
|
||||
return [
|
||||
{
|
||||
category: "equality",
|
||||
label: _lt("is"),
|
||||
value: "=",
|
||||
onDidChange() {},
|
||||
matches({ operator }) {
|
||||
return operator === this.value;
|
||||
},
|
||||
},
|
||||
{
|
||||
category: "equality",
|
||||
label: _lt("is not"),
|
||||
value: "!=",
|
||||
onDidChange() {},
|
||||
matches({ operator }) {
|
||||
return operator === this.value;
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
dsf.add("boolean", DomainSelectorBooleanField);
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorBooleanField" owl="1">
|
||||
<div class="o_ds_value_cell">
|
||||
<select class="o_input" t-on-change="onChange">
|
||||
<option value="1" t-att-selected="props.value">set (true)</option>
|
||||
<option value="0" t-att-selected="!props.value">not set (false)</option>
|
||||
</select>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,52 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { DatePicker, DateTimePicker } from "@web/core/datepicker/datepicker";
|
||||
import {
|
||||
deserializeDate,
|
||||
deserializeDateTime,
|
||||
serializeDate,
|
||||
serializeDateTime,
|
||||
} from "@web/core/l10n/dates";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
const dsf = registry.category("domain_selector/fields");
|
||||
const dso = registry.category("domain_selector/operator");
|
||||
|
||||
export class DomainSelectorDateTimeField extends Component {
|
||||
get component() {
|
||||
const { DatePicker, DateTimePicker } = this.constructor.components;
|
||||
return this.props.field.type === "date" ? DatePicker : DateTimePicker;
|
||||
}
|
||||
get deserializedValue() {
|
||||
const deserialize =
|
||||
this.props.field.type === "date" ? deserializeDate : deserializeDateTime;
|
||||
return this.props.value ? deserialize(this.props.value) : luxon.DateTime.local();
|
||||
}
|
||||
onChange(value) {
|
||||
if (!this.deserializedValue.isValid && !value) {
|
||||
return;
|
||||
}
|
||||
const serialize = this.props.field.type === "date" ? serializeDate : serializeDateTime;
|
||||
this.props.update({ value: serialize(value || luxon.DateTime.local()) });
|
||||
}
|
||||
}
|
||||
Object.assign(DomainSelectorDateTimeField, {
|
||||
template: "web.DomainSelectorDateTimeField",
|
||||
components: {
|
||||
DatePicker,
|
||||
DateTimePicker,
|
||||
},
|
||||
|
||||
onDidTypeChange(field) {
|
||||
const serialize = field.type === "date" ? serializeDate : serializeDateTime;
|
||||
return { value: serialize(luxon.DateTime.local()) };
|
||||
},
|
||||
getOperators() {
|
||||
return ["=", "!=", ">", "<", ">=", "<=", "set", "not set"].map((key) => dso.get(key));
|
||||
},
|
||||
});
|
||||
|
||||
dsf.add("date", DomainSelectorDateTimeField);
|
||||
dsf.add("datetime", DomainSelectorDateTimeField);
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorDateTimeField" owl="1">
|
||||
<div class="o_ds_value_cell">
|
||||
<t t-component="component" date="deserializedValue" onDateTimeChanged.bind="onChange" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,23 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { DomainSelectorFieldInput } from "./domain_selector_field_input";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
const dso = registry.category("domain_selector/operator");
|
||||
|
||||
export class DomainSelectorDefaultField extends Component {}
|
||||
Object.assign(DomainSelectorDefaultField, {
|
||||
template: "web.DomainSelectorDefaultField",
|
||||
components: {
|
||||
DomainSelectorFieldInput,
|
||||
},
|
||||
|
||||
onDidTypeChange() {
|
||||
return { value: "" };
|
||||
},
|
||||
getOperators() {
|
||||
return dso.getAll();
|
||||
},
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorDefaultField" owl="1">
|
||||
<div class="o_ds_value_cell">
|
||||
<DomainSelectorFieldInput t-props="props" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,22 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
const parsers = registry.category("parsers");
|
||||
|
||||
export class DomainSelectorFieldInput extends Component {
|
||||
parseValue(value) {
|
||||
const parser = parsers.get(this.props.field.type, (value) => value);
|
||||
try {
|
||||
return parser(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
onChange(ev) {
|
||||
this.props.update({ value: this.parseValue(ev.target.value) });
|
||||
}
|
||||
}
|
||||
DomainSelectorFieldInput.template = "web.DomainSelectorFieldInput";
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorFieldInput" owl="1">
|
||||
<input type="text" class="o_input o_domain_leaf_value_input" t-att-value="props.value" t-on-change="onChange" />
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, useRef } from "@odoo/owl";
|
||||
|
||||
export class DomainSelectorFieldInputWithTags extends Component {
|
||||
setup() {
|
||||
this.inputRef = useRef("input");
|
||||
}
|
||||
|
||||
removeTag(tagIndex) {
|
||||
const value = [...this.props.value];
|
||||
value.splice(tagIndex, 1);
|
||||
this.props.update({ value });
|
||||
}
|
||||
addTag(value) {
|
||||
this.props.update({ value: this.props.value.concat(value) });
|
||||
}
|
||||
|
||||
onBtnClick() {
|
||||
const value = this.inputRef.el.value;
|
||||
this.inputRef.el.value = "";
|
||||
this.addTag(value);
|
||||
}
|
||||
}
|
||||
DomainSelectorFieldInputWithTags.template = "web.DomainSelectorFieldInputWithTags";
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorFieldInputWithTags" owl="1">
|
||||
<div class="o_ds_value_cell">
|
||||
<div class="o_domain_leaf_value_input">
|
||||
<t t-foreach="props.value" t-as="tag" t-key="tag_index">
|
||||
<span class="badge rounded-pill">
|
||||
<t t-esc="tag" /> <i
|
||||
class="o_domain_leaf_value_remove_tag_button fa fa-times"
|
||||
role="img"
|
||||
aria-label="Remove tag"
|
||||
title="Remove tag"
|
||||
t-on-click="() => this.removeTag(tag_index)"
|
||||
/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_domain_leaf_value_tags">
|
||||
<input type="text" class="o_input" placeholder="Add new value" t-ref="input" />
|
||||
<button
|
||||
class="btn btn-sm btn-primary fa fa-plus o_domain_leaf_value_add_tag_button"
|
||||
aria-label="Add tag"
|
||||
title="Add tag"
|
||||
t-on-click="onBtnClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,39 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { DomainSelectorFieldInput } from "./domain_selector_field_input";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
const dsf = registry.category("domain_selector/fields");
|
||||
const dso = registry.category("domain_selector/operator");
|
||||
|
||||
export class DomainSelectorNumberField extends Component {}
|
||||
Object.assign(DomainSelectorNumberField, {
|
||||
template: "web.DomainSelectorNumberField",
|
||||
components: {
|
||||
DomainSelectorFieldInput,
|
||||
},
|
||||
|
||||
onDidTypeChange() {
|
||||
return { value: 0 };
|
||||
},
|
||||
getOperators() {
|
||||
return [
|
||||
"=",
|
||||
"!=",
|
||||
">",
|
||||
"<",
|
||||
">=",
|
||||
"<=",
|
||||
"ilike",
|
||||
"not ilike",
|
||||
"set",
|
||||
"not set",
|
||||
].map((key) => dso.get(key));
|
||||
},
|
||||
});
|
||||
|
||||
dsf.add("integer", DomainSelectorNumberField);
|
||||
dsf.add("float", DomainSelectorNumberField);
|
||||
dsf.add("monetary", DomainSelectorNumberField);
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorNumberField" owl="1">
|
||||
<div class="o_ds_value_cell">
|
||||
<DomainSelectorFieldInput t-props="props" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,29 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { DomainSelectorFieldInput } from "./domain_selector_field_input";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
const dso = registry.category("domain_selector/operator");
|
||||
|
||||
export class DomainSelectorRelationField extends Component {}
|
||||
Object.assign(DomainSelectorRelationField, {
|
||||
template: "web.DomainSelectorRelationField",
|
||||
components: {
|
||||
DomainSelectorFieldInput,
|
||||
},
|
||||
|
||||
onDidTypeChange() {
|
||||
return { value: "0" };
|
||||
},
|
||||
getOperators() {
|
||||
return ["=", "!=", "ilike", "not ilike", "set", "not set"].map((key) => dso.get(key));
|
||||
},
|
||||
});
|
||||
|
||||
registry
|
||||
.category("domain_selector/fields")
|
||||
.add("one2many", DomainSelectorRelationField)
|
||||
.add("many2one", DomainSelectorRelationField)
|
||||
.add("many2many", DomainSelectorRelationField);
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorRelationField" owl="1">
|
||||
<div class="o_ds_value_cell">
|
||||
<DomainSelectorFieldInput t-props="props" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,44 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { evaluateExpr } from "@web/core/py_js/py";
|
||||
import { formatAST, toPyValue } from "@web/core/py_js/py_utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
const dsf = registry.category("domain_selector/fields");
|
||||
const dso = registry.category("domain_selector/operator");
|
||||
|
||||
export class DomainSelectorSelectionField extends Component {
|
||||
get options() {
|
||||
return [
|
||||
[false, ""],
|
||||
...this.props.field.selection.filter((option) => !this.props.value.includes(option[0])),
|
||||
];
|
||||
}
|
||||
|
||||
get formattedValue() {
|
||||
const ast = toPyValue(this.props.value);
|
||||
return formatAST(ast);
|
||||
}
|
||||
|
||||
onChange(ev) {
|
||||
this.props.update({ value: ev.target.value });
|
||||
}
|
||||
|
||||
onChangeMulti(ev) {
|
||||
this.props.update({ value: evaluateExpr(ev.target.value) });
|
||||
}
|
||||
}
|
||||
Object.assign(DomainSelectorSelectionField, {
|
||||
template: "web.DomainSelectorSelectionField",
|
||||
|
||||
onDidTypeChange(field) {
|
||||
return { value: field.selection[0][0] };
|
||||
},
|
||||
getOperators() {
|
||||
return ["=", "!=", "set", "not set"].map((key) => dso.get(key));
|
||||
},
|
||||
});
|
||||
|
||||
dsf.add("selection", DomainSelectorSelectionField);
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorSelectionField" owl="1">
|
||||
<div class="o_ds_value_cell">
|
||||
<t t-if="props.operator.category === 'in'">
|
||||
<input type="text" class="o_input o_domain_leaf_value_input" t-att-value="formattedValue" t-on-change="onChangeMulti" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<select class="o_input o_domain_leaf_value_input" t-on-change="onChange">
|
||||
<t t-foreach="props.field.selection" t-as="option" t-key="option[0]">
|
||||
<option
|
||||
t-att-value="option[0]"
|
||||
t-att-selected="option[0] === props.value"
|
||||
t-esc="option[1]"
|
||||
/>
|
||||
</t>
|
||||
</select>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,32 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { DomainSelectorFieldInput } from "./domain_selector_field_input";
|
||||
import { DomainSelectorFieldInputWithTags } from "./domain_selector_field_input_with_tags";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
const dsf = registry.category("domain_selector/fields");
|
||||
const dso = registry.category("domain_selector/operator");
|
||||
|
||||
export class DomainSelectorTextField extends Component {}
|
||||
Object.assign(DomainSelectorTextField, {
|
||||
template: "web.DomainSelectorTextField",
|
||||
components: {
|
||||
DomainSelectorFieldInput,
|
||||
DomainSelectorFieldInputWithTags,
|
||||
},
|
||||
|
||||
onDidTypeChange() {
|
||||
return { value: "" };
|
||||
},
|
||||
getOperators() {
|
||||
return ["=", "!=", "ilike", "not ilike", "set", "not set", "in", "not in"].map((key) =>
|
||||
dso.get(key)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
dsf.add("char", DomainSelectorTextField);
|
||||
dsf.add("html", DomainSelectorTextField);
|
||||
dsf.add("text", DomainSelectorTextField);
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelectorTextField" owl="1">
|
||||
<t t-if="props.operator.category === 'in'">
|
||||
<DomainSelectorFieldInputWithTags t-props="props" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="o_ds_value_cell">
|
||||
<DomainSelectorFieldInput t-props="props" />
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -6,17 +6,7 @@ export function useModelField() {
|
||||
const view = useService("view");
|
||||
|
||||
const loadModelFields = (resModel) => {
|
||||
return view.loadFields(resModel, {
|
||||
attributes: [
|
||||
"store",
|
||||
"searchable",
|
||||
"type",
|
||||
"string",
|
||||
"relation",
|
||||
"selection",
|
||||
"related",
|
||||
],
|
||||
});
|
||||
return view.loadFields(resModel);
|
||||
};
|
||||
|
||||
const loadChain = async (resModel, fieldName) => {
|
||||
|
||||
@@ -4,24 +4,30 @@ import { useModelField } from "./model_field_hook";
|
||||
import { useUniquePopover } from "./unique_popover_hook";
|
||||
import { ModelFieldSelectorPopover } from "./model_field_selector_popover";
|
||||
|
||||
import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl";
|
||||
import { Component, onWillStart, onWillUpdateProps, useState } from "@odoo/owl";
|
||||
|
||||
export class ModelFieldSelector extends Component {
|
||||
setup() {
|
||||
this.popover = useUniquePopover();
|
||||
this.modelField = useModelField();
|
||||
this.chain = [];
|
||||
this.state = useState({
|
||||
chain: [],
|
||||
fieldName: this.props.fieldName,
|
||||
isDirty: false,
|
||||
});
|
||||
|
||||
onWillStart(async () => {
|
||||
this.chain = await this.loadChain(this.props.resModel, this.props.fieldName);
|
||||
this.state.chain = await this.loadChain(this.props.resModel, this.props.fieldName);
|
||||
});
|
||||
onWillUpdateProps(async (nextProps) => {
|
||||
this.chain = await this.loadChain(nextProps.resModel, nextProps.fieldName);
|
||||
this.state.chain = await this.loadChain(nextProps.resModel, nextProps.fieldName);
|
||||
this.state.fieldName = nextProps.fieldName;
|
||||
this.state.isDirty = false;
|
||||
});
|
||||
}
|
||||
|
||||
get fieldNameChain() {
|
||||
return this.getFieldNameChain(this.props.fieldName);
|
||||
return this.getFieldNameChain(this.state.fieldName ?? this.props.fieldName);
|
||||
}
|
||||
|
||||
getFieldNameChain(fieldName) {
|
||||
@@ -32,29 +38,16 @@ export class ModelFieldSelector extends Component {
|
||||
if ("01".includes(fieldName)) {
|
||||
return [{ resModel, field: { string: fieldName } }];
|
||||
}
|
||||
const fieldNameChain = this.getFieldNameChain(fieldName);
|
||||
let currentNode = {
|
||||
resModel,
|
||||
field: null,
|
||||
};
|
||||
const chain = [currentNode];
|
||||
for (const fieldName of fieldNameChain) {
|
||||
const fieldsInfo = await this.modelField.loadModelFields(currentNode.resModel);
|
||||
Object.assign(currentNode, {
|
||||
field: { ...fieldsInfo[fieldName], name: fieldName },
|
||||
});
|
||||
if (fieldsInfo[fieldName].relation) {
|
||||
currentNode = {
|
||||
resModel: fieldsInfo[fieldName].relation,
|
||||
field: null,
|
||||
};
|
||||
chain.push(currentNode);
|
||||
}
|
||||
}
|
||||
return chain;
|
||||
return this.modelField.loadChain(resModel, fieldName);
|
||||
}
|
||||
update(chain) {
|
||||
this.props.update(chain.join("."));
|
||||
async update(fieldName, isFieldSelected) {
|
||||
this.state.fieldName = fieldName;
|
||||
this.state.isDirty = !isFieldSelected;
|
||||
if (isFieldSelected) {
|
||||
await this.props.update(fieldName);
|
||||
} else {
|
||||
this.state.chain = await this.loadChain(this.props.resModel, fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
onFieldSelectorClick(ev) {
|
||||
@@ -65,7 +58,7 @@ export class ModelFieldSelector extends Component {
|
||||
ev.currentTarget,
|
||||
this.constructor.components.Popover,
|
||||
{
|
||||
chain: this.chain,
|
||||
chain: this.state.chain,
|
||||
update: this.update.bind(this),
|
||||
showSearchInput: this.props.showSearchInput,
|
||||
isDebugMode: this.props.isDebugMode,
|
||||
@@ -76,6 +69,11 @@ export class ModelFieldSelector extends Component {
|
||||
{
|
||||
closeOnClickAway: true,
|
||||
popoverClass: "o_popover_field_selector",
|
||||
onClose: () => {
|
||||
if (this.state.isDirty) {
|
||||
this.props.update(this.state.fieldName);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
t-on-click="onFieldSelectorClick"
|
||||
>
|
||||
<div class="o_field_selector_value" tabindex="0">
|
||||
<t t-foreach="chain" t-as="node" t-key="node_index">
|
||||
<t t-foreach="state.chain" t-as="node" t-key="node_index">
|
||||
<t t-if="node.field">
|
||||
<t t-if="!node_first">
|
||||
<i class="fa fa-chevron-right" role="img" aria-label="Followed by" title="Followed-by" />
|
||||
|
||||
@@ -62,12 +62,16 @@ export class ModelFieldSelectorPopover extends Component {
|
||||
const keys = Object.keys(obj);
|
||||
return sortBy(keys, (key) => obj[key].string);
|
||||
}
|
||||
async update() {
|
||||
const fieldNameChain = this.fieldNameChain;
|
||||
this.fullFieldName = fieldNameChain.join(".");
|
||||
await this.props.update(fieldNameChain);
|
||||
async update(isSelected) {
|
||||
const fieldNameChain = this.fieldNameChain.join(".");
|
||||
this.fullFieldName = fieldNameChain;
|
||||
await this.loadFields();
|
||||
this.render();
|
||||
await this.props.update(fieldNameChain, isSelected);
|
||||
if (isSelected) {
|
||||
this.props.close();
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
async onInputKeydown(ev) {
|
||||
@@ -147,11 +151,9 @@ export class ModelFieldSelectorPopover extends Component {
|
||||
this.update();
|
||||
} else if (this.props.needDefaultValue) {
|
||||
this.isDefaultValueVisible = true;
|
||||
this.render();
|
||||
this.update();
|
||||
} else {
|
||||
this.update();
|
||||
this.props.close();
|
||||
this.update(true);
|
||||
this.props.validate(this.fieldNameChain, this.defaultValue);
|
||||
}
|
||||
}
|
||||
@@ -159,8 +161,7 @@ export class ModelFieldSelectorPopover extends Component {
|
||||
if (!acceptDefaultValue) {
|
||||
this.defaultValue = "";
|
||||
}
|
||||
this.props.close();
|
||||
this.update();
|
||||
this.update(true);
|
||||
this.props.validate(this.fieldNameChain, this.defaultValue);
|
||||
}
|
||||
async onFieldNameChange(ev) {
|
||||
@@ -172,7 +173,7 @@ export class ModelFieldSelectorPopover extends Component {
|
||||
} catch {
|
||||
// WOWL TODO: rethrow error when not the expected type
|
||||
this.chain = [{ resModel, field: null }];
|
||||
await this.props.update([]);
|
||||
await this.props.update("");
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { DomainSelector } from "@web/core/domain_selector/domain_selector";
|
||||
import { OPERATOR_DESCRIPTIONS } from "@web/core/domain_selector/domain_selector_operators";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { ormService } from "@web/core/orm_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { viewService } from "@web/views/view_service";
|
||||
import { registerCleanup } from "../helpers/cleanup";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { click, editInput, editSelect, getFixture, mount, triggerEvent } from "../helpers/utils";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { click, editInput, editSelect, getFixture, mount, triggerEvent } from "../helpers/utils";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
@@ -433,7 +434,10 @@ QUnit.module("Components", (hooks) => {
|
||||
);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_selector_chain_part").innerText, "1");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "0"); // option "="
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf_operator_select").value,
|
||||
"equal"
|
||||
); // option "="
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, "1");
|
||||
|
||||
newValue = `[(0, "=", 1)]`;
|
||||
@@ -452,7 +456,10 @@ QUnit.module("Components", (hooks) => {
|
||||
);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_selector_chain_part").innerText, "0");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "0"); // option "="
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf_operator_select").value,
|
||||
"equal"
|
||||
); // option "="
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, "1");
|
||||
}
|
||||
);
|
||||
@@ -469,13 +476,14 @@ QUnit.module("Components", (hooks) => {
|
||||
});
|
||||
|
||||
QUnit.test("operator fallback in edit mode", async (assert) => {
|
||||
registry.category("domain_selector/operator").add("test", {
|
||||
category: "test",
|
||||
OPERATOR_DESCRIPTIONS.push({
|
||||
key: "test",
|
||||
label: "test",
|
||||
value: "test",
|
||||
onDidChange: () => null,
|
||||
matches: ({ operator }) => operator === "test",
|
||||
hideValue: true,
|
||||
symbol: "test",
|
||||
valueMode: "none",
|
||||
});
|
||||
registerCleanup(() => {
|
||||
OPERATOR_DESCRIPTIONS.pop();
|
||||
});
|
||||
|
||||
await mountComponent(DomainSelector, {
|
||||
@@ -542,13 +550,16 @@ QUnit.module("Components", (hooks) => {
|
||||
await mountComponent(Parent);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_selector_chain_part").innerText, "State");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "2"); // option "!="
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf_operator_select").value,
|
||||
"not_equal"
|
||||
); // option "!="
|
||||
|
||||
await editSelect(target, ".o_domain_leaf_operator_select", 0);
|
||||
await editSelect(target, ".o_domain_leaf_operator_select", "equal");
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_selector_chain_part").innerText, "State");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "0"); // option "="
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, "abc");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "equal"); // option "="
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, `"abc"`);
|
||||
});
|
||||
|
||||
QUnit.test("show correct operator", async (assert) => {
|
||||
|
||||
@@ -79,6 +79,7 @@ QUnit.module("Components", (hooks) => {
|
||||
this.fieldName = "";
|
||||
}
|
||||
onUpdate(value) {
|
||||
assert.step(`update: ${value}`);
|
||||
this.fieldName = value;
|
||||
this.render();
|
||||
}
|
||||
@@ -136,6 +137,7 @@ QUnit.module("Components", (hooks) => {
|
||||
"bar",
|
||||
"the selected field should be correctly set"
|
||||
);
|
||||
assert.verifySteps(["update: bar"]);
|
||||
|
||||
// Focusing the input again should open the same popover
|
||||
await click(target, ".o_field_selector");
|
||||
@@ -182,12 +184,14 @@ QUnit.module("Components", (hooks) => {
|
||||
"Product -> Product Name",
|
||||
"field selector value should be displayed with two tags: 'Product' and 'Product Name'"
|
||||
);
|
||||
assert.verifySteps(["update: product_id.name"]);
|
||||
|
||||
// Remove the current selection and recreate it again
|
||||
await click(target, ".o_field_selector");
|
||||
await click(target, ".o_field_selector_prev_page");
|
||||
await click(target, ".o_field_selector_prev_page");
|
||||
await click(target, ".o_field_selector_close");
|
||||
assert.verifySteps(["update: "]);
|
||||
|
||||
await click(target, ".o_field_selector");
|
||||
assert.containsOnce(
|
||||
@@ -210,6 +214,7 @@ QUnit.module("Components", (hooks) => {
|
||||
"Product -> Product Name",
|
||||
"field selector value should be displayed with two tags: 'Product' and 'Product Name'"
|
||||
);
|
||||
assert.verifySteps(["update: product_id.name"]);
|
||||
});
|
||||
|
||||
QUnit.test("default field chain should set the page data correctly", async (assert) => {
|
||||
|
||||
@@ -812,7 +812,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
assert.verifySteps(["/web/webclient/load_menus"]);
|
||||
|
||||
await doAction(webClient, 1);
|
||||
assert.verifySteps(["/web/action/load", "get_views", "read", "search_count", "fields_get"]);
|
||||
assert.verifySteps(["/web/action/load", "get_views", "read", "search_count"]);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
|
||||
assert.containsOnce(target, ".o_datepicker", "there should be a datepicker");
|
||||
|
||||
Reference in New Issue
Block a user