[IMP] *: improve domain selector edition, dom, and style
We improve various aspects of the domain selector: - We make the domain selector be able to display all domains accepted by the class Domain (domain.js) - We improve the edition/selection of values, e.g. for the operators in/not in - We improve/simplify the style/dom Task IDs: 3507123, 3508940 closes odoo/odoo#128680 Related: odoo/enterprise#48324 Signed-off-by: Michaël Mattiello (mcm) <mcm@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: Mathieu Notté <mano@odoo.com>
This commit is contained in:
co-authored by
Mathieu Notté
parent
d9ed827323
commit
6428aef678
@@ -23,6 +23,7 @@ import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import testUtils from "@web/../tests/legacy/helpers/test_utils";
|
||||
import { makeFakeUserService } from "@web/../tests/helpers/mock_services";
|
||||
import * as dsHelpers from "@web/../tests/core/domain_selector_tests";
|
||||
|
||||
const patchDate = testUtils.mock.patchDate;
|
||||
const favoriteMenuRegistry = registry.category("favoriteMenu");
|
||||
@@ -495,7 +496,7 @@ QUnit.module("Board", (hooks) => {
|
||||
// Add a filter ['foo', '=', 'yop']
|
||||
await toggleSearchBarMenu(target);
|
||||
await openAddCustomFilterDialog(target);
|
||||
await editInput(target, ".o_domain_debug_input", `[("foo", "=", "yop")]`);
|
||||
await editInput(target, dsHelpers.SELECTORS.debugArea, `[("foo", "=", "yop")]`);
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
|
||||
// Add to dashboard
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import * as testUtils from "@web/../tests/helpers/utils";
|
||||
import weTestUtils from "@web_editor/../tests/test_utils";
|
||||
import * as dsHelpers from "@web/../tests/core/domain_selector_tests";
|
||||
|
||||
let fixture;
|
||||
let serverData;
|
||||
@@ -377,7 +378,7 @@ QUnit.module('favorite filter widget', (hooks) => {
|
||||
"should show icon to save the filter because domain is set in the mailing");
|
||||
|
||||
// If domain is not set on mailing and no filter available, both drop-down and icon container are hidden
|
||||
await testUtils.click(fixture.querySelector('.o_domain_delete_node_button'));
|
||||
await dsHelpers.clickOnButtonDeleteNode(fixture);
|
||||
assert.isNotVisible(fixture.querySelector('.o_field_mailing_filter .o_input_dropdown'),
|
||||
"should not display drop-down because there is still no filter available to select from");
|
||||
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_filter_container'),
|
||||
@@ -400,7 +401,7 @@ QUnit.module('favorite filter widget', (hooks) => {
|
||||
assert.isNotVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
|
||||
"should not have option to save filter if filter is selected");
|
||||
|
||||
await testUtils.click(fixture.querySelector('.o_domain_add_node_button'));
|
||||
await dsHelpers.clickOnButtonAddNewRule(fixture);
|
||||
await testUtils.nextTick();
|
||||
assert.isVisible(fixture.querySelector('.o_mass_mailing_save_filter_container'),
|
||||
"should have option to save filter because mailing domain is changed");
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
.o_input {
|
||||
color: #3c3c3c !important;
|
||||
}
|
||||
.o_domain_leaf_info {
|
||||
.o_read_mode .o_domain_selector_condition {
|
||||
background: #f6f7fa !important;
|
||||
border-color: #d5dae8 !important;
|
||||
}
|
||||
|
||||
@@ -15,15 +15,15 @@ import { Domain } from "@web/core/domain";
|
||||
import { CheckBox } from "@web/core/checkbox/checkbox";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { getOperatorInfo, toOperator } from "@web/core/domain_selector/domain_selector_operators";
|
||||
import { getPathEditorInfo } from "@web/core/domain_selector/domain_selector_path_editor";
|
||||
import {
|
||||
Editor,
|
||||
PathEditor,
|
||||
getDefaultOperator,
|
||||
getOperatorEditorInfo,
|
||||
} from "@web/core/domain_selector/domain_selector_operator_editor";
|
||||
import {
|
||||
getDefaultValue,
|
||||
getOperatorsInfo,
|
||||
getEditorInfo,
|
||||
} from "@web/core/domain_selector/domain_selector_fields";
|
||||
getValueEditorInfo,
|
||||
} from "@web/core/domain_selector/domain_selector_value_editors";
|
||||
import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector";
|
||||
import { useLoadFieldInfo } from "@web/core/model_field_selector/utils";
|
||||
import { formatValue } from "@web/core/domain_tree";
|
||||
@@ -88,13 +88,11 @@ function restoreVirtualOperators(tree, otherTree) {
|
||||
}
|
||||
|
||||
export class DomainSelector extends Component {
|
||||
static template = "web._DomainSelector";
|
||||
static template = "web.DomainSelector";
|
||||
static components = {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
ModelFieldSelector,
|
||||
Editor,
|
||||
PathEditor,
|
||||
CheckBox,
|
||||
};
|
||||
static props = {
|
||||
@@ -230,7 +228,7 @@ export class DomainSelector extends Component {
|
||||
}
|
||||
|
||||
notifyChanges() {
|
||||
this.previousTree = cloneTree(this.tree);
|
||||
this.previousTree = this.tree ? cloneTree(this.tree) : null;
|
||||
const archiveDomain = this.includeArchived ? `[("active", "in", [True, False])]` : `[]`;
|
||||
const domain = this.tree
|
||||
? Domain.and([buildDomain(this.tree), archiveDomain]).toString()
|
||||
@@ -304,10 +302,28 @@ export class DomainSelector extends Component {
|
||||
}
|
||||
|
||||
getDescription(node) {
|
||||
const { path, value } = node;
|
||||
const fieldDef = this.getFieldDef(path);
|
||||
const operatorInfo = this.getOperatorInfo(node);
|
||||
return leafToString(fieldDef, operatorInfo, value, this.displayNames[fieldDef?.relation]);
|
||||
const fieldDef = this.getFieldDef(node.path);
|
||||
return leafToString(
|
||||
node,
|
||||
fieldDef,
|
||||
this.displayNames[fieldDef?.relation || fieldDef?.comodel]
|
||||
);
|
||||
}
|
||||
|
||||
getPathEditorInfo() {
|
||||
const { resModel, isDebugMode } = this.props;
|
||||
const defaultPath = this.defaultCondition.path;
|
||||
return getPathEditorInfo({ defaultPath, isDebugMode, resModel });
|
||||
}
|
||||
|
||||
getOperatorEditorInfo(node) {
|
||||
const fieldDef = this.getFieldDef(node.path);
|
||||
return getOperatorEditorInfo(fieldDef);
|
||||
}
|
||||
|
||||
getValueEditorInfo(node) {
|
||||
const fieldDef = this.getFieldDef(node.path);
|
||||
return getValueEditorInfo(fieldDef, node.operator);
|
||||
}
|
||||
|
||||
resetDomain() {
|
||||
@@ -321,69 +337,27 @@ export class DomainSelector extends Component {
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
updatePath(node, path, { fieldDef } = {}) {
|
||||
if (!path) {
|
||||
// don't like that
|
||||
Object.assign(node, this.createNewLeaf());
|
||||
} else {
|
||||
node.path = path;
|
||||
node.operator = getDefaultOperator(fieldDef);
|
||||
node.value = getDefaultValue(fieldDef, node.operator);
|
||||
}
|
||||
async updatePath(node, path) {
|
||||
const { fieldDef } = await this.loadFieldInfo(this.props.resModel, path);
|
||||
node.path = path;
|
||||
node.negate = false;
|
||||
node.operator = getDefaultOperator(fieldDef);
|
||||
node.value = getDefaultValue(fieldDef, node.operator);
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
updateLeafOperator(node, operatorKey) {
|
||||
const previousOperatorInfo = getOperatorInfo(node.operator);
|
||||
|
||||
const previousNode = node ? cloneTree(node) : null;
|
||||
|
||||
const [operator, negate] = toOperator(operatorKey);
|
||||
updateLeafOperator(node, operator, negate) {
|
||||
const previousNode = cloneTree(node);
|
||||
const fieldDef = this.getFieldDef(node.path);
|
||||
node.negate = negate;
|
||||
node.operator = operator;
|
||||
|
||||
const editorInfo = this.getEditorInfo(node);
|
||||
if (!editorInfo.isSupported(node.value) || editorInfo.shouldResetValue?.(node.value)) {
|
||||
node.value = getDefaultValue(this.getFieldDef(node.path), node.operator);
|
||||
}
|
||||
|
||||
const operatorInfo = getOperatorInfo(operator);
|
||||
if (previousOperatorInfo.valueCount !== operatorInfo.valueCount) {
|
||||
switch (operatorInfo.valueCount) {
|
||||
// binary operator with a variable sized array value
|
||||
case "variable": {
|
||||
node.value = [];
|
||||
break;
|
||||
}
|
||||
// unary operator (set | not set)
|
||||
case 0: {
|
||||
node.value = false;
|
||||
break;
|
||||
}
|
||||
// binary operator with a non array value
|
||||
case 1: {
|
||||
node.value = getDefaultValue(this.getFieldDef(node.path), node.operator);
|
||||
break;
|
||||
}
|
||||
// binary operator with a fixed sized array value
|
||||
default: {
|
||||
const defaultValue = getDefaultValue(
|
||||
this.getFieldDef(node.path),
|
||||
node.operator
|
||||
);
|
||||
node.value = Array(operatorInfo.valueCount).fill(defaultValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buildDomain(node) === buildDomain(previousNode)) {
|
||||
node.value = getDefaultValue(fieldDef, operator, node.value);
|
||||
if (buildDomain(previousNode) === buildDomain(node)) {
|
||||
// no interesting changes for parent (only possible domain formatting/rewriting)
|
||||
// this means that parent might not render the domain selector
|
||||
// but we need to udpate editors
|
||||
this.render();
|
||||
}
|
||||
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
@@ -396,30 +370,8 @@ export class DomainSelector extends Component {
|
||||
return this.props.update(value, true);
|
||||
}
|
||||
|
||||
getEditorInfo(node) {
|
||||
return getEditorInfo(this.getFieldDef(node.path), node.operator);
|
||||
}
|
||||
|
||||
getOperatorInfo(node) {
|
||||
return getOperatorInfo(node.operator, node.negate);
|
||||
}
|
||||
|
||||
getOperatorsInfo(node) {
|
||||
const fieldDef = this.getFieldDef(node.path);
|
||||
const operatorsInfo = getOperatorsInfo(fieldDef);
|
||||
if (
|
||||
!operatorsInfo.some((op) => op.operator === node.operator && op.negate === node.negate)
|
||||
) {
|
||||
const operatorInfo = this.getOperatorInfo(node);
|
||||
operatorsInfo.push(operatorInfo);
|
||||
}
|
||||
return operatorsInfo;
|
||||
}
|
||||
|
||||
highlightNode(target, toggle, classNames) {
|
||||
const nodeEl = target.closest(".o_domain_node");
|
||||
for (const className of classNames.split(/\s+/i)) {
|
||||
nodeEl.classList.toggle(className, toggle);
|
||||
}
|
||||
highlightNode(target, toggle) {
|
||||
const nodeEl = target.closest(".o_domain_selector_node");
|
||||
nodeEl.classList.toggle("o_hovered_button", toggle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,175 +1,15 @@
|
||||
.o_domain_selector:not(.o_domain_selector_legacy) {
|
||||
cursor: default;
|
||||
|
||||
&.o_domain_node, .o_domain_node {
|
||||
$o-domain-selector-indent: 32px;
|
||||
$o-domain-selector-panel-space: 60px;
|
||||
$o-domain-selector-row-height: 35px; // should be greater than a "normal" row so that "header" parts are the same size of rows
|
||||
$o-domain-animation-bar-height: 15px;
|
||||
|
||||
position: relative;
|
||||
|
||||
// Rows in a domain node (selector) have a minimum height and holds the control panel
|
||||
.o_domain_selector_row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: $o-domain-selector-row-height;
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
> .o_domain_node_control_panel {
|
||||
@include o-position-absolute($top: 0, $bottom: 0, $right: -$o-domain-selector-panel-space);
|
||||
|
||||
> button {
|
||||
float: left;
|
||||
width: $o-domain-selector-panel-space/2;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
font-size: 16px;
|
||||
opacity: 0.2;
|
||||
cursor: pointer;
|
||||
|
||||
&.o_domain_delete_node_button {
|
||||
font-size: 12px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.o_domain_selector {
|
||||
.o_domain_selector_node {
|
||||
.o_domain_selector_node_control_panel > button {
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
// Rules specific to domain parts which contain sub-domain parts
|
||||
&.o_domain_tree {
|
||||
> .o_domain_tree_header {
|
||||
position: relative;
|
||||
}
|
||||
.o_domain_tree_connector_caret::after {
|
||||
@include o-caret-down;
|
||||
}
|
||||
&.o_hovered_button {
|
||||
.o_domain_selector_node_control_panel > button {
|
||||
opacity: .5;
|
||||
|
||||
& .o_domain_node_children_container {
|
||||
padding-left: $o-domain-selector-indent;
|
||||
}
|
||||
|
||||
// Rules specific to domain root
|
||||
&.o_domain_selector {
|
||||
> .o_domain_tree_header {
|
||||
min-height: 22px; // prevents "records button" to overflow 1-row domain
|
||||
}
|
||||
|
||||
> .o_domain_node_children_container {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&.o_edit_mode > .o_domain_node_children_container {
|
||||
padding-right: $o-domain-selector-panel-space; // delete button is over the row
|
||||
}
|
||||
|
||||
> .o_domain_debug_container {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
padding: 8px 10px 12px;
|
||||
background: var(--DomainDebugContainer-background-colomap-get, #{$o-gray-900});
|
||||
color: rgba(white, 0.5);
|
||||
font-family: monospace;
|
||||
font-weight: normal;
|
||||
|
||||
> textarea {
|
||||
border: none;
|
||||
padding-top: 8px;
|
||||
background: transparent;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rules specific to domain parts which do not contain sub-domain parts
|
||||
&.o_domain_leaf {
|
||||
&.o_read_mode {
|
||||
display: inline-flex;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
> .o_domain_leaf_info {
|
||||
background: $o-brand-lightsecondary;
|
||||
border: 1px solid darken($o-brand-lightsecondary, 10%);
|
||||
padding: 2px 4px;
|
||||
|
||||
> .o_model_field_selector {
|
||||
float: left;
|
||||
margin-right: 4px;
|
||||
|
||||
> .o_model_field_selector_value > .o_model_field_selector_chain_part {
|
||||
border: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
.o_domain_leaf_operator {
|
||||
font-style: italic;
|
||||
}
|
||||
.o_domain_leaf_value {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
> .o_domain_leaf_edition {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 4px ($o-domain-selector-panel-space/2) 4px 0;
|
||||
|
||||
> * {
|
||||
width: auto;
|
||||
|
||||
&.o_model_field_selector {
|
||||
flex: 0 1 40%;
|
||||
width: 40%;
|
||||
|
||||
> .o_model_field_selector_value > .o_model_field_selector_chain_part {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child { // field value
|
||||
flex: 1 1 25%;
|
||||
}
|
||||
|
||||
+ * {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
input, select, .o_datepicker, .o_datepicker_input {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Animation effects
|
||||
transition: margin-bottom .05s ease .05s;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
@include o-position-absolute($left: 0, $right: 0, $top: 100%);
|
||||
display: block;
|
||||
height: $o-domain-animation-bar-height;
|
||||
max-height: 0;
|
||||
background-image: linear-gradient(45deg, rgba($o-brand-primary, 0.4), rgba($o-brand-primary, 0.1));
|
||||
transition: max-height .05s ease .05s;
|
||||
}
|
||||
|
||||
&.o_hover_btns, &.o_hover_add_node {
|
||||
.o_domain_node_control_panel > button {
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
&.o_domain_delete_node_button {
|
||||
color: var(--danger);
|
||||
}
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +1,103 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web._DomainSelector">
|
||||
<div class="o_domain_node o_domain_tree o_domain_selector w-100" aria-atomic="true" t-att-class="className" t-ref="root">
|
||||
<t t-name="web.DomainSelector">
|
||||
<div class="o_domain_selector w-100" aria-atomic="true" t-att-class="className">
|
||||
<t t-if="tree">
|
||||
<t t-set="node" t-value="tree"/>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<t t-if="node.children.length === 0">
|
||||
<span>Match <strong>all records</strong></span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span>Match </span>
|
||||
<t t-call="web._DomainSelector.connector.dropdown" />
|
||||
<span> of the following rules:</span>
|
||||
</t>
|
||||
<div t-attf-class="o_domain_selector_node d-flex flex-column #{props.readonly ? 'gap-2' : 'gap-1'}">
|
||||
<t t-set="node" t-value="tree" />
|
||||
<div class="o_domain_selector_row d-flex align-items-center">
|
||||
<div class="o_domain_selector_connector d-flex flex-grow-1 align-items-center">
|
||||
<t t-if="node.children.length">
|
||||
<span>Match</span>
|
||||
<t t-if="node.children.length > 1">
|
||||
<t t-call="web.DomainSelector.connector.dropdown" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="px-1">
|
||||
<t t-call="web.DomainSelector.connector.title" />
|
||||
</span>
|
||||
</t>
|
||||
<span>of the following rules:</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span>Match <strong>all records</strong></span>
|
||||
</t>
|
||||
</div>
|
||||
<CheckBox
|
||||
t-if="showArchivedCheckbox"
|
||||
value="includeArchived"
|
||||
disabled="props.readonly"
|
||||
className="'form-switch'"
|
||||
onChange.bind="toggleIncludeArchived"
|
||||
>
|
||||
Include archived
|
||||
</CheckBox>
|
||||
</div>
|
||||
<CheckBox
|
||||
t-if="showArchivedCheckbox"
|
||||
value="includeArchived"
|
||||
disabled="props.readonly"
|
||||
className="'form-switch'"
|
||||
onChange.bind="toggleIncludeArchived"
|
||||
>
|
||||
Include archived
|
||||
</CheckBox>
|
||||
<t t-if="node.children.length" t-call="web.DomainSelector.connector.children" />
|
||||
<t t-if="!props.readonly">
|
||||
<div class="o_domain_selector_row d-flex align-items-center">
|
||||
<a href="#" role="button" t-on-click="() => this.insertRootLeaf(node)">New Rule</a>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="node.children.length" t-call="web._DomainSelector.connector.children" />
|
||||
<t t-if="!props.readonly">
|
||||
<a href="#" class="pt-1 ms-1" role="button" t-on-click="() => this.insertRootLeaf(node)">New Rule</a>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
This domain is not supported.
|
||||
<t t-if="!props.readonly">
|
||||
<button class="btn btn-sm btn-primary o_reset_domain_button ms-1" t-on-click="() => this.resetDomain()">Reset domain</button>
|
||||
</t>
|
||||
<div class="o_domain_selector_row d-flex align-items-center">
|
||||
This domain is not supported.
|
||||
<t t-if="!props.readonly">
|
||||
<button t-on-click="() => this.resetDomain()">Reset domain</button>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="props.isDebugMode and (!tree or !props.readonly)">
|
||||
<label class="o_domain_debug_container">
|
||||
<label class="o_domain_selector_debug_container d-block mt-3 border rounded p-3 bg-100 text-muted font-monospace">
|
||||
<span class="small"># Code editor</span>
|
||||
<textarea type="text" class="o_domain_debug_input" t-att-readonly="props.readonly" spellcheck="false" t-att-value="props.domain" t-on-change="(ev) => this.onDebugValueChange(ev.target.value)" />
|
||||
<textarea class="pt-2 border-0 bg-transparent text-body" type="text" t-att-readonly="props.readonly" spellcheck="false" t-att-value="props.domain" t-on-change="(ev) => this.onDebugValueChange(ev.target.value)" />
|
||||
</label>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.controls">
|
||||
<div class="o_domain_node_control_panel" role="toolbar" aria-label="Domain node">
|
||||
<t t-name="web.DomainSelector.controls">
|
||||
<div class="o_domain_selector_node_control_panel d-flex" role="toolbar" aria-label="Domain node">
|
||||
<button
|
||||
class="btn o_domain_add_node_button"
|
||||
class="btn px-2 fs-4"
|
||||
role="button"
|
||||
title="Add New Rule"
|
||||
aria-label="Add New Rule"
|
||||
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')"
|
||||
t-on-mouseenter="(ev) => this.highlightNode(ev.target, true)"
|
||||
t-on-mouseleave="(ev) => this.highlightNode(ev.target, false)"
|
||||
>
|
||||
<i class="fa fa-plus"/>
|
||||
<i class="fa fa-plus" />
|
||||
</button>
|
||||
<button
|
||||
class="btn o_domain_add_node_button"
|
||||
class="btn px-2 fs-4"
|
||||
role="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')"
|
||||
t-on-mouseenter="(ev) => this.highlightNode(ev.target, true)"
|
||||
t-on-mouseleave="(ev) => this.highlightNode(ev.target, false)"
|
||||
>
|
||||
<i class="fa fa-sitemap"/>
|
||||
<i class="fa fa-sitemap" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-link text-danger o_domain_delete_node_button"
|
||||
class="btn btn-link px-2 text-danger fs-4"
|
||||
role="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')"
|
||||
t-on-mouseenter="(ev) => this.highlightNode(ev.target, true)"
|
||||
t-on-mouseleave="(ev) => this.highlightNode(ev.target, false)"
|
||||
>
|
||||
<i class="fa fa-trash"/>
|
||||
<i class="fa fa-trash" />
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.connector">
|
||||
<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.connector.dropdown" />
|
||||
<span class="ml4">of:</span>
|
||||
</div>
|
||||
<t t-call="web._DomainSelector.connector.children" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.connector.dropdown">
|
||||
<t t-name="web.DomainSelector.connector.title">
|
||||
<t t-set="title">
|
||||
<t t-if="node.value === '|'">
|
||||
<t t-if="node.negate">none</t>
|
||||
@@ -105,16 +108,20 @@
|
||||
<t t-else="">all</t>
|
||||
</t>
|
||||
</t>
|
||||
<t t-esc="title" />
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.connector.dropdown">
|
||||
<t t-if="props.readonly">
|
||||
<strong>
|
||||
<t t-esc="title"/>
|
||||
<strong class="px-1">
|
||||
<t t-call="web.DomainSelector.connector.title" />
|
||||
</strong>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="d-inline-flex o_domain_tree_connector_selector" aria-atomic="true">
|
||||
<Dropdown togglerClass="'btn btn-link btn-primary py-0 px-1 o_domain_tree_connector_caret'">
|
||||
<div aria-atomic="true">
|
||||
<Dropdown togglerClass="'btn btn-link btn-primary py-0 px-1'" showCaret="true">
|
||||
<t t-set-slot="toggler">
|
||||
<t t-esc="title"/>
|
||||
<t t-call="web.DomainSelector.connector.title" />
|
||||
</t>
|
||||
<DropdownItem onSelected="() => this.updateBranchConnector(node, '&')">all</DropdownItem>
|
||||
<DropdownItem onSelected="() => this.updateBranchConnector(node, '|')">any</DropdownItem>
|
||||
@@ -123,65 +130,84 @@
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.connector.children">
|
||||
<div class="o_domain_node_children_container">
|
||||
<t t-foreach="node.children" t-as="child" t-key="child.type + '_' + child_index">
|
||||
<t t-call="web._DomainSelector.{{ child.type }}">
|
||||
<t t-name="web.DomainSelector.connector.children">
|
||||
<t t-foreach="node.children" t-as="child" t-key="child.type + '_' + child_index">
|
||||
<div class="o_domain_selector_node" t-att-class="{ 'ps-4': addPadding }">
|
||||
<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.condition">
|
||||
<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.condition:readonly" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="web._DomainSelector.controls" />
|
||||
<div class="o_domain_leaf_edition">
|
||||
<t t-call="web._DomainSelector.condition:editable" />
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.condition:readonly">
|
||||
<ModelFieldSelector path="node.path" resModel="props.resModel" readonly="true" />
|
||||
<t t-set="description" t-value="getDescription(node)"/>
|
||||
<span class="o_domain_leaf_operator"> <t t-esc="description.operatorDescription" /> </span>
|
||||
<t t-if="description.valueDescription">
|
||||
<t t-set="values" t-value="description.valueDescription.values"/>
|
||||
<t t-set="join" t-value="description.valueDescription.join"/>
|
||||
<t t-set="addParenthesis" t-value="description.valueDescription.addParenthesis"/>
|
||||
<t t-if="addParenthesis">( </t>
|
||||
<t t-foreach="values" t-as="val" t-key="val_index">
|
||||
<span class="o_domain_leaf_value text-primary"><t t-esc="val"/></span>
|
||||
<t t-if="!val_last"> <t t-esc="join" /> </t>
|
||||
</t>
|
||||
<t t-if="addParenthesis"> )</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web._DomainSelector.condition:editable">
|
||||
<PathEditor path="node.path" resModel="props.resModel" readonly="false" update="(path, fieldInfo) => this.updatePath(node, path, fieldInfo)" isDebugMode="props.isDebugMode" />
|
||||
<div>
|
||||
<select class="o_domain_leaf_operator_select o_input text-truncate pe-3" t-on-change="(ev) => this.updateLeafOperator(node, ev.target.value)">
|
||||
<t t-foreach="getOperatorsInfo(node)" t-as="operatorInfo" t-key="operatorInfo.key">
|
||||
<option t-att-value="operatorInfo.key" t-att-selected="node.operator === operatorInfo.operator and node.negate === operatorInfo.negate" t-esc="operatorInfo.label" />
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<t t-if="getOperatorInfo(node).valueCount !== 0">
|
||||
<div class="o_ds_value_cell">
|
||||
<Editor info="getEditorInfo(node)" value="node.value" update="(value) => this.updateLeafValue(node, value)" fieldDef="getFieldDef(node.path)"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.connector">
|
||||
<div class="o_domain_selector_row d-flex align-items-center">
|
||||
<div class="o_domain_selector_connector d-flex flex-grow-1">
|
||||
<t t-call="web.DomainSelector.connector.dropdown" />
|
||||
<span>of:</span>
|
||||
</div>
|
||||
<t t-if="!props.readonly">
|
||||
<t t-call="web.DomainSelector.controls" />
|
||||
</t>
|
||||
</div>
|
||||
<t t-call="web.DomainSelector.connector.children">
|
||||
<t t-set="addPadding" t-value="true" />
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.condition">
|
||||
<div class="o_domain_selector_row d-flex align-items-center">
|
||||
<t t-if="props.readonly">
|
||||
<t t-call="web.DomainSelector.condition:readonly" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="web.DomainSelector.condition:editable" />
|
||||
<t t-call="web.DomainSelector.controls" />
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.condition:readonly">
|
||||
<t t-set="description" t-value="getDescription(node)" />
|
||||
<div class="o_domain_selector_condition d-flex gap-1 px-2 border bg-100">
|
||||
<ModelFieldSelector path="node.path" resModel="props.resModel" readonly="true" />
|
||||
<span class="fst-italic" t-esc="description.operatorDescription" />
|
||||
<t t-if="description.valueDescription">
|
||||
<t t-set="values" t-value="description.valueDescription.values" />
|
||||
<t t-set="join" t-value="description.valueDescription.join" />
|
||||
<t t-set="addParenthesis" t-value="description.valueDescription.addParenthesis" />
|
||||
<t t-if="addParenthesis">( </t>
|
||||
<t t-foreach="values" t-as="val" t-key="val_index">
|
||||
<span class="text-primary fw-bolder"><t t-esc="val" /></span>
|
||||
<t t-if="!val_last"> <t t-esc="join" /> </t>
|
||||
</t>
|
||||
<t t-if="addParenthesis"> )</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.condition:editable">
|
||||
<div class="o_domain_selector_condition row flex-nowrap align-items-end flex-grow-1">
|
||||
<t t-call="web.DomainSelector.Editor">
|
||||
<t t-set="_classes" t-value="'col col-md-4 pe-0'" />
|
||||
<t t-set="info" t-value="getPathEditorInfo(node)" />
|
||||
<t t-set="value" t-value="node.path" />
|
||||
<t t-set="update" t-value="(path) => this.updatePath(node, path)" />
|
||||
</t>
|
||||
<t t-call="web.DomainSelector.Editor">
|
||||
<t t-set="_classes" t-value="'col col-md-2 px-3'" />
|
||||
<t t-set="info" t-value="getOperatorEditorInfo(node)" />
|
||||
<t t-set="value" t-value="[node.operator, node.negate]" />
|
||||
<t t-set="update" t-value="(operator, negate) => this.updateLeafOperator(node, operator, negate)" />
|
||||
</t>
|
||||
<t t-call="web.DomainSelector.Editor">
|
||||
<t t-set="_classes" t-value="'col ps-0 overflow-hidden'" />
|
||||
<t t-set="info" t-value="getValueEditorInfo(node)" />
|
||||
<t t-set="value" t-value="node.value" />
|
||||
<t t-set="update" t-value="(value) => this.updateLeafValue(node, value)" />
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { TagsList } from "@web/core/tags_list/tags_list";
|
||||
|
||||
export class Input extends Component {
|
||||
static props = ["value", "update", "startEmpty?"];
|
||||
static template = "web.DomainSelector.Input";
|
||||
}
|
||||
|
||||
export class Select extends Component {
|
||||
static props = ["value", "update", "options", "addBlankOption?"];
|
||||
static template = "web.DomainSelector.Select";
|
||||
|
||||
deserialize(value) {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
|
||||
serialize(value) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
export class Range extends Component {
|
||||
static props = ["value", "update", "editorInfo"];
|
||||
static template = "web.DomainSelector.Range";
|
||||
|
||||
update(index, newValue) {
|
||||
const result = [...this.props.value];
|
||||
result[index] = newValue;
|
||||
return this.props.update(result);
|
||||
}
|
||||
}
|
||||
|
||||
export class List extends Component {
|
||||
static components = { TagsList };
|
||||
static props = ["value", "update", "editorInfo"];
|
||||
static template = "web.DomainSelector.List";
|
||||
|
||||
get tags() {
|
||||
const { isSupported, stringify } = this.props.editorInfo;
|
||||
return this.props.value.map((val, index) => ({
|
||||
text: stringify(val),
|
||||
colorIndex: isSupported(val) ? 0 : 2,
|
||||
onDelete: () => {
|
||||
this.props.update([
|
||||
...this.props.value.slice(0, index),
|
||||
...this.props.value.slice(index + 1),
|
||||
]);
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
update(newValue) {
|
||||
return this.props.update([...this.props.value, newValue]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelector.Editor">
|
||||
<t t-if="!info.isSupported(value)">
|
||||
<div t-attf-class="o_domain_selector_editor #{_classes}">
|
||||
<div class="o_input d-flex align-items-center">
|
||||
<span class="flex-grow-1 text-truncate" t-esc="info.stringify(value)" />
|
||||
<i role="alert" class="fa fa-exclamation-triangle text-warning mx-2" t-att-title="(typeof info.message === 'function') ? info.message(value) : info.message" />
|
||||
<i class="fa fa-times" title="Clear" t-on-click="() => update(info.defaultValue())" />
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="info.component">
|
||||
<div t-attf-class="o_domain_selector_editor #{_classes}">
|
||||
<t t-component="info.component" t-props="info.extractProps({ update, value })" />
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.Input">
|
||||
<input type="text" class="o_input" t-att-value="props.startEmpty ? '' : props.value" t-on-change="(ev) => props.update(ev.target.value)" />
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.Select">
|
||||
<select class="o_input pe-3 text-truncate" t-on-change="(ev) => props.update(deserialize(ev.target.value))" >
|
||||
<option t-if="props.addBlankOption" hidden="true" />
|
||||
<t t-foreach="props.options" t-as="option" t-key="serialize(option[0])">
|
||||
<option t-att-value="serialize(option[0])" t-att-selected="!props.addBlankOption and option[0] === props.value" t-esc="option[1]" />
|
||||
</t>
|
||||
</select>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.Range">
|
||||
<div class="d-flex align-items-center">
|
||||
<t t-call="web.DomainSelector.Editor">
|
||||
<t t-set="_classes" t-value="'overflow-hidden flex-grow-1'"/>
|
||||
<t t-set="info" t-value="props.editorInfo" />
|
||||
<t t-set="value" t-value="props.value[0]" />
|
||||
<t t-set="update" t-value="(val) => this.update(0, val)" />
|
||||
</t>
|
||||
<i class="fa fa-long-arrow-right mx-2" aria-label="Arrow icon" title="Arrow" />
|
||||
<t t-call="web.DomainSelector.Editor">
|
||||
<t t-set="_classes" t-value="'overflow-hidden flex-grow-1'"/>
|
||||
<t t-set="info" t-value="props.editorInfo" />
|
||||
<t t-set="value" t-value="props.value[1]" />
|
||||
<t t-set="update" t-value="(val) => this.update(1, val)" />
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.List">
|
||||
<div class="o_input d-flex flex-wrap gap-1">
|
||||
<TagsList tags="tags"/>
|
||||
<div class="flex-grow-1">
|
||||
<t t-call="web.DomainSelector.Editor">
|
||||
<t t-set="info" t-value="props.editorInfo" />
|
||||
<t t-set="value" t-value="props.editorInfo.defaultValue()" />
|
||||
<t t-set="update" t-value="(val) => this.update(val)" />
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,517 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { selectOperators } from "@web/core/domain_selector/domain_selector_operators";
|
||||
import {
|
||||
deserializeDate,
|
||||
deserializeDateTime,
|
||||
serializeDate,
|
||||
serializeDateTime,
|
||||
} from "@web/core/l10n/dates";
|
||||
import { _t } 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";
|
||||
import { DateTimeInput } from "../datetime/datetime_input";
|
||||
import { TagsList } from "@web/core/tags_list/tags_list";
|
||||
import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector";
|
||||
import { Expression, formatValue } from "@web/core/domain_tree";
|
||||
import {
|
||||
DomainSelectorAutocomplete,
|
||||
DomainSelectorSingleAutocomplete,
|
||||
} from "./domain_selector_autocomplete";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
const identity = (x) => x;
|
||||
const isString = (value) => typeof value === "string";
|
||||
|
||||
// ============================================================================
|
||||
|
||||
export class Editor extends Component {
|
||||
static props = ["info", "value", "update", "fieldDef?"];
|
||||
static template = "web.DomainSelector.Editor";
|
||||
|
||||
get stringifiedValue() {
|
||||
return formatValue(this.props.value);
|
||||
}
|
||||
}
|
||||
|
||||
export class PathEditor extends Editor {
|
||||
static props = ["isDebugMode", "readonly", "resModel", "path", "update"];
|
||||
static template = "web.DomainSelector.PathEditor";
|
||||
|
||||
get component() {
|
||||
return ModelFieldSelector;
|
||||
}
|
||||
|
||||
get isSupportedPath() {
|
||||
const { path } = this.props;
|
||||
return [0, 1].includes(path) || typeof path === "string";
|
||||
}
|
||||
|
||||
get stringifiedPath() {
|
||||
return formatValue(this.props.path);
|
||||
}
|
||||
|
||||
onClear() {
|
||||
this.props.update();
|
||||
}
|
||||
}
|
||||
|
||||
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 components = { TagsList };
|
||||
static props = ["value", "update"];
|
||||
static template = "web.DomainSelector.TagInput";
|
||||
get value() {
|
||||
return Array.isArray(this.props.value) ? this.props.value : [this.props.value];
|
||||
}
|
||||
get tags() {
|
||||
return this.value.map((val, index) => ({
|
||||
text: String(val),
|
||||
colorIndex: typeof val === "string" ? 0 : 2,
|
||||
onDelete: () => {
|
||||
this.props.update([...this.value.slice(0, index), ...this.value.slice(index + 1)]);
|
||||
},
|
||||
}));
|
||||
}
|
||||
onChange(ev) {
|
||||
const newVal = ev.currentTarget.value;
|
||||
ev.currentTarget.value = "";
|
||||
this.props.update([...this.value, newVal]);
|
||||
}
|
||||
}
|
||||
|
||||
class Range extends Component {
|
||||
static props = ["value", "update", "editorInfo", "fieldDef"];
|
||||
static template = "web.DomainSelector.Range";
|
||||
static components = { Editor };
|
||||
|
||||
update(index, newValue) {
|
||||
const result = [...this.props.value];
|
||||
result[index] = newValue;
|
||||
return this.props.update(result);
|
||||
}
|
||||
|
||||
getValue(index) {
|
||||
return this.props.value[index];
|
||||
}
|
||||
|
||||
getUpdater(index) {
|
||||
return (newValue) => this.update(index, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
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, isSupported, defaultValue } = {}) {
|
||||
return {
|
||||
component,
|
||||
extractProps: props || (({ value, update }) => ({ value, update })),
|
||||
isSupported: isSupported || ((value) => !(value instanceof Expression)),
|
||||
defaultValue:
|
||||
defaultValue ||
|
||||
((fieldDef, operator) => {
|
||||
switch (operator) {
|
||||
case "in":
|
||||
case "not in":
|
||||
return [];
|
||||
case "set":
|
||||
case "not_set":
|
||||
case "child_of":
|
||||
case "parent_of":
|
||||
return false;
|
||||
case "is":
|
||||
case "is_not":
|
||||
return true;
|
||||
case "=like":
|
||||
case "=ilike":
|
||||
case "like":
|
||||
case "not like":
|
||||
case "ilike":
|
||||
case "not ilike":
|
||||
return "";
|
||||
default: {
|
||||
let val;
|
||||
switch (fieldDef?.type) {
|
||||
case "integer":
|
||||
case "float":
|
||||
case "monetary":
|
||||
val = 1;
|
||||
break;
|
||||
case "boolean":
|
||||
val = false;
|
||||
break;
|
||||
default:
|
||||
val = "";
|
||||
}
|
||||
if (operator === "between") {
|
||||
return [val, val];
|
||||
}
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {"date" | "datetime"} type
|
||||
* @param {Parameters<serializeDate>} args
|
||||
*/
|
||||
const genericSerializeDate = (type, ...args) =>
|
||||
type === "date" ? serializeDate(...args) : serializeDateTime(...args);
|
||||
|
||||
/**
|
||||
* @param {"date" | "datetime"} type
|
||||
* @param {Parameters<deserializeDate>} args
|
||||
*/
|
||||
const genericDeserializeDate = (type, ...args) =>
|
||||
type === "date" ? deserializeDate(...args) : deserializeDateTime(...args);
|
||||
|
||||
const DEFAULT = {
|
||||
operators: [
|
||||
"=",
|
||||
"!=",
|
||||
">",
|
||||
">=",
|
||||
"<",
|
||||
"<=",
|
||||
"ilike",
|
||||
"not ilike",
|
||||
"like",
|
||||
"not like",
|
||||
"=like",
|
||||
"=ilike",
|
||||
"child_of",
|
||||
"parent_of",
|
||||
"in",
|
||||
"not in",
|
||||
"set",
|
||||
"not_set",
|
||||
],
|
||||
editors: { default: makeEditor(Input) },
|
||||
defaultValue: () => "",
|
||||
};
|
||||
|
||||
const SET_NOT_SET_EDITOR = {
|
||||
component: null,
|
||||
extractProps: identity,
|
||||
isSupported: (value) => value === false,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const BOOLEAN = {
|
||||
operators: ["is", "is_not"],
|
||||
editors: {
|
||||
default: makeEditor(Select, {
|
||||
props: ({ value, update }) => ({
|
||||
value,
|
||||
update,
|
||||
options: [
|
||||
[true, _t("set")],
|
||||
[false, _t("not set")],
|
||||
],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
defaultValue: () => true,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const DATETIME_EDITOR_BETWEEN = makeEditor(Range, {
|
||||
props: ({ value, update, fieldDef }) => ({
|
||||
value,
|
||||
update,
|
||||
editorInfo: getEditorInfo(fieldDef),
|
||||
fieldDef,
|
||||
}),
|
||||
});
|
||||
|
||||
const DATETIME = {
|
||||
operators: ["=", "!=", ">", ">=", "<", "<=", "between", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(DateTimeInput, {
|
||||
props: ({ value, update, fieldDef }) => ({
|
||||
value: genericDeserializeDate(fieldDef.type, value),
|
||||
type: fieldDef.type,
|
||||
onApply: (value) =>
|
||||
update(value ? genericSerializeDate(fieldDef.type, value) : DateTime.local()),
|
||||
}),
|
||||
}),
|
||||
between: DATETIME_EDITOR_BETWEEN,
|
||||
},
|
||||
defaultValue: ({ type }) => genericSerializeDate(type, DateTime.local()),
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const TEXT = {
|
||||
operators: ["=", "!=", "ilike", "not ilike", "in", "not in", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(Input),
|
||||
in: makeEditor(TagInput),
|
||||
"not in": makeEditor(TagInput),
|
||||
},
|
||||
defaultValue: () => "",
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const NUMBER = {
|
||||
operators: ["=", "!=", ">", ">=", "<", "<=", "between", "ilike", "not ilike", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(Input, {
|
||||
props: ({ value, update, fieldDef }) => ({
|
||||
value: String(value),
|
||||
update: (value) => update(parseValue(fieldDef.type, value)),
|
||||
}),
|
||||
}),
|
||||
between: makeEditor(Range, {
|
||||
props: ({ value, update, fieldDef }) => ({
|
||||
value,
|
||||
update,
|
||||
editorInfo: getEditorInfo(fieldDef),
|
||||
fieldDef,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
defaultValue: () => 1,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const isId = (value) => Number.isInteger(value) && value >= 1;
|
||||
|
||||
const RELATIONAL_EDITOR_EQUALITY = makeEditor(DomainSelectorSingleAutocomplete, {
|
||||
props: ({ value, update, fieldDef }) => {
|
||||
return {
|
||||
resModel: fieldDef.relation,
|
||||
fieldString: fieldDef.string,
|
||||
update,
|
||||
resId: value,
|
||||
};
|
||||
},
|
||||
isSupported: () => true,
|
||||
});
|
||||
RELATIONAL_EDITOR_EQUALITY.shouldResetValue = (value) => value !== false || !isId(value);
|
||||
|
||||
const RELATIONAL_EDITOR_IN = makeEditor(DomainSelectorAutocomplete, {
|
||||
props: ({ value, update, fieldDef }) => {
|
||||
return {
|
||||
resModel: fieldDef.relation,
|
||||
fieldString: fieldDef.string,
|
||||
update: (resIds) => update([...new Set(resIds)]),
|
||||
resIds: Array.isArray(value) ? [...new Set(value)] : value,
|
||||
};
|
||||
},
|
||||
isSupported: (value) => Array.isArray(value),
|
||||
});
|
||||
|
||||
const MANY2ONE = {
|
||||
operators: ["in", "not in", "=", "!=", "ilike", "not ilike", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(Input, { isSupported: isString }), // ilike, not ilike + others (found in original domain)
|
||||
"=": RELATIONAL_EDITOR_EQUALITY,
|
||||
"!=": RELATIONAL_EDITOR_EQUALITY,
|
||||
in: RELATIONAL_EDITOR_IN,
|
||||
"not in": RELATIONAL_EDITOR_IN,
|
||||
set: SET_NOT_SET_EDITOR,
|
||||
not_set: SET_NOT_SET_EDITOR,
|
||||
},
|
||||
defaultValue: (_, operator) => {
|
||||
switch (operator) {
|
||||
case "in":
|
||||
case "not in":
|
||||
return [];
|
||||
case "=":
|
||||
case "!=":
|
||||
case "set":
|
||||
case "not_set":
|
||||
return false;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const X2MANY = {
|
||||
operators: ["in", "not in", "=", "!=", "ilike", "not ilike", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(Input, { isSupported: isString }), // ilike, not ilike + others (found in original domain)
|
||||
in: RELATIONAL_EDITOR_IN,
|
||||
"not in": RELATIONAL_EDITOR_IN,
|
||||
"=": RELATIONAL_EDITOR_IN,
|
||||
"!=": RELATIONAL_EDITOR_IN,
|
||||
set: SET_NOT_SET_EDITOR,
|
||||
not_set: SET_NOT_SET_EDITOR,
|
||||
},
|
||||
defaultValue: (_, operator) => {
|
||||
switch (operator) {
|
||||
case "in":
|
||||
case "not in":
|
||||
case "=":
|
||||
case "!=":
|
||||
return [];
|
||||
case "set":
|
||||
case "not_set":
|
||||
return false;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const SELECTION_EDITOR_IN = makeEditor(Input, {
|
||||
props: ({ value, update }) => ({
|
||||
value: formatAST(toPyValue(value)),
|
||||
update: (value) => update(evaluateExpr(value)),
|
||||
}),
|
||||
});
|
||||
|
||||
const SELECTION = {
|
||||
operators: ["=", "!=", "in", "not in", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(Select, {
|
||||
props: ({ value, update, fieldDef }) => ({
|
||||
value,
|
||||
update,
|
||||
options: fieldDef.selection || [],
|
||||
}),
|
||||
}),
|
||||
in: SELECTION_EDITOR_IN,
|
||||
"not in": SELECTION_EDITOR_IN,
|
||||
},
|
||||
defaultValue: (fieldDef) => fieldDef.selection[0][0] ?? false,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const PROPERTIES = {
|
||||
operators: ["set", "not_set"],
|
||||
defaultValue: () => false,
|
||||
};
|
||||
|
||||
const PROPERTIES_SELECTION = {
|
||||
operators: ["=", "!=", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(Select, {
|
||||
props: ({ value, update, fieldDef }) => ({
|
||||
value,
|
||||
update,
|
||||
options: fieldDef.selection || [],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
defaultValue: ({ selection }) => selection?.[0]?.[0] ?? false,
|
||||
};
|
||||
|
||||
const PROPERTIES_RELATIONAL = {
|
||||
operators: ["=", "!=", "set", "not_set"],
|
||||
editors: { default: makeEditor(Input) },
|
||||
defaultValue: (fieldDef) => (fieldDef.type === "many2one" ? 1 : []),
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const JSON_FIELD = {
|
||||
operators: ["=", "!=", "ilike", "not ilike", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(Input),
|
||||
},
|
||||
defaultValue: () => "",
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
export const FIELD_DESCRIPTIONS = {
|
||||
boolean: BOOLEAN,
|
||||
char: TEXT,
|
||||
date: DATETIME,
|
||||
datetime: DATETIME,
|
||||
float: NUMBER,
|
||||
html: TEXT,
|
||||
integer: NUMBER,
|
||||
json: JSON_FIELD,
|
||||
many2many: X2MANY,
|
||||
many2one: MANY2ONE,
|
||||
monetary: NUMBER,
|
||||
one2many: X2MANY,
|
||||
properties: PROPERTIES,
|
||||
properties_definition: PROPERTIES,
|
||||
selection: SELECTION,
|
||||
text: TEXT,
|
||||
};
|
||||
|
||||
const PROPERTIES_DESCRIPTIONS = {
|
||||
selection: PROPERTIES_SELECTION,
|
||||
many2many: PROPERTIES_RELATIONAL,
|
||||
many2one: PROPERTIES_RELATIONAL,
|
||||
one2many: PROPERTIES_RELATIONAL,
|
||||
tags: PROPERTIES_RELATIONAL,
|
||||
};
|
||||
|
||||
export function getFieldInfo(fieldDef) {
|
||||
const { is_property, type } = fieldDef || {};
|
||||
if (is_property && PROPERTIES_DESCRIPTIONS[type]) {
|
||||
return PROPERTIES_DESCRIPTIONS[type];
|
||||
} else if (FIELD_DESCRIPTIONS[type]) {
|
||||
return FIELD_DESCRIPTIONS[type];
|
||||
} else {
|
||||
return DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
export function getEditorInfo(fieldDef, operator) {
|
||||
const descr = getFieldInfo(fieldDef);
|
||||
const editorInfo = descr.editors[operator] || descr.editors.default;
|
||||
editorInfo.defaultValue = () => getDefaultValue(fieldDef, operator);
|
||||
return editorInfo;
|
||||
}
|
||||
|
||||
export function getOperatorsInfo(fieldDef) {
|
||||
const descr = getFieldInfo(fieldDef);
|
||||
return selectOperators(descr.operators);
|
||||
}
|
||||
|
||||
export function getDefaultValue(fieldDef, operator) {
|
||||
const descr = getFieldInfo(fieldDef);
|
||||
return descr.defaultValue(fieldDef, operator);
|
||||
}
|
||||
|
||||
export function getDefaultOperator(fieldDef) {
|
||||
const descr = getFieldInfo(fieldDef);
|
||||
return descr.operators[0];
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelector.Editor">
|
||||
<t t-if="props.info.isSupported(props.value)">
|
||||
<t t-component="props.info.component" t-props="props.info.extractProps({ value: props.value, update: props.update, fieldDef: props.fieldDef })" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="o_ds_expr_value o_input d-flex align-items-center p-0">
|
||||
<span class="ms-2" t-esc="stringifiedValue" />
|
||||
<i role="alert" class="ms-auto fa fa-exclamation-triangle text-warning" title="Value not supported" />
|
||||
<button class="btn btn-link p-1" title="Clear" t-on-click="() => this.props.update(props.info.defaultValue())">
|
||||
<i class="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.PathEditor">
|
||||
<t t-if="isSupportedPath">
|
||||
<t t-component="component" isDebugMode="props.isDebugMode" readonly="props.readonly" resModel="props.resModel" path="props.path" update="props.update"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="d-flex align-items-center p-0">
|
||||
<span class="ms-2" t-esc="stringifiedPath" />
|
||||
<i role="alert" class="ms-auto fa fa-exclamation-triangle text-warning" title="Path not supported" />
|
||||
<button class="btn btn-link p-1" title="Clear" t-on-click="onClear">
|
||||
<i class="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.Input">
|
||||
<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">
|
||||
<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">
|
||||
<div class="d-flex flex-wrap">
|
||||
<TagsList tags="tags"/>
|
||||
<input class="o_input o_domain_leaf_value_input flex-grow-1" t-on-change="onChange"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.Range">
|
||||
<div class="d-flex h-100 align-items-center">
|
||||
<Editor info="props.editorInfo" value="getValue(0)" update="getUpdater(0)" fieldDef="props.fieldDef" />
|
||||
<i class="fa fa-long-arrow-right mx-2" aria-label="Arrow icon" title="Arrow" />
|
||||
<Editor info="props.editorInfo" value="getValue(1)" update="getUpdater(1)" fieldDef="props.fieldDef" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,181 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { formatValue, TERM_OPERATORS_NEGATION, toValue } from "@web/core/domain_tree";
|
||||
import { parseExpr } from "@web/core/py_js/py";
|
||||
import { Select } from "./domain_selector_components";
|
||||
|
||||
const OPERATOR_DESCRIPTIONS = {
|
||||
// valid operators (see TERM_OPERATORS in expression.py)
|
||||
"=": "=",
|
||||
"!=": "!=",
|
||||
"<=": "<=",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
">=": ">=",
|
||||
"=?": "=?",
|
||||
"=like": _t("=like"),
|
||||
"=ilike": _t("=ilike"),
|
||||
like: _t("like"),
|
||||
"not like": _t("not like"),
|
||||
ilike: _t("contains"),
|
||||
"not ilike": _t("does not contain"),
|
||||
in: _t("is in"),
|
||||
"not in": _t("is not in"),
|
||||
child_of: _t("child of"),
|
||||
parent_of: _t("parent of"),
|
||||
|
||||
// virtual operators (replace = and != in some cases)
|
||||
is: _t("is"),
|
||||
is_not: _t("is not"),
|
||||
set: _t("is set"),
|
||||
not_set: _t("is not set"),
|
||||
|
||||
// virtual operator (equivalent to a couple (>=,<=))
|
||||
between: _t("is between"),
|
||||
};
|
||||
|
||||
function getDisplayedOperators(fieldDef) {
|
||||
const { type, is_property } = fieldDef;
|
||||
|
||||
if (is_property) {
|
||||
switch (type) {
|
||||
case "many2many":
|
||||
case "tags":
|
||||
return ["in", "not in", "set", "not_set"];
|
||||
case "many2one":
|
||||
case "selection":
|
||||
return ["=", "!=", "set", "not_set"];
|
||||
}
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "boolean":
|
||||
return ["is", "is_not"];
|
||||
case "selection":
|
||||
return ["=", "!=", "in", "not in", "set", "not_set"];
|
||||
case "char":
|
||||
case "text":
|
||||
case "html":
|
||||
return ["=", "!=", "ilike", "not ilike", "in", "not in", "set", "not_set"];
|
||||
case "date":
|
||||
case "datetime":
|
||||
return ["=", "!=", ">", ">=", "<", "<=", "between", "set", "not_set"];
|
||||
case "integer":
|
||||
case "float":
|
||||
case "monetary":
|
||||
return [
|
||||
"=",
|
||||
"!=",
|
||||
">",
|
||||
">=",
|
||||
"<",
|
||||
"<=",
|
||||
"between",
|
||||
"ilike",
|
||||
"not ilike",
|
||||
"set",
|
||||
"not_set",
|
||||
];
|
||||
case "many2one":
|
||||
case "many2many":
|
||||
case "one2many":
|
||||
return ["in", "not in", "=", "!=", "ilike", "not ilike", "set", "not_set"];
|
||||
case "json":
|
||||
return ["=", "!=", "ilike", "not ilike", "set", "not_set"];
|
||||
case "properties":
|
||||
return ["set", "not_set"];
|
||||
case undefined:
|
||||
return ["="];
|
||||
default:
|
||||
return [
|
||||
"=",
|
||||
"!=",
|
||||
">",
|
||||
">=",
|
||||
"<",
|
||||
"<=",
|
||||
"ilike",
|
||||
"not ilike",
|
||||
"like",
|
||||
"not like",
|
||||
"=like",
|
||||
"=ilike",
|
||||
"child_of",
|
||||
"parent_of",
|
||||
"in",
|
||||
"not in",
|
||||
"set",
|
||||
"not_set",
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function toKey(operator, negate = false) {
|
||||
if (!negate && typeof operator === "string" && operator in OPERATOR_DESCRIPTIONS) {
|
||||
// this case is the main one. We keep it simple
|
||||
return operator;
|
||||
}
|
||||
return JSON.stringify([formatValue(operator), negate]);
|
||||
}
|
||||
|
||||
function toOperator(key) {
|
||||
if (!key.includes("[")) {
|
||||
return [key, false];
|
||||
}
|
||||
const [expr, negate] = JSON.parse(key);
|
||||
return [toValue(parseExpr(expr)), negate];
|
||||
}
|
||||
|
||||
export function getOperatorLabel(operator, negate = false) {
|
||||
let label;
|
||||
if (typeof operator === "string" && operator in OPERATOR_DESCRIPTIONS) {
|
||||
if (negate && operator in TERM_OPERATORS_NEGATION) {
|
||||
return OPERATOR_DESCRIPTIONS[TERM_OPERATORS_NEGATION[operator]];
|
||||
}
|
||||
label = OPERATOR_DESCRIPTIONS[operator];
|
||||
} else {
|
||||
label = formatValue(operator);
|
||||
}
|
||||
if (negate) {
|
||||
return _t(`not %s`, label);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
function getOperatorInfo(operator, negate = false) {
|
||||
const key = toKey(operator, negate);
|
||||
const label = getOperatorLabel(operator, negate);
|
||||
return [key, label];
|
||||
}
|
||||
|
||||
export function getOperatorEditorInfo(fieldDef) {
|
||||
const operators = getDisplayedOperators(fieldDef || {});
|
||||
const defaultOperator = operators[0];
|
||||
const operatorsInfo = operators.map((operator) => getOperatorInfo(operator));
|
||||
return {
|
||||
component: Select,
|
||||
extractProps: ({ update, value: [operator, negate] }) => {
|
||||
const [operatorKey, operatorLabel] = getOperatorInfo(operator, negate);
|
||||
const options = [...operatorsInfo];
|
||||
if (!options.some(([key]) => key === operatorKey)) {
|
||||
options.push([operatorKey, operatorLabel]);
|
||||
}
|
||||
return {
|
||||
value: operatorKey,
|
||||
update: (operatorKey) => update(...toOperator(operatorKey)),
|
||||
options,
|
||||
};
|
||||
},
|
||||
defaultValue: () => defaultOperator,
|
||||
isSupported: ([operator]) =>
|
||||
typeof operator === "string" && operator in OPERATOR_DESCRIPTIONS, // should depend on fieldDef too... (e.g. parent_id does not always make sense)
|
||||
message: _t("Operator not supported"),
|
||||
stringify: ([operator, negate]) => getOperatorLabel(operator, negate),
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefaultOperator(fieldDef) {
|
||||
const { defaultValue } = getOperatorEditorInfo(fieldDef);
|
||||
return defaultValue();
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { formatValue, toValue } from "@web/core/domain_tree";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import { parseExpr } from "@web/core/py_js/py";
|
||||
|
||||
/**
|
||||
* @typedef {Object} OperatorInfo
|
||||
* @property {import("@web/core/domain_tree").Value} operator
|
||||
* @property {string} label
|
||||
* @property {number|"variable"} valueCount
|
||||
*/
|
||||
|
||||
export const OPERATOR_DESCRIPTIONS = {
|
||||
// valid operators (see TERM_OPERATORS in expression.py)
|
||||
"=": { label: "=", valueCount: 1 },
|
||||
"!=": { label: "!=", valueCount: 1 },
|
||||
"<=": { label: "<=", valueCount: 1 },
|
||||
"<": { label: "<", valueCount: 1 },
|
||||
">": { label: ">", valueCount: 1 },
|
||||
">=": { label: ">=", valueCount: 1 },
|
||||
"=?": { label: "=?", valueCount: 1 },
|
||||
"=like": { label: _t("=like"), valueCount: 1 },
|
||||
"=ilike": { label: _t("=ilike"), valueCount: 1 },
|
||||
like: { label: _t("like"), valueCount: 1 },
|
||||
"not like": { label: _t("not like"), valueCount: 1 },
|
||||
ilike: { label: _t("contains"), valueCount: 1 },
|
||||
"not ilike": { label: _t("does not contain"), valueCount: 1 },
|
||||
in: { label: _t("is in"), valueCount: "variable" },
|
||||
"not in": { label: _t("is not in"), valueCount: "variable" },
|
||||
child_of: { label: _t("child of"), valueCount: 1 },
|
||||
parent_of: { label: _t("parent of"), valueCount: 1 },
|
||||
|
||||
// virtual operators (replace = and != in some cases)
|
||||
is: { label: _t("is"), valueCount: 1 },
|
||||
is_not: { label: _t("is not"), valueCount: 1 },
|
||||
set: { label: _t("is set"), valueCount: 0 },
|
||||
not_set: { label: _t("is not set"), valueCount: 0 },
|
||||
|
||||
// virtual operator (equivalent to a couple (>=,<=))
|
||||
between: { label: _t("is between"), valueCount: 2 },
|
||||
};
|
||||
|
||||
function toKey(operator, negate = false) {
|
||||
if (!negate && typeof operator === "string" && operator in OPERATOR_DESCRIPTIONS) {
|
||||
// this case is the main one. We keep it simple
|
||||
return operator;
|
||||
}
|
||||
return JSON.stringify([formatValue(operator), negate]);
|
||||
}
|
||||
|
||||
export function toOperator(key) {
|
||||
if (!key.includes("[")) {
|
||||
return [key, false];
|
||||
}
|
||||
const [expr, negate] = JSON.parse(key);
|
||||
return [toValue(parseExpr(expr)), negate];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("@web/core/domain_tree").Value} operator
|
||||
* @param {boolean} [negate=false]
|
||||
* @returns {OperatorInfo}
|
||||
*/
|
||||
export function getOperatorInfo(operator, negate = false) {
|
||||
let operatorInfo;
|
||||
const key = toKey(operator, negate);
|
||||
if (typeof operator === "string" && operator in OPERATOR_DESCRIPTIONS) {
|
||||
const { label, valueCount } = OPERATOR_DESCRIPTIONS[operator];
|
||||
operatorInfo = {
|
||||
key,
|
||||
label: label.toString(),
|
||||
operator,
|
||||
negate,
|
||||
valueCount,
|
||||
};
|
||||
} else {
|
||||
operatorInfo = {
|
||||
key,
|
||||
label: formatValue(operator),
|
||||
operator,
|
||||
negate,
|
||||
valueCount: 0,
|
||||
};
|
||||
}
|
||||
if (negate) {
|
||||
operatorInfo.label = sprintf(`not %s`, operatorInfo.label);
|
||||
}
|
||||
return operatorInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("@web/core/domain_tree").Value[]} operators
|
||||
* @returns {(OperatorInfo)[]}
|
||||
*/
|
||||
export function selectOperators(operators) {
|
||||
return operators.map((operator) => getOperatorInfo(operator));
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { formatValue } from "@web/core/domain_tree";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector";
|
||||
|
||||
export function getPathEditorInfo(params) {
|
||||
return {
|
||||
component: ModelFieldSelector,
|
||||
extractProps: ({ update, value: path }) => {
|
||||
const { resModel, isDebugMode } = params;
|
||||
return {
|
||||
path,
|
||||
update,
|
||||
resModel,
|
||||
isDebugMode,
|
||||
readonly: false,
|
||||
};
|
||||
},
|
||||
isSupported: (path) => [0, 1].includes(path) || typeof path === "string",
|
||||
defaultValue: () => params.defaultPath,
|
||||
stringify: (path) => formatValue(path),
|
||||
message: _t("Invalid field chain"),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
deserializeDate,
|
||||
deserializeDateTime,
|
||||
serializeDate,
|
||||
serializeDateTime,
|
||||
} from "@web/core/l10n/dates";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { DateTimeInput } from "@web/core/datetime/datetime_input";
|
||||
import {
|
||||
DomainSelectorAutocomplete,
|
||||
DomainSelectorSingleAutocomplete,
|
||||
} from "./domain_selector_autocomplete";
|
||||
import { unique } from "@web/core/utils/arrays";
|
||||
import { Input, Select, List, Range } from "./domain_selector_components";
|
||||
import { formatValue } from "@web/core/domain_tree";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
const formatters = registry.category("formatters");
|
||||
const parsers = registry.category("parsers");
|
||||
|
||||
function parseValue(fieldType, value) {
|
||||
const parser = parsers.get(fieldType, (value) => value);
|
||||
try {
|
||||
return parser(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function isParsable(fieldType, value) {
|
||||
const parser = parsers.get(fieldType, (value) => value);
|
||||
try {
|
||||
parser(value);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function genericSerializeDate(type, value) {
|
||||
return type === "date" ? serializeDate(value) : serializeDateTime(value);
|
||||
}
|
||||
|
||||
function genericDeserializeDate(type, value) {
|
||||
return type === "date" ? deserializeDate(value) : deserializeDateTime(value);
|
||||
}
|
||||
|
||||
function isId(value) {
|
||||
return Number.isInteger(value) && value >= 1;
|
||||
}
|
||||
|
||||
export function disambiguate(value, displayNames) {
|
||||
if (!Array.isArray(value)) {
|
||||
return value === "";
|
||||
}
|
||||
let hasSomeString = false;
|
||||
let hasSomethingElse = false;
|
||||
for (const val of value) {
|
||||
if (val === "") {
|
||||
return true;
|
||||
}
|
||||
if (typeof val === "string" || (displayNames && isId(val))) {
|
||||
hasSomeString = true;
|
||||
} else {
|
||||
hasSomethingElse = true;
|
||||
}
|
||||
}
|
||||
return hasSomeString && hasSomethingElse;
|
||||
}
|
||||
|
||||
const STRING_EDITOR = {
|
||||
component: Input,
|
||||
extractProps: ({ value, update }) => ({ value, update }),
|
||||
isSupported: (value) => typeof value === "string",
|
||||
defaultValue: () => "",
|
||||
};
|
||||
|
||||
function makeSelectEditor(options, params = {}) {
|
||||
const getOption = (value) => options.find(([v]) => v === value) || null;
|
||||
return {
|
||||
component: Select,
|
||||
extractProps: ({ value, update }) => ({
|
||||
value,
|
||||
update,
|
||||
options,
|
||||
addBlankOption: params.addBlankOption,
|
||||
}),
|
||||
isSupported: (value) => Boolean(getOption(value)),
|
||||
defaultValue: () => options[0]?.[0] ?? false,
|
||||
stringify: (value, disambiguate) => {
|
||||
const option = getOption(value);
|
||||
return option ? option[1] : disambiguate ? formatValue(value) : String(value);
|
||||
},
|
||||
message: _t("Value not in selection"),
|
||||
};
|
||||
}
|
||||
|
||||
function makeAutoCompleteEditor(fieldDef) {
|
||||
return {
|
||||
component: DomainSelectorAutocomplete,
|
||||
extractProps: ({ value, update }) => {
|
||||
return {
|
||||
resModel: fieldDef.is_property ? fieldDef.comodel : fieldDef.relation,
|
||||
fieldString: fieldDef.string,
|
||||
update: (value) => update(unique(value)),
|
||||
resIds: unique(value),
|
||||
};
|
||||
},
|
||||
isSupported: (value) => Array.isArray(value),
|
||||
defaultValue: () => [],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
function getPartialValueEditorInfo(fieldDef, operator, params = {}) {
|
||||
switch (operator) {
|
||||
case "set":
|
||||
case "not_set":
|
||||
return {
|
||||
component: null,
|
||||
extractProps: null,
|
||||
isSupported: (value) => value === false,
|
||||
defaultValue: () => false,
|
||||
};
|
||||
case "=like":
|
||||
case "=ilike":
|
||||
case "like":
|
||||
case "not like":
|
||||
case "ilike":
|
||||
case "not ilike":
|
||||
return STRING_EDITOR;
|
||||
case "between": {
|
||||
const editorInfo = getValueEditorInfo(fieldDef, "=");
|
||||
return {
|
||||
component: Range,
|
||||
extractProps: ({ value, update }) => ({
|
||||
value,
|
||||
update,
|
||||
editorInfo,
|
||||
}),
|
||||
isSupported: (value) => Array.isArray(value) && value.length === 2,
|
||||
defaultValue: () => {
|
||||
const { defaultValue } = editorInfo;
|
||||
return [defaultValue(), defaultValue()];
|
||||
},
|
||||
};
|
||||
}
|
||||
case "in":
|
||||
case "not in": {
|
||||
switch (fieldDef.type) {
|
||||
case "tags":
|
||||
return STRING_EDITOR;
|
||||
case "many2one":
|
||||
case "many2many":
|
||||
case "one2many":
|
||||
return makeAutoCompleteEditor(fieldDef);
|
||||
default: {
|
||||
const editorInfo = getValueEditorInfo(fieldDef, "=", {
|
||||
addBlankOption: true,
|
||||
startEmpty: true,
|
||||
});
|
||||
return {
|
||||
component: List,
|
||||
extractProps: ({ value, update }) => {
|
||||
if (!disambiguate(value)) {
|
||||
const { stringify } = editorInfo;
|
||||
editorInfo.stringify = (val) => stringify(val, false);
|
||||
}
|
||||
return {
|
||||
value,
|
||||
update,
|
||||
editorInfo,
|
||||
};
|
||||
},
|
||||
isSupported: (value) => Array.isArray(value),
|
||||
defaultValue: () => [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { type } = fieldDef;
|
||||
switch (type) {
|
||||
case "integer":
|
||||
case "float":
|
||||
case "monetary": {
|
||||
const formatType = type === "integer" ? "integer" : "float";
|
||||
return {
|
||||
component: Input,
|
||||
extractProps: ({ value, update }) => ({
|
||||
value: String(value),
|
||||
update: (value) => update(parseValue(formatType, value)),
|
||||
startEmpty: params.startEmpty,
|
||||
}),
|
||||
isSupported: () => true,
|
||||
defaultValue: () => 1,
|
||||
shouldResetValue: (value) => parseValue(formatType, value) === value,
|
||||
};
|
||||
}
|
||||
case "date":
|
||||
case "datetime":
|
||||
return {
|
||||
component: DateTimeInput,
|
||||
extractProps: ({ value, update }) => ({
|
||||
value:
|
||||
params.startEmpty || value === false
|
||||
? false
|
||||
: genericDeserializeDate(type, value),
|
||||
type,
|
||||
onApply: (value) => {
|
||||
if (!params.startEmpty || value) {
|
||||
update(genericSerializeDate(type, value || DateTime.local()));
|
||||
}
|
||||
},
|
||||
}),
|
||||
isSupported: (value) =>
|
||||
value === false || (typeof value === "string" && isParsable(type, value)),
|
||||
defaultValue: () => genericSerializeDate(type, DateTime.local()),
|
||||
stringify: (value) => {
|
||||
if (value === false) {
|
||||
return _t("False");
|
||||
}
|
||||
if (typeof value === "string" && isParsable(type, value)) {
|
||||
const formatter = formatters.get(type, formatValue);
|
||||
return formatter(genericDeserializeDate(type, value));
|
||||
}
|
||||
return formatValue(value);
|
||||
},
|
||||
message: _t("Not a valid %s", type),
|
||||
};
|
||||
case "char":
|
||||
case "html":
|
||||
case "text":
|
||||
return STRING_EDITOR;
|
||||
case "boolean": {
|
||||
if (["is", "is_not"].includes(operator)) {
|
||||
const options = [
|
||||
[true, _t("set")],
|
||||
[false, _t("not set")],
|
||||
];
|
||||
return makeSelectEditor(options, params);
|
||||
}
|
||||
const options = [
|
||||
[true, _t("True")],
|
||||
[false, _t("False")],
|
||||
];
|
||||
return makeSelectEditor(options, params);
|
||||
}
|
||||
case "many2one": {
|
||||
if (["=", "!=", "parent_of", "child_of"].includes(operator)) {
|
||||
return {
|
||||
component: DomainSelectorSingleAutocomplete,
|
||||
extractProps: ({ value, update }) => {
|
||||
return {
|
||||
resModel: fieldDef.is_property ? fieldDef.comodel : fieldDef.relation,
|
||||
fieldString: fieldDef.string,
|
||||
update,
|
||||
resId: value,
|
||||
};
|
||||
},
|
||||
isSupported: () => true,
|
||||
defaultValue: () => false,
|
||||
shouldResetValue: (value) => value !== false && !isId(value),
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "many2many":
|
||||
case "one2many":
|
||||
if (["=", "!="].includes(operator)) {
|
||||
return makeAutoCompleteEditor(fieldDef);
|
||||
}
|
||||
break;
|
||||
case "selection": {
|
||||
const options = fieldDef.selection || [];
|
||||
return makeSelectEditor(options, params);
|
||||
}
|
||||
}
|
||||
|
||||
// Global default for visualization mainly. It is there to visualize what
|
||||
// has been produced in the debug textarea (in o_domain_selector_debug_container)
|
||||
// It is hardly useful to produce a string in general.
|
||||
return {
|
||||
component: Input,
|
||||
extractProps: ({ value, update }) => ({
|
||||
value: String(value),
|
||||
update,
|
||||
}),
|
||||
isSupported: () => true,
|
||||
defaultValue: () => "",
|
||||
};
|
||||
}
|
||||
|
||||
export function getValueEditorInfo(fieldDef, operator, options = {}) {
|
||||
const info = getPartialValueEditorInfo(fieldDef || {}, operator, options);
|
||||
return {
|
||||
extractProps: ({ value, update }) => ({ value, update }),
|
||||
message: _t("Value not supported"),
|
||||
stringify: (val, disambiguate = true) => {
|
||||
if (disambiguate) {
|
||||
return formatValue(val);
|
||||
}
|
||||
return String(val);
|
||||
},
|
||||
...info,
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefaultValue(fieldDef, operator, value = null) {
|
||||
const { isSupported, shouldResetValue, defaultValue } = getValueEditorInfo(fieldDef, operator);
|
||||
if (value === null || !isSupported(value) || shouldResetValue?.(value)) {
|
||||
return defaultValue();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { getDefaultValue, getDefaultOperator } from "./domain_selector_fields";
|
||||
import { disambiguate, getDefaultValue } from "./domain_selector_value_editors";
|
||||
import { getDefaultOperator, getOperatorLabel } from "./domain_selector_operator_editor";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { unique, zip } from "@web/core/utils/arrays";
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
formatValue as toSring,
|
||||
} from "@web/core/domain_tree";
|
||||
import { useLoadFieldInfo, useLoadPathDescription } from "@web/core/model_field_selector/utils";
|
||||
import { getOperatorInfo } from "@web/core/domain_selector/domain_selector_operators";
|
||||
|
||||
/**
|
||||
* @param {import("@web/core/domain_tree").Tree} tree
|
||||
@@ -28,7 +28,10 @@ export function createVirtualOperators(tree, getFieldDef) {
|
||||
const fieldDef = getFieldDef(path);
|
||||
if (fieldDef?.type === "boolean") {
|
||||
return { ...tree, operator: operator === "=" ? "is" : "is_not" };
|
||||
} else if (fieldDef?.type !== "many2one" && value === false) {
|
||||
} else if (
|
||||
!["many2one", "date", "datetime"].includes(fieldDef?.type) &&
|
||||
value === false
|
||||
) {
|
||||
return { ...tree, operator: operator === "=" ? "not_set" : "set" };
|
||||
}
|
||||
}
|
||||
@@ -138,7 +141,7 @@ export function extractIdsFromDomain(domain, getFieldDef) {
|
||||
const value = toValue(node.value[2]);
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
const ids = values.filter((val) => isId(val));
|
||||
const resModel = fieldDef.relation;
|
||||
const resModel = fieldDef.is_property ? fieldDef.comodel : fieldDef.relation;
|
||||
if (ids.length) {
|
||||
if (!idsByModel[resModel]) {
|
||||
idsByModel[resModel] = [];
|
||||
@@ -186,7 +189,7 @@ function formatValue(val, disambiguate, fieldDef, displayNames) {
|
||||
}
|
||||
}
|
||||
if (fieldDef?.type === "selection") {
|
||||
const [, label] = (fieldDef.selection || []).find(([v]) => v === val);
|
||||
const [, label] = (fieldDef.selection || []).find(([v]) => v === val) || [];
|
||||
if (label !== undefined) {
|
||||
val = label;
|
||||
}
|
||||
@@ -197,35 +200,19 @@ function formatValue(val, disambiguate, fieldDef, displayNames) {
|
||||
return val;
|
||||
}
|
||||
|
||||
function disambiguate(value, displayNames) {
|
||||
if (!Array.isArray(value)) {
|
||||
return value === "";
|
||||
}
|
||||
let hasSomeString = false;
|
||||
let hasSomethingElse = false;
|
||||
for (const val of value) {
|
||||
if (val === "") {
|
||||
return true;
|
||||
}
|
||||
if (typeof val === "string" || (displayNames && isId(val))) {
|
||||
hasSomeString = true;
|
||||
} else {
|
||||
hasSomethingElse = true;
|
||||
}
|
||||
}
|
||||
return hasSomeString && hasSomethingElse;
|
||||
}
|
||||
export function leafToString(tree, fieldDef, displayNames) {
|
||||
const { operator, negate, value } = tree;
|
||||
const operatorLabel = getOperatorLabel(operator, negate);
|
||||
|
||||
export function leafToString(fieldDef, operatorInfo, value, displayNames) {
|
||||
const description = {
|
||||
operatorDescription: `${operatorInfo.label}`,
|
||||
operatorDescription: `${operatorLabel}`,
|
||||
valueDescription: null,
|
||||
};
|
||||
|
||||
if (["set", "not_set"].includes(operatorInfo.operator)) {
|
||||
if (["set", "not_set"].includes(operator)) {
|
||||
return description;
|
||||
}
|
||||
if (["is", "is_not"].includes(operatorInfo.operator)) {
|
||||
if (["is", "is_not"].includes(operator)) {
|
||||
description.valueDescription = {
|
||||
values: [value ? _t("set") : _t("not set")],
|
||||
join: "",
|
||||
@@ -240,7 +227,7 @@ export function leafToString(fieldDef, operatorInfo, value, displayNames) {
|
||||
);
|
||||
let join;
|
||||
let addParenthesis = Array.isArray(value);
|
||||
switch (operatorInfo.operator) {
|
||||
switch (operator) {
|
||||
case "between":
|
||||
join = _t("and");
|
||||
addParenthesis = false;
|
||||
@@ -368,14 +355,12 @@ export function getDomainTreeDescription(
|
||||
}
|
||||
return description;
|
||||
}
|
||||
const { negate, operator, path, value } = tree;
|
||||
const { path } = tree;
|
||||
const fieldDef = getFieldDef(path);
|
||||
const operatorInfo = getOperatorInfo(operator, negate);
|
||||
const { operatorDescription, valueDescription } = leafToString(
|
||||
tree,
|
||||
fieldDef,
|
||||
operatorInfo,
|
||||
value,
|
||||
displayNames[fieldDef?.relation]
|
||||
displayNames[fieldDef?.relation || fieldDef?.comodel]
|
||||
);
|
||||
let description = `${getDescription(path)} ${operatorDescription} `;
|
||||
if (valueDescription) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { formatAST, toPyValue } from "@web/core/py_js/py_utils";
|
||||
|
||||
const TERM_OPERATORS_NEGATION = {
|
||||
export const TERM_OPERATORS_NEGATION = {
|
||||
"<": ">=",
|
||||
">": "<=",
|
||||
"<=": ">",
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web._ModelFieldSelector">
|
||||
<div class="o_model_field_selector flex d-flex" aria-atomic="true" t-att-class="props.readonly ? 'o_read_mode' : 'o_edit_mode o_input'" t-on-click="(ev) => this.openPopover(ev.currentTarget)">
|
||||
<div class="o_model_field_selector d-flex" aria-atomic="true" t-att-class="props.readonly ? 'o_read_mode' : 'o_edit_mode o_input'" t-on-click="(ev) => this.openPopover(ev.currentTarget)">
|
||||
<div class="o_model_field_selector_value flex-grow-1 h-100" tabindex="0" t-att-data-tooltip="state.displayNames.join(' > ')" data-tooltip-position="top">
|
||||
<t t-foreach="state.displayNames" t-as="displayName" t-key="displayName_index">
|
||||
<t t-if="!displayName_first">
|
||||
<i class="oi oi-chevron-right m-1" role="img" aria-label="Followed by" title="Followed by" />
|
||||
</t>
|
||||
<span class="o_model_field_selector_chain_part px-1 mb-1">
|
||||
<span t-attf-class="o_model_field_selector_chain_part mb-1 #{props.readonly ? 'border-0 fw-bolder' : 'px-1'} text-nowrap">
|
||||
<t t-esc="displayName" />
|
||||
</span>
|
||||
</t>
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.MultiRecordSelector" >
|
||||
<div class="o_input d-flex flex-wrap h-100 gap-1 o_multi_record_selector">
|
||||
<div class="o_input d-flex flex-wrap gap-1 o_multi_record_selector">
|
||||
<TagsList tags="tags"/>
|
||||
<RecordAutocomplete
|
||||
resModel="props.resModel"
|
||||
value="''"
|
||||
domain="props.domain"
|
||||
context="props.context"
|
||||
className="'o_input o_domain_leaf_value_input flex-grow-1'"
|
||||
className="'o_input flex-grow-1'"
|
||||
fieldString="props.fieldString"
|
||||
multiSelect="true"
|
||||
getIds.bind="getIds"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
value="displayName"
|
||||
domain="props.domain"
|
||||
context="props.context"
|
||||
className="'o_input o_domain_leaf_value_input h-100 o_record_selector'"
|
||||
className="'o_input h-100 o_record_selector'"
|
||||
fieldString="props.fieldString"
|
||||
multiSelect="false"
|
||||
getIds="() => []"
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="o_facet_values position-relative d-flex align-items-center ps-2 rounded-end-2">
|
||||
<div class="o_facet_values position-relative d-flex align-items-center ps-2 rounded-end-2 text-wrap">
|
||||
<t t-foreach="facet.values" t-as="facetValue" t-key="facetValue_index">
|
||||
<em t-if="!facetValue_first" class="o_facet_values_sep small fw-bold mx-1 opacity-50" t-esc="facet.separator"/>
|
||||
<small class="o_facet_value" t-esc="facetValue"/>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="o_searchview_facet_label rounded-start-2 btn btn-primary rounded-end-0 p-0" role="button">
|
||||
<i class="px-1 small fa fa-filter" role="image"/>
|
||||
</div>
|
||||
<div class="o_facet_values d-flex align-items-center px-2 bg-200 rounded-end-2">
|
||||
<div class="o_facet_values d-flex align-items-center px-2 bg-200 rounded-end-2 text-wrap">
|
||||
<small class="o_facet_value" t-esc="facet"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.o_domain_debug_container {
|
||||
.o_domain_selector_debug_container {
|
||||
width: 100%;
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@ function getDisplayedFieldNames(target) {
|
||||
);
|
||||
}
|
||||
|
||||
function getModelFieldSelectorValues(target) {
|
||||
export function getModelFieldSelectorValues(target) {
|
||||
return getNodesTextContent(target.querySelectorAll("span.o_model_field_selector_chain_part"));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
editSelect,
|
||||
getFixture,
|
||||
getNodesTextContent,
|
||||
patchDate,
|
||||
@@ -31,6 +30,7 @@ import { registry } from "@web/core/registry";
|
||||
import { Component, onWillUpdateProps, xml } from "@odoo/owl";
|
||||
import { createWebClient, doAction } from "../webclient/helpers";
|
||||
import { openModelFieldSelectorPopover } from "@web/../tests/core/model_field_selector_tests";
|
||||
import * as dsHelpers from "@web/../tests/core/domain_selector_tests";
|
||||
|
||||
function getDomain(searchable) {
|
||||
return searchable.env.searchModel.domain;
|
||||
@@ -1536,7 +1536,7 @@ QUnit.module("Search", (hooks) => {
|
||||
"Add Custom Filter"
|
||||
);
|
||||
assert.containsOnce(target, ".modal .o_domain_selector");
|
||||
assert.containsOnce(target, ".modal .o_domain_selector .o_domain_leaf");
|
||||
assert.containsOnce(target, ".modal .o_domain_selector .o_domain_selector_condition");
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".modal footer button")), [
|
||||
"Add",
|
||||
"Cancel",
|
||||
@@ -1554,13 +1554,15 @@ QUnit.module("Search", (hooks) => {
|
||||
});
|
||||
await toggleSearchBarMenu(target);
|
||||
await openAddCustomFilterDialog(target);
|
||||
assert.containsOnce(target, ".modal .o_domain_selector .o_domain_leaf");
|
||||
assert.containsOnce(target, ".o_domain_leaf .o_model_field_selector_chain_part");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf .o_model_field_selector_chain_part")
|
||||
.innerText,
|
||||
"ID"
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".modal .o_domain_selector .o_domain_selector_condition"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_domain_selector_condition .o_model_field_selector_chain_part"
|
||||
);
|
||||
assert.strictEqual(dsHelpers.getCurrentPath(target), "ID");
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1580,13 +1582,15 @@ QUnit.module("Search", (hooks) => {
|
||||
});
|
||||
await toggleSearchBarMenu(target);
|
||||
await openAddCustomFilterDialog(target);
|
||||
assert.containsOnce(target, ".modal .o_domain_selector .o_domain_leaf");
|
||||
assert.containsOnce(target, ".o_domain_leaf .o_model_field_selector_chain_part");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf .o_model_field_selector_chain_part")
|
||||
.innerText,
|
||||
"Country"
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".modal .o_domain_selector .o_domain_selector_condition"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_domain_selector_condition .o_model_field_selector_chain_part"
|
||||
);
|
||||
assert.strictEqual(dsHelpers.getCurrentPath(target), "Country");
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1599,21 +1603,21 @@ QUnit.module("Search", (hooks) => {
|
||||
});
|
||||
await toggleSearchBarMenu(target);
|
||||
await openAddCustomFilterDialog(target);
|
||||
assert.containsOnce(target, ".modal .o_domain_selector .o_domain_leaf");
|
||||
assert.containsOnce(target, ".o_domain_leaf .o_model_field_selector_chain_part");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf .o_model_field_selector_chain_part").innerText,
|
||||
"ID"
|
||||
assert.containsOnce(target, ".modal .o_domain_selector .o_domain_selector_condition");
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_domain_selector_condition .o_model_field_selector_chain_part"
|
||||
);
|
||||
assert.containsOnce(target, "button.o_domain_tree_connector_caret");
|
||||
assert.strictEqual(dsHelpers.getCurrentPath(target), "ID");
|
||||
assert.containsOnce(target, ".o_domain_selector .o_domain_selector_connector");
|
||||
|
||||
await click(target, ".o_domain_add_node_button .fa-plus");
|
||||
assert.containsOnce(target, "button.o_domain_tree_connector_caret");
|
||||
await dsHelpers.clickOnButtonAddNewRule(target);
|
||||
assert.containsOnce(target, ".o_domain_selector .dropdown-toggle");
|
||||
assert.strictEqual(
|
||||
target.querySelector("button.o_domain_tree_connector_caret").innerText,
|
||||
target.querySelector(".o_domain_selector .dropdown-toggle").innerText,
|
||||
"any"
|
||||
);
|
||||
assert.containsN(target, ".modal .o_domain_selector .o_domain_leaf", 2);
|
||||
assert.containsN(target, ".modal .o_domain_selector .o_domain_selector_condition", 2);
|
||||
});
|
||||
|
||||
QUnit.test("Add a custom filter", async function (assert) {
|
||||
@@ -1642,22 +1646,13 @@ QUnit.module("Search", (hooks) => {
|
||||
|
||||
await toggleSearchBarMenu(target);
|
||||
await openAddCustomFilterDialog(target);
|
||||
await click(target, ".o_domain_add_node_button .fa-plus");
|
||||
await dsHelpers.clickOnButtonAddNewRule(target);
|
||||
|
||||
const connectorEl = target.querySelector(".o_domain_tree_connector_selector");
|
||||
await click(connectorEl.querySelector("button.o_domain_tree_connector_caret"));
|
||||
await click(
|
||||
[
|
||||
...target.querySelectorAll(".o_domain_tree_connector_selector .dropdown-item"),
|
||||
].find((el) => el.innerText === "all")
|
||||
);
|
||||
await click(target.querySelector(".o_domain_selector .dropdown .dropdown-toggle"));
|
||||
await click(target.querySelector(".o_domain_selector .dropdown .dropdown-item"));
|
||||
|
||||
await click(
|
||||
[...target.querySelectorAll(".o_domain_add_node_button .fa-sitemap")].at(-1)
|
||||
);
|
||||
await click(
|
||||
[...target.querySelectorAll(".o_domain_add_node_button .fa-sitemap")].at(-1)
|
||||
);
|
||||
await dsHelpers.clickOnButtonAddBranch(target, -1);
|
||||
await dsHelpers.clickOnButtonAddBranch(target, -1);
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), [
|
||||
@@ -1704,7 +1699,11 @@ QUnit.module("Search", (hooks) => {
|
||||
|
||||
await toggleSearchBarMenu(target);
|
||||
await openAddCustomFilterDialog(target);
|
||||
await editInput(target, ".o_domain_debug_input", `[("foo", "in", [uid, 1, "a"])]`);
|
||||
await editInput(
|
||||
target,
|
||||
dsHelpers.SELECTORS.debugArea,
|
||||
`[("foo", "in", [uid, 1, "a"])]`
|
||||
);
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), [`Foo is in ( uid , 1 , "a" )`]);
|
||||
@@ -1734,7 +1733,7 @@ QUnit.module("Search", (hooks) => {
|
||||
|
||||
await toggleSearchBarMenu(target);
|
||||
await openAddCustomFilterDialog(target);
|
||||
await editInput(target, ".o_domain_debug_input", `[("id", "between", [0, 10])]`);
|
||||
await editInput(target, dsHelpers.SELECTORS.debugArea, `[("id", "between", [0, 10])]`);
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), [`ID is between 0 and 10`]);
|
||||
@@ -1761,11 +1760,11 @@ QUnit.module("Search", (hooks) => {
|
||||
await openAddCustomFilterDialog(target);
|
||||
await editInput(
|
||||
target,
|
||||
".o_domain_debug_input",
|
||||
dsHelpers.SELECTORS.debugArea,
|
||||
`["!", "|", ("foo", "=", 1 ), ("id", "=", 2)]`
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_tree_connector_caret").textContent,
|
||||
target.querySelector(".o_domain_selector_row .dropdown-toggle").textContent,
|
||||
"none"
|
||||
);
|
||||
|
||||
@@ -1797,14 +1796,14 @@ QUnit.module("Search", (hooks) => {
|
||||
|
||||
await toggleSearchBarMenu(target);
|
||||
await openAddCustomFilterDialog(target);
|
||||
await editSelect(target, ".o_domain_leaf_operator_select", "not_set");
|
||||
await dsHelpers.selectOperator(target, "not_set");
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ["ID is not set"]);
|
||||
assert.deepEqual(getDomain(controlPanel), [["id", "=", false]]);
|
||||
|
||||
await click(target, ".o_searchview_facet_label");
|
||||
await editSelect(target, ".o_domain_leaf_operator_select", "set");
|
||||
await dsHelpers.selectOperator(target, "set");
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ["ID is set"]);
|
||||
@@ -1819,21 +1818,21 @@ QUnit.module("Search", (hooks) => {
|
||||
assert.deepEqual(getDomain(controlPanel), [["boolean", "=", true]]);
|
||||
|
||||
await click(target, ".o_searchview_facet_label");
|
||||
await editSelect(target, ".o_domain_leaf_value_input", false);
|
||||
await dsHelpers.selectValue(target, false);
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ["Boolean is not set"]);
|
||||
assert.deepEqual(getDomain(controlPanel), [["boolean", "=", false]]);
|
||||
|
||||
await click(target, ".o_searchview_facet_label");
|
||||
await editSelect(target, ".o_domain_leaf_operator_select", "is_not");
|
||||
await dsHelpers.selectOperator(target, "is_not");
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ["Boolean is not not set"]);
|
||||
assert.deepEqual(getDomain(controlPanel), [["boolean", "!=", false]]);
|
||||
|
||||
await click(target, ".o_searchview_facet_label");
|
||||
await editSelect(target, ".o_domain_leaf_value_input", true);
|
||||
await dsHelpers.selectValue(target, true);
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ["Boolean is not set"]);
|
||||
@@ -1871,7 +1870,7 @@ QUnit.module("Search", (hooks) => {
|
||||
|
||||
await toggleSearchBarMenu(target);
|
||||
await openAddCustomFilterDialog(target);
|
||||
await editInput(target, ".o_domain_debug_input", "[(uid, uid, uid)]");
|
||||
await editInput(target, dsHelpers.SELECTORS.debugArea, "[(uid, uid, uid)]");
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
assert.containsOnce(target, ".modal .o_domain_selector");
|
||||
});
|
||||
@@ -1903,7 +1902,7 @@ QUnit.module("Search", (hooks) => {
|
||||
await openAddCustomFilterDialog(target);
|
||||
await editInput(
|
||||
target,
|
||||
".o_domain_debug_input",
|
||||
dsHelpers.SELECTORS.debugArea,
|
||||
`[("bar", "=", 1 ), ("bar", "in", [2, 5555]), ("bar", "!=", false), ("id", "=", 2)]`
|
||||
);
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
@@ -1925,6 +1924,55 @@ QUnit.module("Search", (hooks) => {
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("display names in facets (with a property)", async function (assert) {
|
||||
patchWithCleanup(odoo, { debug: true });
|
||||
serverData.models.partner = {
|
||||
fields: {},
|
||||
records: [{ id: 1, display_name: "John" }],
|
||||
};
|
||||
|
||||
async function mockRPC(route, { method, model }) {
|
||||
if (route === "/web/domain/validate") {
|
||||
return true;
|
||||
}
|
||||
if (method === "web_search_read" && model === "parentModel") {
|
||||
return {
|
||||
records: [
|
||||
{
|
||||
id: 1337,
|
||||
display_name: "First Parent",
|
||||
properties_definition: [
|
||||
{
|
||||
name: "m2o",
|
||||
type: "many2one",
|
||||
string: "M2O",
|
||||
comodel: "partner",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const controlPanel = await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: SearchBar,
|
||||
searchMenuTypes: ["filter"],
|
||||
searchViewId: false,
|
||||
searchViewArch: `<search />`,
|
||||
mockRPC,
|
||||
});
|
||||
await toggleSearchBarMenu(target);
|
||||
await openAddCustomFilterDialog(target);
|
||||
await editInput(target, dsHelpers.SELECTORS.debugArea, `[("properties.m2o", "=", 1)]`);
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ["Properties 🠒 M2O = John"]);
|
||||
assert.deepEqual(getDomain(controlPanel), [["properties.m2o", "=", 1]]);
|
||||
});
|
||||
|
||||
QUnit.test("group by properties", async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import * as dsHelpers from "@web/../tests/core/domain_selector_tests";
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
@@ -1459,28 +1460,25 @@ QUnit.module("Search", (hooks) => {
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.strictEqual(target.querySelector(".modal header").innerText, "Modify Condition");
|
||||
assert.containsOnce(target, ".modal .o_domain_selector");
|
||||
assert.containsOnce(target, ".o_domain_leaf");
|
||||
assert.containsOnce(target, dsHelpers.SELECTORS.condition);
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".modal footer button")), [
|
||||
"Confirm",
|
||||
"Discard",
|
||||
]);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_model_field_selector_value").innerText,
|
||||
"Birthday"
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, ">=");
|
||||
assert.strictEqual(target.querySelector(".o_ds_expr_value").innerText, "context_today()");
|
||||
assert.strictEqual(dsHelpers.getCurrentPath(target), "Birthday");
|
||||
assert.strictEqual(dsHelpers.getCurrentOperator(target), ">=");
|
||||
assert.strictEqual(dsHelpers.getCurrentValue(target), "context_today()");
|
||||
assert.notOk(target.querySelector(".modal footer button").disabled);
|
||||
|
||||
await click(target, ".o_domain_delete_node_button");
|
||||
assert.containsNone(target, ".o_domain_leaf");
|
||||
await dsHelpers.clickOnButtonDeleteNode(target);
|
||||
assert.containsNone(target, dsHelpers.SELECTORS.condition);
|
||||
assert.ok(target.querySelector(".modal footer button").disabled);
|
||||
|
||||
await click(target, ".modal .o_domain_tree a[role=button]");
|
||||
assert.containsOnce(target, ".o_domain_leaf");
|
||||
assert.strictEqual(target.querySelector(".o_model_field_selector_value").innerText, "ID");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "=");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell .o_input").value, "1");
|
||||
await click(target, `.modal ${dsHelpers.SELECTORS.addNewRule}`);
|
||||
assert.containsOnce(target, dsHelpers.SELECTORS.condition);
|
||||
assert.strictEqual(dsHelpers.getCurrentPath(target), "ID");
|
||||
assert.strictEqual(dsHelpers.getCurrentOperator(target), "=");
|
||||
assert.strictEqual(dsHelpers.getCurrentValue(target), "1");
|
||||
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
assert.containsNone(target, ".modal");
|
||||
@@ -1569,9 +1567,9 @@ QUnit.module("Search", (hooks) => {
|
||||
|
||||
await click(target, ".o_facet_with_domain .o_searchview_facet_label");
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.strictEqual(target.querySelector(".o_model_field_selector_value").innerText, "Foo");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "ilike");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell .o_input").value, "abc");
|
||||
assert.strictEqual(dsHelpers.getCurrentPath(target), "Foo");
|
||||
assert.strictEqual(dsHelpers.getCurrentOperator(target), "contains");
|
||||
assert.strictEqual(dsHelpers.getCurrentValue(target), "abc");
|
||||
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
assert.containsNone(target, ".modal");
|
||||
@@ -1620,19 +1618,12 @@ QUnit.module("Search", (hooks) => {
|
||||
|
||||
await click(target, ".o_facet_with_domain .o_searchview_facet_label");
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsOnce(target, ".o_domain_leaf");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_model_field_selector_value").innerText,
|
||||
"Birthday"
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "between");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_ds_value_cell .o_datetime_input:nth-child(1)").value,
|
||||
"04/01/2023"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_ds_value_cell .o_datetime_input:nth-child(3)").value,
|
||||
"04/30/2023"
|
||||
assert.containsOnce(target, dsHelpers.SELECTORS.condition);
|
||||
assert.strictEqual(dsHelpers.getCurrentPath(target), "Birthday");
|
||||
assert.strictEqual(dsHelpers.getCurrentOperator(target), "is between");
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(`.o_datetime_input`)].map((el) => el.value),
|
||||
["04/01/2023", "04/30/2023"]
|
||||
);
|
||||
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
@@ -1674,37 +1665,15 @@ QUnit.module("Search", (hooks) => {
|
||||
assert.deepEqual(getFacetTexts(target), ["Foo\nabc\nor\ndef"]);
|
||||
|
||||
await click(target, ".o_facet_with_domain .o_searchview_facet_label");
|
||||
assert.containsN(target, ".o_domain_leaf", 2);
|
||||
assert.containsN(target, dsHelpers.SELECTORS.condition, 2);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf:nth-child(1) .o_model_field_selector_value")
|
||||
.innerText,
|
||||
"Foo"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf:nth-child(1) .o_domain_leaf_operator_select")
|
||||
.value,
|
||||
"ilike"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf:nth-child(1) .o_domain_leaf_value_input").value,
|
||||
"abc"
|
||||
);
|
||||
assert.strictEqual(dsHelpers.getCurrentPath(target), "Foo");
|
||||
assert.strictEqual(dsHelpers.getCurrentOperator(target), "contains");
|
||||
assert.strictEqual(dsHelpers.getCurrentValue(target), "abc");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf:nth-child(2) .o_model_field_selector_value")
|
||||
.innerText,
|
||||
"Foo"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf:nth-child(2) .o_domain_leaf_operator_select")
|
||||
.value,
|
||||
"ilike"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_leaf:nth-child(2) .o_domain_leaf_value_input").value,
|
||||
"def"
|
||||
);
|
||||
assert.strictEqual(dsHelpers.getCurrentPath(target, 1), "Foo");
|
||||
assert.strictEqual(dsHelpers.getCurrentOperator(target, 1), "contains");
|
||||
assert.strictEqual(dsHelpers.getCurrentValue(target, 1), "def");
|
||||
|
||||
await click(target.querySelector(".modal footer button"));
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
triggerEvent,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { getPickerCell } from "../../core/datetime/datetime_test_helpers";
|
||||
import { getPickerCell } from "@web/../tests/core/datetime/datetime_test_helpers";
|
||||
import * as dsHelpers from "@web/../tests/core/domain_selector_tests";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
@@ -116,7 +117,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_ds_value_cell").textContent,
|
||||
dsHelpers.getCurrentValue(target),
|
||||
"uid",
|
||||
"The widget should show the dynamic filter."
|
||||
);
|
||||
@@ -140,12 +141,9 @@ QUnit.module("Fields", (hooks) => {
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
target.querySelector(".o_ds_value_cell .o_ds_expr_value").innerText,
|
||||
"context_today()"
|
||||
);
|
||||
assert.equal(dsHelpers.getCurrentValue(target), "context_today()");
|
||||
|
||||
await click(target, ".o_ds_value_cell .fa.fa-times");
|
||||
await dsHelpers.clearNotSupported(target);
|
||||
|
||||
// Change the date in the datepicker
|
||||
await click(target, ".o_datetime_input");
|
||||
@@ -156,10 +154,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
await clickDiscard(target);
|
||||
|
||||
// Open the datepicker again
|
||||
assert.equal(
|
||||
target.querySelector(".o_ds_value_cell .o_ds_expr_value").innerText,
|
||||
"context_today()"
|
||||
);
|
||||
assert.equal(dsHelpers.getCurrentValue(target), "context_today()");
|
||||
}
|
||||
);
|
||||
|
||||
@@ -182,11 +177,11 @@ QUnit.module("Fields", (hooks) => {
|
||||
});
|
||||
|
||||
// As the domain is empty, there should be a button to add a new rule
|
||||
assert.containsOnce(target, ".o_domain_tree a[role=button]");
|
||||
assert.containsOnce(target, dsHelpers.SELECTORS.addNewRule);
|
||||
|
||||
// Clicking on the button should add the [["id", "=", "1"]] domain, so
|
||||
// there should be a field selector in the DOM
|
||||
await click(target, ".o_domain_tree a[role=button]");
|
||||
await dsHelpers.addNewRule(target);
|
||||
assert.containsOnce(target, ".o_model_field_selector", "there should be a field selector");
|
||||
|
||||
// Focusing the field selector input should open the field selector
|
||||
@@ -206,9 +201,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
// associated value should reveal one matched record
|
||||
await click(document.body.querySelector(".o_model_field_selector_popover_item_name"));
|
||||
|
||||
const input = target.querySelector(".o_domain_leaf_value_input");
|
||||
input.value = 2;
|
||||
await triggerEvent(input, null, "change");
|
||||
await dsHelpers.editValue(target, 2);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_show_selection_button").textContent.trim().substr(0, 2),
|
||||
@@ -241,7 +234,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await click(target, ".o_domain_tree a[role=button]");
|
||||
await dsHelpers.addNewRule(target);
|
||||
await click(target, ".o_model_field_selector");
|
||||
await click(
|
||||
document.body.querySelector(
|
||||
@@ -510,7 +503,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
"2 record(s)"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_domain_debug_input", "[['id', '<', 40]]");
|
||||
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['id', '<', 40]]");
|
||||
// the count should not be re-computed when editing with the textarea
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
|
||||
@@ -578,7 +571,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
"2 record(s)"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_domain_debug_input", "[['abc', '=', 1]]");
|
||||
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['abc', '=', 1]]");
|
||||
// the count should not be re-computed when editing with the textarea
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
|
||||
@@ -586,7 +579,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
);
|
||||
assert.verifySteps([]);
|
||||
|
||||
await editInput(target, ".o_domain_debug_input", "[['abc']]");
|
||||
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['abc']]");
|
||||
assert.verifySteps([]);
|
||||
|
||||
await clickSave(target);
|
||||
@@ -650,7 +643,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
"2 record(s)"
|
||||
);
|
||||
|
||||
await editInput(target, ".o_domain_debug_input", "[['id', '<', 40]]");
|
||||
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['id', '<', 40]]");
|
||||
// the count should not be re-computed when editing with the textarea
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
|
||||
@@ -747,11 +740,11 @@ QUnit.module("Fields", (hooks) => {
|
||||
});
|
||||
|
||||
await doAction(webClient, 1);
|
||||
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
|
||||
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
|
||||
|
||||
rawDomain = `[("date", ">=", datetime.datetime.combine(context_today() + relativedelta(days = -1), datetime.time(0, 0, 0)).to_utc().strftime("%Y-%m-%d %H:%M:%S"))]`;
|
||||
await editInput(target, ".o_domain_debug_input", rawDomain);
|
||||
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
|
||||
await editInput(target, dsHelpers.SELECTORS.debugArea, rawDomain);
|
||||
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
|
||||
|
||||
await clickSave(target);
|
||||
});
|
||||
@@ -802,10 +795,9 @@ QUnit.module("Fields", (hooks) => {
|
||||
"fields_get",
|
||||
]);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
|
||||
assert.containsOnce(target, ".o_ds_expr_value", "there should be an expression");
|
||||
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
|
||||
|
||||
await click(target, ".o_ds_expr_value button");
|
||||
await dsHelpers.clearNotSupported(target);
|
||||
rawDomain = `[("date", ">=", "2020-09-05")]`;
|
||||
assert.containsOnce(target, ".o_datetime_input", "there should be a datepicker");
|
||||
assert.verifySteps(["search_count"]);
|
||||
@@ -815,19 +807,19 @@ QUnit.module("Fields", (hooks) => {
|
||||
assert.containsOnce(target, ".o_datetime_picker");
|
||||
await triggerEvent(window, null, "scroll");
|
||||
assert.containsOnce(target, ".o_datetime_picker");
|
||||
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
|
||||
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
|
||||
assert.verifySteps([]);
|
||||
|
||||
// Manually input a date
|
||||
rawDomain = `[("date", ">=", "2020-09-09")]`;
|
||||
await editInput(target, ".o_datetime_input", "09/09/2020");
|
||||
assert.verifySteps(["search_count"]);
|
||||
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
|
||||
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
|
||||
|
||||
// Save
|
||||
await clickSave(target);
|
||||
assert.verifySteps(["web_save", "search_count"]);
|
||||
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, rawDomain);
|
||||
assert.strictEqual(target.querySelector(dsHelpers.SELECTORS.debugArea).value, rawDomain);
|
||||
});
|
||||
|
||||
QUnit.test("domain field without model", async function (assert) {
|
||||
@@ -925,14 +917,14 @@ QUnit.module("Fields", (hooks) => {
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.containsNone(target, ".o_domain_leaf");
|
||||
assert.containsNone(target, dsHelpers.SELECTORS.condition);
|
||||
assert.containsNone(target, ".modal");
|
||||
await click(target, ".o_field_domain_dialog_button");
|
||||
assert.containsOnce(target, ".modal");
|
||||
await click(target, ".modal .o_domain_tree a[role=button]");
|
||||
await click(target, `.modal ${dsHelpers.SELECTORS.addNewRule}`);
|
||||
await click(target, ".modal-footer .btn-primary");
|
||||
assert.containsOnce(target, ".o_domain_leaf");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf").textContent, "ID = 1");
|
||||
assert.containsOnce(target, dsHelpers.SELECTORS.condition);
|
||||
assert.strictEqual(dsHelpers.getConditionText(target), "ID = 1");
|
||||
});
|
||||
|
||||
QUnit.test("invalid value in domain field with 'inDialog' options", async function (assert) {
|
||||
@@ -949,15 +941,15 @@ QUnit.module("Fields", (hooks) => {
|
||||
<field name="display_name" widget="domain" options="{'model': 'partner', 'in_dialog': True}"/>
|
||||
</form>`,
|
||||
});
|
||||
assert.containsNone(target, ".o_domain_leaf");
|
||||
assert.containsNone(target, dsHelpers.SELECTORS.condition);
|
||||
assert.containsNone(target, ".modal");
|
||||
assert.containsNone(target, ".o_field_domain .text-warning");
|
||||
|
||||
await click(target, ".o_field_domain_dialog_button");
|
||||
assert.containsOnce(target, ".modal");
|
||||
|
||||
await click(target, ".modal .o_domain_tree a[role=button]");
|
||||
await editInput(target, ".o_domain_debug_input", "[(0, '=', expr)]");
|
||||
await click(target, `.modal ${dsHelpers.SELECTORS.addNewRule}`);
|
||||
await editInput(target, dsHelpers.SELECTORS.debugArea, "[(0, '=', expr)]");
|
||||
await click(target, ".modal-footer .btn-primary");
|
||||
assert.containsOnce(target, ".modal", "the domain is invalid: the dialog is not closed");
|
||||
});
|
||||
@@ -990,7 +982,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
target.querySelector(".o_domain_show_selection_button").textContent.trim(),
|
||||
"0 record(s)"
|
||||
);
|
||||
await editInput(target, ".o_domain_debug_input", "[['id', '!=', False]]");
|
||||
await editInput(target, dsHelpers.SELECTORS.debugArea, "[['id', '!=', False]]");
|
||||
await click(target, "button.o_form_button_save");
|
||||
assert.verifySteps(["/web/domain/validate"]);
|
||||
assert.strictEqual(
|
||||
@@ -1027,11 +1019,11 @@ QUnit.module("Fields", (hooks) => {
|
||||
await click(target, ".o_field_domain > div > div");
|
||||
|
||||
// There should be a button to add a new rule
|
||||
assert.containsOnce(target, ".o_domain_tree a[role=button]");
|
||||
assert.containsOnce(target, dsHelpers.SELECTORS.addNewRule);
|
||||
|
||||
// Clicking on the button should add the [["id", "=", "1"]] domain, so
|
||||
// there should be a field selector in the DOM
|
||||
await click(target, ".o_domain_tree a[role=button]");
|
||||
await dsHelpers.addNewRule(target);
|
||||
assert.containsOnce(target, ".o_model_field_selector");
|
||||
|
||||
// Focusing the field selector input should open the field selector
|
||||
@@ -1051,7 +1043,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
// associated value should reveal one matched record
|
||||
await click(document.body.querySelector(".o_model_field_selector_popover_item_name"));
|
||||
|
||||
await editInput(target, ".o_domain_leaf_value_input", 2);
|
||||
await dsHelpers.editValue(target, 2);
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_domain_show_selection_button").textContent.trim().substr(0, 2),
|
||||
@@ -1093,7 +1085,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
|
||||
// Unfold the domain and delete the condition
|
||||
await click(target, ".o_field_domain > div > div");
|
||||
await click(target, ".o_domain_delete_node_button");
|
||||
await dsHelpers.clickOnButtonDeleteNode(target);
|
||||
|
||||
// Fold domain selector
|
||||
await click(target, ".o_field_domain a i");
|
||||
@@ -1105,6 +1097,9 @@ QUnit.module("Fields", (hooks) => {
|
||||
await click(target, ".o_domain_add_first_node_button");
|
||||
// Domain is now unfolded with the default condition
|
||||
assert.containsOnce(target, ".o_model_field_selector");
|
||||
assert.strictEqual(target.querySelector(".o_domain_debug_input").value, '[("id", "=", 1)]');
|
||||
assert.strictEqual(
|
||||
target.querySelector(dsHelpers.SELECTORS.debugArea).value,
|
||||
'[("id", "=", 1)]'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user