[IMP] web: domain selector: autocompletion for relational fields
For the relational fields and the operators =,!=,in,not in, we allow the user to use autocompletion as in a many2one or a x2many field. Task ID: 3291990 Part-of: odoo/odoo#121688
This commit is contained in:
@@ -18,13 +18,71 @@ import { getOperatorInfo, toOperator } from "@web/core/domain_selector/domain_se
|
||||
import {
|
||||
Editor,
|
||||
PathEditor,
|
||||
getDefaultFieldValue,
|
||||
getEditorInfo,
|
||||
getDefaultOperator,
|
||||
getDefaultValue,
|
||||
getOperatorsInfo,
|
||||
getEditorInfo,
|
||||
} from "@web/core/domain_selector/domain_selector_fields";
|
||||
import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector";
|
||||
import { useLoadFieldInfo } from "@web/core/model_field_selector/utils";
|
||||
import { Expression } from "@web/core/domain_tree";
|
||||
import { formatValue } from "@web/core/domain_tree";
|
||||
|
||||
function collectDifferences(tree, otherTree) {
|
||||
// some differences shadow the other differences "below":
|
||||
if (tree.type !== otherTree.type) {
|
||||
return [{ type: "other" }];
|
||||
}
|
||||
if (tree.negate !== otherTree.negate) {
|
||||
return [{ type: "other" }];
|
||||
}
|
||||
if (tree.type === "condition") {
|
||||
if (formatValue(tree.path) !== formatValue(otherTree.path)) {
|
||||
return [{ type: "other" }];
|
||||
}
|
||||
if (formatValue(tree.value) !== formatValue(otherTree.value)) {
|
||||
return [{ type: "other" }];
|
||||
}
|
||||
if (formatValue(tree.operator) !== formatValue(otherTree.operator)) {
|
||||
if (tree.operator === "!=" && otherTree.operator === "set") {
|
||||
return [{ type: "replacement", tree, operator: "set" }];
|
||||
} else if (tree.operator === "=" && otherTree.operator === "not_set") {
|
||||
return [{ type: "replacement", tree, operator: "not_set" }];
|
||||
} else {
|
||||
return [{ type: "other" }];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (tree.value !== otherTree.value) {
|
||||
return [{ type: "other" }];
|
||||
}
|
||||
if (tree.children.length !== otherTree.children.length) {
|
||||
return [{ type: "other" }];
|
||||
}
|
||||
const diffs = [];
|
||||
for (let i = 0; i < tree.children.length; i++) {
|
||||
const child = tree.children[i];
|
||||
const otherChild = otherTree.children[i];
|
||||
const childDiffs = collectDifferences(child, otherChild);
|
||||
if (childDiffs.some((d) => d.type !== "replacement")) {
|
||||
return [{ type: "other" }];
|
||||
}
|
||||
diffs.push(...childDiffs);
|
||||
}
|
||||
return diffs;
|
||||
}
|
||||
|
||||
function restoreVirtualOperators(tree, otherTree) {
|
||||
const diffs = collectDifferences(tree, otherTree);
|
||||
// note that the array diffs is homogeneous:
|
||||
// we have diffs of the form [], [other], [repl, ..., repl]
|
||||
if (diffs.some((d) => d.type !== "replacement")) {
|
||||
return;
|
||||
}
|
||||
for (const { tree, operator } of diffs) {
|
||||
tree.operator = operator;
|
||||
}
|
||||
}
|
||||
|
||||
export class DomainSelector extends Component {
|
||||
static template = "web._DomainSelector";
|
||||
@@ -58,6 +116,7 @@ export class DomainSelector extends Component {
|
||||
this.loadDisplayNames = useLoadDisplayNames();
|
||||
this.loadFieldInfo = useLoadFieldInfo();
|
||||
this.tree = null;
|
||||
this.previousTree = null;
|
||||
onWillStart(() => this.onPropsUpdated(this.props));
|
||||
onWillUpdateProps((np) => this.onPropsUpdated(np));
|
||||
}
|
||||
@@ -91,6 +150,7 @@ export class DomainSelector extends Component {
|
||||
|
||||
if (!isSupported) {
|
||||
this.tree = null;
|
||||
this.previousTree = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,9 +176,16 @@ export class DomainSelector extends Component {
|
||||
this.getFieldDef.bind(this),
|
||||
options
|
||||
).children[0];
|
||||
|
||||
if (this.previousTree) {
|
||||
// find "first" difference
|
||||
restoreVirtualOperators(this.tree, this.previousTree);
|
||||
this.previousTree = null;
|
||||
}
|
||||
}
|
||||
|
||||
notifyChanges() {
|
||||
this.previousTree = cloneTree(this.tree);
|
||||
const domain = this.tree ? buildDomain(this.tree) : `[]`;
|
||||
this.props.update(domain);
|
||||
}
|
||||
@@ -133,10 +200,6 @@ export class DomainSelector extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDefaultFieldValue(path, operator) {
|
||||
return getDefaultFieldValue(this.getFieldDef(path), operator);
|
||||
}
|
||||
|
||||
async loadFieldDefs(resModel, paths) {
|
||||
const promises = [];
|
||||
const fieldDefs = {};
|
||||
@@ -216,18 +279,26 @@ export class DomainSelector extends Component {
|
||||
Object.assign(node, this.createNewLeaf());
|
||||
} else {
|
||||
node.path = path;
|
||||
const operatorInfo = getOperatorsInfo(fieldDef)[0];
|
||||
node.operator = operatorInfo.operator;
|
||||
node.value = getDefaultFieldValue(fieldDef, node.operator);
|
||||
node.operator = getDefaultOperator(fieldDef);
|
||||
node.value = getDefaultValue(fieldDef, node.operator);
|
||||
}
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
updateLeafOperator(node, operatorKey) {
|
||||
const [operator, negate] = toOperator(operatorKey);
|
||||
const previousOperatorInfo = getOperatorInfo(node.operator);
|
||||
|
||||
const previousNode = node ? cloneTree(node) : null;
|
||||
|
||||
const [operator, negate] = toOperator(operatorKey);
|
||||
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) {
|
||||
@@ -243,17 +314,28 @@ export class DomainSelector extends Component {
|
||||
}
|
||||
// binary operator with a non array value
|
||||
case 1: {
|
||||
node.value = this.getDefaultFieldValue(node.path, operator);
|
||||
node.value = getDefaultValue(this.getFieldDef(node.path), node.operator);
|
||||
break;
|
||||
}
|
||||
// binary operator with a fixed sized array value
|
||||
default: {
|
||||
const defaultValue = this.getDefaultFieldValue(node.path, operator);
|
||||
const defaultValue = getDefaultValue(
|
||||
this.getFieldDef(node.path),
|
||||
node.operator
|
||||
);
|
||||
node.value = Array(operatorInfo.valueCount).fill(defaultValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buildDomain(node) === buildDomain(previousNode)) {
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -262,14 +344,6 @@ export class DomainSelector extends Component {
|
||||
this.notifyChanges();
|
||||
}
|
||||
|
||||
isExprValue(value) {
|
||||
return value instanceof Expression;
|
||||
}
|
||||
|
||||
removeExprValue(node) {
|
||||
this.updateLeafValue(node, this.getDefaultFieldValue(node.path, node.operator));
|
||||
}
|
||||
|
||||
onDebugValueChange(value) {
|
||||
return this.props.update(value, true);
|
||||
}
|
||||
|
||||
@@ -168,9 +168,9 @@
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<t t-if="getOperatorInfo(node).valueCount">
|
||||
<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)" onExprRemove="() => this.removeExprValue(node)" />
|
||||
<Editor info="getEditorInfo(node)" value="node.value" update="(value) => this.updateLeafValue(node, value)" fieldDef="getFieldDef(node.path)"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, onWillStart, onWillUpdateProps } from "@odoo/owl";
|
||||
import { AutoComplete } from "@web/core/autocomplete/autocomplete";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { formatAST, toPyValue } from "@web/core/py_js/py_utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { TagsList } from "@web/core/tags_list/tags_list";
|
||||
import { useOwnedDialogs, useService } from "@web/core/utils/hooks";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import { Expression } from "@web/core/domain_tree";
|
||||
|
||||
const SEARCH_LIMIT = 7;
|
||||
const SEARCH_MORE_LIMIT = 320;
|
||||
|
||||
const isId = (val) => Number.isInteger(val) && val >= 1;
|
||||
|
||||
class AutoCompleteWithSources extends Component {
|
||||
static props = {
|
||||
resModel: String,
|
||||
update: Function,
|
||||
multiSelect: Boolean,
|
||||
getIds: Function,
|
||||
value: String,
|
||||
className: { type: String, optional: true },
|
||||
fieldString: { type: String, optional: true },
|
||||
};
|
||||
static components = { AutoComplete };
|
||||
static template = "web.DomainSelector.AutoCompleteWithSources";
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.nameService = useService("name");
|
||||
this.addDialog = useOwnedDialogs();
|
||||
this.sources = [
|
||||
{
|
||||
placeholder: _t("Loading..."),
|
||||
options: this.loadOptionsSource.bind(this),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
addNames(nameGets) {
|
||||
const displayNames = {};
|
||||
for (const [id, label] of nameGets) {
|
||||
displayNames[id] = label.split("\n")[0];
|
||||
}
|
||||
this.nameService.addDisplayNames(this.props.resModel, displayNames);
|
||||
}
|
||||
|
||||
getIds() {
|
||||
return this.props.getIds();
|
||||
}
|
||||
|
||||
async loadOptionsSource(name) {
|
||||
if (this.lastProm) {
|
||||
this.lastProm.abort(false);
|
||||
}
|
||||
this.lastProm = this.search(name, SEARCH_LIMIT + 1);
|
||||
const nameGets = await this.lastProm;
|
||||
this.addNames(nameGets);
|
||||
const options = nameGets.map(([value, label]) => ({ value, label: label.split("\n")[0] }));
|
||||
if (SEARCH_LIMIT < nameGets.length) {
|
||||
options.push({
|
||||
label: _t("Search More..."),
|
||||
action: this.onSearchMore.bind(this, name),
|
||||
classList: "o_m2o_dropdown_option",
|
||||
});
|
||||
}
|
||||
if (options.length === 0) {
|
||||
options.push({ label: _t("(no result)"), unselectable: true });
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async onSearchMore(name) {
|
||||
const { fieldString, multiSelect, resModel } = this.props;
|
||||
let operator;
|
||||
const ids = [];
|
||||
if (name) {
|
||||
const nameGets = await this.search(name, SEARCH_MORE_LIMIT);
|
||||
this.addNames(nameGets);
|
||||
operator = "in";
|
||||
ids.push(...nameGets.map((nameGet) => nameGet[0]));
|
||||
} else {
|
||||
operator = "not in";
|
||||
ids.push(...this.getIds());
|
||||
}
|
||||
const dynamicFilters = ids.length
|
||||
? [
|
||||
{
|
||||
description: sprintf(_t("Quick search: %s"), name),
|
||||
domain: [["id", operator, ids]],
|
||||
},
|
||||
]
|
||||
: undefined;
|
||||
// fine for now but we don't like this kind of dependence of core to views
|
||||
const SelectCreateDialog = registry.category("dialogs").get("select_create");
|
||||
this.addDialog(SelectCreateDialog, {
|
||||
title: sprintf(_t("Search: %s"), fieldString),
|
||||
dynamicFilters,
|
||||
resModel,
|
||||
noCreate: true,
|
||||
multiSelect,
|
||||
onSelected: (resId) => {
|
||||
const resIds = Array.isArray(resId) ? resId : [resId];
|
||||
this.props.update([...resIds]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onSelect({ value: resId, action }, params) {
|
||||
if (action) {
|
||||
return action(params);
|
||||
}
|
||||
this.props.update([resId]);
|
||||
}
|
||||
|
||||
search(name, limit) {
|
||||
const ids = this.getIds();
|
||||
const domain = [["id", "not in", ids]];
|
||||
return this.orm.call(this.props.resModel, "name_search", [], {
|
||||
name,
|
||||
args: domain,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
|
||||
onChange({ inputValue }) {
|
||||
if (!inputValue.length) {
|
||||
this.props.update([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getFormat = (val, displayNames) => {
|
||||
let text;
|
||||
let colorIndex;
|
||||
if (isId(val)) {
|
||||
text =
|
||||
typeof displayNames[val] === "string"
|
||||
? displayNames[val]
|
||||
: sprintf(_t(`Inaccessible/missing record ID: %s`), val);
|
||||
colorIndex = typeof displayNames[val] === "string" ? 0 : 2; // 0 = grey, 2 = orange
|
||||
} else {
|
||||
text =
|
||||
val instanceof Expression
|
||||
? String(val)
|
||||
: sprintf(_t(`Invalid record ID: %s`), formatAST(toPyValue(val)));
|
||||
colorIndex = val instanceof Expression ? 2 : 1; // 1 = red
|
||||
}
|
||||
return { text, colorIndex };
|
||||
};
|
||||
|
||||
export class DomainSelectorAutocomplete extends Component {
|
||||
static props = {
|
||||
resModel: String,
|
||||
update: Function,
|
||||
value: true,
|
||||
fieldString: { type: String, optional: true },
|
||||
};
|
||||
static components = { AutoCompleteWithSources, TagsList };
|
||||
static template = "web.DomainSelector.DomainSelectorAutocomplete";
|
||||
|
||||
setup() {
|
||||
this.nameService = useService("name");
|
||||
onWillStart(() => this.computeDerivedParams());
|
||||
onWillUpdateProps((nextProps) => this.computeDerivedParams(nextProps));
|
||||
}
|
||||
|
||||
async computeDerivedParams(props = this.props) {
|
||||
const displayNames = await this.getDisplayNames(props);
|
||||
this.tags = this.getTags(props, displayNames);
|
||||
}
|
||||
|
||||
async getDisplayNames(props) {
|
||||
const ids = this.getIds(props);
|
||||
return this.nameService.loadDisplayNames(props.resModel, ids);
|
||||
}
|
||||
|
||||
getIds(props = this.props) {
|
||||
return props.value.filter((val) => isId(val));
|
||||
}
|
||||
|
||||
getTags(props, displayNames) {
|
||||
return props.value.map((val, index) => {
|
||||
const { text, colorIndex } = getFormat(val, displayNames);
|
||||
return {
|
||||
text,
|
||||
colorIndex,
|
||||
onDelete: () => {
|
||||
this.props.update([
|
||||
...this.props.value.slice(0, index),
|
||||
...this.props.value.slice(index + 1),
|
||||
]);
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
update(resIds) {
|
||||
this.props.update([...this.props.value, ...resIds]);
|
||||
}
|
||||
}
|
||||
|
||||
export class DomainSelectorSingleAutocomplete extends Component {
|
||||
static props = {
|
||||
resModel: String,
|
||||
update: Function,
|
||||
value: true,
|
||||
fieldString: { type: String, optional: true },
|
||||
};
|
||||
static components = { AutoCompleteWithSources };
|
||||
static template = "web.DomainSelector.DomainSelectorSingleAutocomplete";
|
||||
|
||||
setup() {
|
||||
this.nameService = useService("name");
|
||||
onWillStart(() => this.computeDerivedParams());
|
||||
onWillUpdateProps((nextProps) => this.computeDerivedParams(nextProps));
|
||||
}
|
||||
|
||||
async computeDerivedParams(props = this.props) {
|
||||
const displayNames = await this.getDisplayNames(props);
|
||||
this.displayName = this.getDisplayName(props, displayNames);
|
||||
}
|
||||
|
||||
async getDisplayNames(props) {
|
||||
const ids = this.getIds(props);
|
||||
return this.nameService.loadDisplayNames(props.resModel, ids);
|
||||
}
|
||||
|
||||
getDisplayName(props = this.props, displayNames) {
|
||||
const { value } = props;
|
||||
if (value === false) {
|
||||
return "";
|
||||
}
|
||||
const { text } = getFormat(value, displayNames);
|
||||
return text;
|
||||
}
|
||||
|
||||
getIds(props = this.props) {
|
||||
if (isId(props.value)) {
|
||||
return [props.value];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
update(resIds) {
|
||||
this.props.update(resIds[0] || false);
|
||||
this.render(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelector.AutoCompleteWithSources" owl="1" >
|
||||
<AutoComplete
|
||||
value="props.value"
|
||||
autoSelect="true"
|
||||
resetOnSelect="props.multiSelect"
|
||||
class="props.className"
|
||||
sources="sources"
|
||||
onSelect.bind="onSelect"
|
||||
onChange.bind="onChange"
|
||||
/>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.DomainSelectorAutocomplete" owl="1" >
|
||||
<div class="d-flex flex-wrap h-100 gap-1">
|
||||
<TagsList tags="tags"/>
|
||||
<AutoCompleteWithSources
|
||||
resModel="props.resModel"
|
||||
value="''"
|
||||
className="'o_input o_domain_leaf_value_input flex-grow-1'"
|
||||
fieldString="props.fieldString"
|
||||
multiSelect="true"
|
||||
getIds.bind="getIds"
|
||||
update.bind="update"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.DomainSelectorSingleAutocomplete" owl="1" >
|
||||
<AutoCompleteWithSources
|
||||
resModel="props.resModel"
|
||||
value="displayName"
|
||||
className="'o_input o_domain_leaf_value_input h-100'"
|
||||
fieldString="props.fieldString"
|
||||
multiSelect="false"
|
||||
getIds="() => []"
|
||||
update.bind="update"
|
||||
/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -16,17 +16,24 @@ 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?", "onExprRemove"];
|
||||
static props = ["info", "value", "update", "fieldDef?"];
|
||||
static template = "web.DomainSelector.Editor";
|
||||
|
||||
get isExprValue() {
|
||||
return this.props.value instanceof Expression;
|
||||
get stringifiedValue() {
|
||||
return formatValue(this.props.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,14 +101,10 @@ class TagInput extends Component {
|
||||
}
|
||||
|
||||
class Range extends Component {
|
||||
static props = ["value", "update", "component", "subProps", "getValueOnExprRemove"];
|
||||
static props = ["value", "update", "editorInfo", "fieldDef"];
|
||||
static template = "web.DomainSelector.Range";
|
||||
static components = { Editor };
|
||||
|
||||
get editorInfo() {
|
||||
return { component: this.props.component, extractProps: this.props.subProps };
|
||||
}
|
||||
|
||||
update(index, newValue) {
|
||||
const result = [...this.props.value];
|
||||
result[index] = newValue;
|
||||
@@ -115,10 +118,6 @@ class Range extends Component {
|
||||
getUpdater(index) {
|
||||
return (newValue) => this.update(index, newValue);
|
||||
}
|
||||
|
||||
getExprRemover(index) {
|
||||
return () => this.update(index, this.props.getValueOnExprRemove());
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -133,10 +132,54 @@ function parseValue(fieldType, value) {
|
||||
}
|
||||
}
|
||||
|
||||
function makeEditor(component, props) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -179,47 +222,53 @@ const DEFAULT = {
|
||||
defaultValue: () => "",
|
||||
};
|
||||
|
||||
const SET_NOT_SET_EDITOR = {
|
||||
component: null,
|
||||
extractProps: identity,
|
||||
isSupported: (value) => value === false,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const BOOLEAN = {
|
||||
operators: ["is", "is_not"],
|
||||
editors: {
|
||||
default: makeEditor(Select, ({ value, update }) => ({
|
||||
value,
|
||||
update,
|
||||
options: [
|
||||
[true, _lt("set")],
|
||||
[false, _lt("not set")],
|
||||
],
|
||||
})),
|
||||
default: makeEditor(Select, {
|
||||
props: ({ value, update }) => ({
|
||||
value,
|
||||
update,
|
||||
options: [
|
||||
[true, _lt("set")],
|
||||
[false, _lt("not set")],
|
||||
],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
defaultValue: () => true,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const DATETIME_EDITOR_BETWEEN = makeEditor(Range, ({ value, update, fieldDef }) => ({
|
||||
value,
|
||||
update,
|
||||
getValueOnExprRemove: () => genericSerializeDate(fieldDef.type, DateTime.local()),
|
||||
component: DateTimeInput,
|
||||
subProps: ({ value, update }) => ({
|
||||
value: genericDeserializeDate(fieldDef.type, value),
|
||||
type: fieldDef.type,
|
||||
onApply: (value) =>
|
||||
update(value ? genericSerializeDate(fieldDef.type, value) : DateTime.local()),
|
||||
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, ({ value, update, fieldDef }) => ({
|
||||
value: genericDeserializeDate(fieldDef.type, value),
|
||||
type: fieldDef.type,
|
||||
onApply: (value) =>
|
||||
update(value ? genericSerializeDate(fieldDef.type, value) : DateTime.local()),
|
||||
})),
|
||||
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()),
|
||||
@@ -242,56 +291,126 @@ const TEXT = {
|
||||
const NUMBER = {
|
||||
operators: ["=", "!=", ">", ">=", "<", "<=", "between", "ilike", "not ilike", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(Input, ({ value, update, fieldDef }) => ({
|
||||
value: String(value),
|
||||
update: (value) => update(parseValue(fieldDef.type, value)),
|
||||
})),
|
||||
between: makeEditor(Range, ({ value, update, fieldDef }) => ({
|
||||
value,
|
||||
update,
|
||||
getValueOnExprRemove: () => 1,
|
||||
component: Input,
|
||||
subProps: ({ value, update }) => ({
|
||||
default: makeEditor(Input, {
|
||||
props: ({ value, update, fieldDef }) => ({
|
||||
value: String(value),
|
||||
update: (newValue) => update(parseValue(fieldDef.type, newValue)),
|
||||
update: (value) => update(parseValue(fieldDef.type, value)),
|
||||
}),
|
||||
})),
|
||||
}),
|
||||
between: makeEditor(Range, {
|
||||
props: ({ value, update, fieldDef }) => ({
|
||||
value,
|
||||
update,
|
||||
editorInfo: getEditorInfo(fieldDef),
|
||||
fieldDef,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
defaultValue: () => 1,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const RELATIONAL_EDITOR_EQUALITY = makeEditor(Input, ({ value, update }) => ({
|
||||
value: `${value}`,
|
||||
update: (value) => update(parseValue("integer", value)),
|
||||
}));
|
||||
const isId = (value) => Number.isInteger(value) && value >= 1;
|
||||
|
||||
const RELATIONAL = {
|
||||
operators: ["=", "!=", "ilike", "not ilike", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(Input),
|
||||
equal: RELATIONAL_EDITOR_EQUALITY,
|
||||
"!=": RELATIONAL_EDITOR_EQUALITY,
|
||||
const RELATIONAL_EDITOR_EQUALITY = makeEditor(DomainSelectorSingleAutocomplete, {
|
||||
props: ({ value, update, fieldDef }) => {
|
||||
return {
|
||||
resModel: fieldDef.relation,
|
||||
fieldString: fieldDef.string,
|
||||
update,
|
||||
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: (value) => update([...new Set(value)]),
|
||||
value: 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 "";
|
||||
}
|
||||
},
|
||||
defaultValue: ({ type }) => (type === "many2one" ? 1 : []),
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
const SELECTION_EDITOR_IN = makeEditor(Input, ({ value, update }) => ({
|
||||
value: formatAST(toPyValue(value)),
|
||||
update: (value) => update(evaluateExpr(value)),
|
||||
}));
|
||||
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, ({ value, update, fieldDef }) => ({
|
||||
value,
|
||||
update,
|
||||
options: fieldDef.selection || [],
|
||||
})),
|
||||
default: makeEditor(Select, {
|
||||
props: ({ value, update, fieldDef }) => ({
|
||||
value,
|
||||
update,
|
||||
options: fieldDef.selection || [],
|
||||
}),
|
||||
}),
|
||||
in: SELECTION_EDITOR_IN,
|
||||
"not in": SELECTION_EDITOR_IN,
|
||||
},
|
||||
@@ -308,11 +427,13 @@ const PROPERTIES = {
|
||||
const PROPERTIES_SELECTION = {
|
||||
operators: ["=", "!=", "set", "not_set"],
|
||||
editors: {
|
||||
default: makeEditor(Select, ({ value, update, fieldDef }) => ({
|
||||
value,
|
||||
update,
|
||||
options: fieldDef.selection || [],
|
||||
})),
|
||||
default: makeEditor(Select, {
|
||||
props: ({ value, update, fieldDef }) => ({
|
||||
value,
|
||||
update,
|
||||
options: fieldDef.selection || [],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
defaultValue: ({ selection }) => selection?.[0]?.[0] ?? false,
|
||||
};
|
||||
@@ -344,10 +465,10 @@ export const FIELD_DESCRIPTIONS = {
|
||||
html: TEXT,
|
||||
integer: NUMBER,
|
||||
json: JSON_FIELD,
|
||||
many2many: RELATIONAL,
|
||||
many2one: RELATIONAL,
|
||||
many2many: X2MANY,
|
||||
many2one: MANY2ONE,
|
||||
monetary: NUMBER,
|
||||
one2many: RELATIONAL,
|
||||
one2many: X2MANY,
|
||||
properties: PROPERTIES,
|
||||
properties_definition: PROPERTIES,
|
||||
selection: SELECTION,
|
||||
@@ -375,7 +496,9 @@ export function getFieldInfo(fieldDef) {
|
||||
|
||||
export function getEditorInfo(fieldDef, operator) {
|
||||
const descr = getFieldInfo(fieldDef);
|
||||
return descr.editors[operator] || descr.editors.default;
|
||||
const editorInfo = descr.editors[operator] || descr.editors.default;
|
||||
editorInfo.defaultValue = () => getDefaultValue(fieldDef, operator);
|
||||
return editorInfo;
|
||||
}
|
||||
|
||||
export function getOperatorsInfo(fieldDef) {
|
||||
@@ -383,12 +506,12 @@ export function getOperatorsInfo(fieldDef) {
|
||||
return selectOperators(descr.operators);
|
||||
}
|
||||
|
||||
export function getDefaultFieldValue(fieldDef, operator) {
|
||||
export function getDefaultValue(fieldDef, operator) {
|
||||
const descr = getFieldInfo(fieldDef);
|
||||
return descr.defaultValue(fieldDef, operator);
|
||||
}
|
||||
|
||||
export function getDefaultOperator(fieldDef) {
|
||||
const [firstOperator] = getOperatorsInfo(fieldDef);
|
||||
return firstOperator;
|
||||
const descr = getFieldInfo(fieldDef);
|
||||
return descr.operators[0];
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.DomainSelector.Editor" owl="1">
|
||||
<t t-if="isExprValue">
|
||||
<div class="o_ds_expr_value o_input d-flex align-items-center p-0" data-tooltip="Expressions can not be edited">
|
||||
<span class="ms-2" t-esc="props.value" />
|
||||
<i role="alert" class="ms-auto fa fa-exclamation-triangle text-warning" title="Expression" />
|
||||
<button class="btn btn-link p-1" title="Clear" t-on-click="() => this.props.onExprRemove()">
|
||||
<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-else="">
|
||||
<t t-component="props.info.component" t-props="props.info.extractProps({ value: props.value, update: props.update, fieldDef: props.fieldDef })" />
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="web.DomainSelector.PathEditor" owl="1">
|
||||
@@ -52,9 +52,9 @@
|
||||
|
||||
<t t-name="web.DomainSelector.Range" owl="1">
|
||||
<div class="d-flex h-100 align-items-center">
|
||||
<Editor info="editorInfo" value="getValue(0)" update="getUpdater(0)" onExprRemove="getExprRemover(0)" />
|
||||
<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="editorInfo" value="getValue(1)" update="getUpdater(1)" onExprRemove="getExprRemover(1)" />
|
||||
<Editor info="props.editorInfo" value="getValue(1)" update="getUpdater(1)" fieldDef="props.fieldDef" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { getDefaultFieldValue, getDefaultOperator } from "./domain_selector_fields";
|
||||
import { getDefaultValue, getDefaultOperator } from "./domain_selector_fields";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Expression, toValue, toDomain, toTree } from "@web/core/domain_tree";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
@@ -20,7 +20,7 @@ export function createVirtualOperators(tree, getFieldDef) {
|
||||
const fieldDef = getFieldDef(path);
|
||||
if (fieldDef?.type === "boolean") {
|
||||
return { ...tree, operator: operator === "=" ? "is" : "is_not" };
|
||||
} else if (value === false) {
|
||||
} else if (fieldDef?.type !== "many2one" && value === false) {
|
||||
return { ...tree, operator: operator === "=" ? "not_set" : "set" };
|
||||
}
|
||||
}
|
||||
@@ -255,13 +255,13 @@ function getDefaultDomain(fieldDefs) {
|
||||
for (const name of SPECIAL_FIELDS) {
|
||||
fieldDef = fieldDefs[name];
|
||||
if (fieldDef) {
|
||||
const operatorInfo = getDefaultOperator(fieldDef);
|
||||
const value = getDefaultFieldValue(fieldDef, operatorInfo.operator);
|
||||
const operator = getDefaultOperator(fieldDef);
|
||||
const value = getDefaultValue(fieldDef, operator);
|
||||
return buildDomain({
|
||||
type: "condition",
|
||||
negate: false,
|
||||
path: fieldDef.name,
|
||||
operator: operatorInfo.operator,
|
||||
operator,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { escape } from "@web/core/utils/strings";
|
||||
import { FormViewDialog } from "./form_view_dialog";
|
||||
|
||||
import { Component, markup, useState } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
export class SelectCreateDialog extends Component {
|
||||
setup() {
|
||||
@@ -108,3 +109,5 @@ SelectCreateDialog.defaultProps = {
|
||||
domain: [],
|
||||
context: {},
|
||||
};
|
||||
|
||||
registry.category("dialogs").add("select_create", SelectCreateDialog);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
nextTick,
|
||||
patchDate,
|
||||
patchTimeZone,
|
||||
patchWithCleanup,
|
||||
triggerEvent,
|
||||
} from "../helpers/utils";
|
||||
import { Component, useState, xml } from "@odoo/owl";
|
||||
@@ -28,15 +29,40 @@ import { uiService } from "@web/core/ui/ui_service";
|
||||
import { getPickerApplyButton, getPickerCell } from "./datetime/datetime_test_helpers";
|
||||
import { openModelFieldSelectorPopover } from "./model_field_selector_tests";
|
||||
import { nameService } from "@web/core/name_service";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
function addProductIds() {
|
||||
serverData.models.partner.fields.product_ids = {
|
||||
string: "Products",
|
||||
type: "many2many",
|
||||
relation: "product",
|
||||
searchable: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function selectOperator(el, operator, index = 0) {
|
||||
const select = el.querySelectorAll("select.o_domain_leaf_operator_select")[index];
|
||||
await editSelect(select, null, operator);
|
||||
}
|
||||
|
||||
function getOperatorOptions(el, index = 0) {
|
||||
const select = el.querySelectorAll("select.o_domain_leaf_operator_select")[index];
|
||||
return [...select.options].map((o) => o.label);
|
||||
}
|
||||
|
||||
function getSelectedOperator(el, index = 0) {
|
||||
const select = el.querySelectorAll("select.o_domain_leaf_operator_select")[index];
|
||||
return select.options[select.selectedIndex].label;
|
||||
}
|
||||
|
||||
function getAutocompletValue(target, index = 0) {
|
||||
return target.querySelectorAll(".o_ds_value_cell .o-autocomplete--input")[index].value;
|
||||
}
|
||||
|
||||
async function mountComponent(Component, params = {}) {
|
||||
const env = await makeTestEnv({ serverData, mockRPC: params.mockRPC });
|
||||
await mount(MainComponentsContainer, target, { env });
|
||||
@@ -132,6 +158,7 @@ QUnit.module("Components", (hooks) => {
|
||||
registry.category("services").add("localization", makeFakeLocalizationService());
|
||||
registry.category("services").add("field", fieldService);
|
||||
registry.category("services").add("name", nameService);
|
||||
registry.category("services").add("dialog", dialogService);
|
||||
|
||||
target = getFixture();
|
||||
});
|
||||
@@ -338,7 +365,7 @@ QUnit.module("Components", (hooks) => {
|
||||
await makeDomainSelector({
|
||||
domain: `[("int", "=", id)]`,
|
||||
update(domain) {
|
||||
assert.strictEqual(domain, `[("int", "<", id)]`);
|
||||
assert.strictEqual(domain, `[("int", "<", 1)]`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -351,29 +378,29 @@ QUnit.module("Components", (hooks) => {
|
||||
await editSelect(target, ".o_domain_leaf_operator_select", "<");
|
||||
assert.strictEqual(target.querySelector(".o_model_field_selector").innerText, "Integer");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "<");
|
||||
assert.containsNone(target, ".o_ds_value_cell input");
|
||||
assert.containsOnce(target, ".o_ds_expr_value");
|
||||
assert.strictEqual(target.querySelector(".o_ds_expr_value").textContent, "id");
|
||||
assert.containsOnce(target, ".o_ds_value_cell input");
|
||||
assert.containsNone(target, ".o_ds_expr_value");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell input").value, "1");
|
||||
});
|
||||
|
||||
QUnit.test("building a domain with a m2o without following the relation", async (assert) => {
|
||||
assert.expect(1);
|
||||
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_id", "ilike", 1)]`,
|
||||
isDebugMode: true,
|
||||
update: (domain) => {
|
||||
assert.strictEqual(
|
||||
domain,
|
||||
`[("product_id", "ilike", "pad")]`,
|
||||
"string should have been allowed as m2o value"
|
||||
);
|
||||
assert.step(domain);
|
||||
},
|
||||
});
|
||||
assert.verifySteps([]);
|
||||
assert.containsOnce(target, ".o_ds_expr_value");
|
||||
|
||||
await click(target, ".o_ds_expr_value button");
|
||||
assert.verifySteps([`[("product_id", "ilike", "")]`]);
|
||||
|
||||
const input = target.querySelector(".o_domain_leaf_value_input");
|
||||
input.value = "pad";
|
||||
await triggerEvent(input, null, "change");
|
||||
assert.verifySteps([`[("product_id", "ilike", "pad")]`]);
|
||||
});
|
||||
|
||||
QUnit.test("editing a domain with `parent` key", async (assert) => {
|
||||
@@ -514,7 +541,7 @@ QUnit.module("Components", (hooks) => {
|
||||
QUnit.test("operator fallback (edit mode)", async (assert) => {
|
||||
OPERATOR_DESCRIPTIONS.test = {
|
||||
label: "test",
|
||||
valueMode: "none",
|
||||
valueCount: 0,
|
||||
};
|
||||
registerCleanup(() => {
|
||||
delete OPERATOR_DESCRIPTIONS.test;
|
||||
@@ -1592,11 +1619,11 @@ QUnit.module("Components", (hooks) => {
|
||||
},
|
||||
{
|
||||
domain: `[("product_id", "=", False)]`,
|
||||
text: "Product is not set ",
|
||||
text: "Product = false",
|
||||
},
|
||||
{
|
||||
domain: `[("product_id", "!=", False)]`,
|
||||
text: "Product is set ",
|
||||
text: "Product != false",
|
||||
},
|
||||
{
|
||||
domain: `[("product_id", "in", [])]`,
|
||||
@@ -1633,4 +1660,422 @@ QUnit.module("Components", (hooks) => {
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf").textContent, text);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("many2one field operators (edit)", async (assert) => {
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_id", "=", false)]`,
|
||||
});
|
||||
assert.deepEqual(getOperatorOptions(target), [
|
||||
"is in",
|
||||
"is not in",
|
||||
"=",
|
||||
"!=",
|
||||
"contains",
|
||||
"does not contain",
|
||||
"is set",
|
||||
"is not set",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("many2one field: operator switch (edit)", async (assert) => {
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_id", "=", false)]`,
|
||||
update(domain) {
|
||||
assert.step(domain);
|
||||
},
|
||||
});
|
||||
await selectOperator(target, "in");
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")),
|
||||
[]
|
||||
);
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([`[("product_id", "in", [])]`]);
|
||||
|
||||
await selectOperator(target, "=");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([`[("product_id", "=", False)]`]);
|
||||
|
||||
await selectOperator(target, "not in");
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")),
|
||||
[]
|
||||
);
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([`[("product_id", "not in", [])]`]);
|
||||
|
||||
await selectOperator(target, "ilike");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell .o_input").value, "");
|
||||
assert.verifySteps([`[("product_id", "ilike", "")]`]);
|
||||
|
||||
await selectOperator(target, "!=");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([`[("product_id", "!=", False)]`]);
|
||||
|
||||
await selectOperator(target, "not ilike");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell .o_input").value, "");
|
||||
assert.verifySteps([`[("product_id", "not ilike", "")]`]);
|
||||
});
|
||||
|
||||
QUnit.test("many2one field and operator =/!= (edit)", async (assert) => {
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_id", "=", False)]`,
|
||||
update(domain) {
|
||||
assert.step(domain);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(getSelectedOperator(target), "=");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([]);
|
||||
assert.containsNone(target, ".dropdown-menu");
|
||||
|
||||
await editInput(target, ".o-autocomplete--input", "xph");
|
||||
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".dropdown-menu li")), [
|
||||
"xphone",
|
||||
]);
|
||||
assert.strictEqual(getSelectedOperator(target), "=");
|
||||
assert.strictEqual(getAutocompletValue(target), "xph");
|
||||
|
||||
await click(target, ".dropdown-menu li");
|
||||
assert.strictEqual(getSelectedOperator(target), "=");
|
||||
assert.strictEqual(getAutocompletValue(target), "xphone");
|
||||
assert.verifySteps([`[("product_id", "=", 37)]`]);
|
||||
assert.containsNone(target, ".dropdown-menu");
|
||||
|
||||
await editInput(target, ".o-autocomplete--input", "");
|
||||
assert.strictEqual(getSelectedOperator(target), "=");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([`[("product_id", "=", False)]`]);
|
||||
|
||||
await selectOperator(target, "!=");
|
||||
assert.strictEqual(getSelectedOperator(target), "!=");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([`[("product_id", "!=", False)]`]);
|
||||
|
||||
await editInput(target, ".o-autocomplete--input", "xpa");
|
||||
await click(target, ".dropdown-menu li");
|
||||
assert.strictEqual(getSelectedOperator(target), "!=");
|
||||
assert.strictEqual(getAutocompletValue(target), "xpad");
|
||||
assert.verifySteps([`[("product_id", "!=", 41)]`]);
|
||||
});
|
||||
|
||||
QUnit.test("many2one field and operator in/not in (edit)", async (assert) => {
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_id", "in", [37])]`,
|
||||
update(domain) {
|
||||
assert.step(domain);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(getSelectedOperator(target), "is in");
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")), [
|
||||
"xphone",
|
||||
]);
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([]);
|
||||
assert.containsNone(target, ".dropdown-menu");
|
||||
|
||||
await editInput(target, ".o-autocomplete--input", "x");
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".dropdown-menu li")), [
|
||||
"xpad",
|
||||
]);
|
||||
|
||||
await click(target, ".dropdown-menu li");
|
||||
assert.verifySteps([`[("product_id", "in", [37, 41])]`]);
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")), [
|
||||
"xphone",
|
||||
"xpad",
|
||||
]);
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
|
||||
await selectOperator(target, "not in");
|
||||
assert.strictEqual(getSelectedOperator(target), "is not in");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")), [
|
||||
"xphone",
|
||||
"xpad",
|
||||
]);
|
||||
assert.verifySteps([`[("product_id", "not in", [37, 41])]`]);
|
||||
|
||||
await click(target.querySelector(".o_tag .o_delete"));
|
||||
assert.strictEqual(getSelectedOperator(target), "is not in");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")), [
|
||||
"xpad",
|
||||
]);
|
||||
assert.verifySteps([`[("product_id", "not in", [41])]`]);
|
||||
});
|
||||
|
||||
QUnit.test("many2one field and operator ilike/not ilike (edit)", async (assert) => {
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_id", "ilike", "abc")]`,
|
||||
update(domain) {
|
||||
assert.step(domain);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(getSelectedOperator(target), "contains");
|
||||
assert.containsNone(target, ".o-autocomplete--input");
|
||||
assert.containsOnce(target, ".o_ds_value_cell .o_input");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell .o_input").value, "abc");
|
||||
assert.verifySteps([]);
|
||||
|
||||
await editInput(target, ".o_ds_value_cell .o_input", "def");
|
||||
assert.strictEqual(getSelectedOperator(target), "contains");
|
||||
assert.containsOnce(target, ".o_ds_value_cell .o_input");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell .o_input").value, "def");
|
||||
assert.verifySteps([`[("product_id", "ilike", "def")]`]);
|
||||
|
||||
await selectOperator(target, "not ilike");
|
||||
assert.strictEqual(getSelectedOperator(target), "does not contain");
|
||||
assert.containsOnce(target, ".o_ds_value_cell .o_input");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell .o_input").value, "def");
|
||||
assert.verifySteps([`[("product_id", "not ilike", "def")]`]);
|
||||
});
|
||||
|
||||
QUnit.test("many2many field and operator set/not set (edit)", async (assert) => {
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_id", "=", False)]`,
|
||||
update(domain) {
|
||||
assert.step(domain);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(getSelectedOperator(target), "=");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([]);
|
||||
|
||||
await selectOperator(target, "not_set");
|
||||
|
||||
assert.strictEqual(getSelectedOperator(target), "is not set");
|
||||
assert.containsNone(target, ".o_ds_value_cell");
|
||||
assert.verifySteps([`[("product_id", "=", False)]`]);
|
||||
|
||||
await selectOperator(target, "set");
|
||||
assert.strictEqual(getSelectedOperator(target), "is set");
|
||||
assert.containsNone(target, ".o_ds_value_cell");
|
||||
assert.verifySteps([`[("product_id", "!=", False)]`]);
|
||||
|
||||
await selectOperator(target, "!=");
|
||||
assert.strictEqual(getSelectedOperator(target), "!=");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([`[("product_id", "!=", False)]`]);
|
||||
});
|
||||
|
||||
QUnit.test("many2many field: clone a set/not set condition", async (assert) => {
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_id", "=", False)]`,
|
||||
update(domain) {
|
||||
assert.step(domain);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(getSelectedOperator(target), "=");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([]);
|
||||
|
||||
await selectOperator(target, "not_set");
|
||||
assert.strictEqual(getSelectedOperator(target), "is not set");
|
||||
assert.containsNone(target, ".o_ds_value_cell");
|
||||
assert.verifySteps([`[("product_id", "=", False)]`]);
|
||||
assert.containsOnce(target, ".o_domain_leaf");
|
||||
|
||||
await click(target, ".o_domain_add_node_button .fa-plus");
|
||||
assert.containsN(target, ".o_domain_leaf", 2);
|
||||
assert.strictEqual(getSelectedOperator(target), "is not set");
|
||||
assert.strictEqual(getSelectedOperator(target, 1), "is not set");
|
||||
assert.verifySteps([`["&", ("product_id", "=", False), ("product_id", "=", False)]`]);
|
||||
});
|
||||
|
||||
QUnit.test("x2many field operators (edit)", async (assert) => {
|
||||
addProductIds();
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_ids", "=", false)]`,
|
||||
});
|
||||
assert.deepEqual(getOperatorOptions(target), [
|
||||
"is in",
|
||||
"is not in",
|
||||
"=",
|
||||
"!=",
|
||||
"contains",
|
||||
"does not contain",
|
||||
"is set",
|
||||
"is not set",
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("x2many field: operator switch (edit)", async (assert) => {
|
||||
addProductIds();
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_ids", "=", false)]`,
|
||||
update(domain) {
|
||||
assert.step(domain);
|
||||
},
|
||||
});
|
||||
await selectOperator(target, "in");
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")),
|
||||
[]
|
||||
);
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([`[("product_ids", "in", [])]`]);
|
||||
|
||||
await selectOperator(target, "=");
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")),
|
||||
[]
|
||||
);
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([`[("product_ids", "=", [])]`]);
|
||||
|
||||
await selectOperator(target, "not in");
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")),
|
||||
[]
|
||||
);
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([`[("product_ids", "not in", [])]`]);
|
||||
|
||||
await selectOperator(target, "ilike");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell .o_input").value, "");
|
||||
assert.verifySteps([`[("product_ids", "ilike", "")]`]);
|
||||
|
||||
await selectOperator(target, "not_set");
|
||||
assert.containsNone(target, ".o_ds_value_cell");
|
||||
assert.verifySteps([`[("product_ids", "=", False)]`]);
|
||||
|
||||
await selectOperator(target, "!=");
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")),
|
||||
[]
|
||||
);
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([`[("product_ids", "!=", [])]`]);
|
||||
|
||||
await selectOperator(target, "not ilike");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell .o_input").value, "");
|
||||
assert.verifySteps([`[("product_ids", "not ilike", "")]`]);
|
||||
|
||||
await selectOperator(target, "set");
|
||||
assert.containsNone(target, ".o_ds_value_cell");
|
||||
assert.verifySteps([`[("product_ids", "!=", False)]`]);
|
||||
});
|
||||
|
||||
QUnit.test("many2many field and operator =/!=/in/not in (edit)", async (assert) => {
|
||||
addProductIds();
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_ids", "in", [37])]`,
|
||||
update(domain) {
|
||||
assert.step(domain);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(getSelectedOperator(target), "is in");
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")), [
|
||||
"xphone",
|
||||
]);
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.verifySteps([]);
|
||||
assert.containsNone(target, ".dropdown-menu");
|
||||
|
||||
await editInput(target, ".o-autocomplete--input", "x");
|
||||
assert.containsOnce(target, ".dropdown-menu");
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".dropdown-menu li")), [
|
||||
"xpad",
|
||||
]);
|
||||
|
||||
await click(target, ".dropdown-menu li");
|
||||
assert.verifySteps([`[("product_ids", "in", [37, 41])]`]);
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")), [
|
||||
"xphone",
|
||||
"xpad",
|
||||
]);
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
|
||||
await selectOperator(target, "not in");
|
||||
assert.strictEqual(getSelectedOperator(target), "is not in");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")), [
|
||||
"xphone",
|
||||
"xpad",
|
||||
]);
|
||||
assert.verifySteps([`[("product_ids", "not in", [37, 41])]`]);
|
||||
|
||||
await click(target.querySelector(".o_tag .o_delete"));
|
||||
assert.strictEqual(getSelectedOperator(target), "is not in");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")), [
|
||||
"xpad",
|
||||
]);
|
||||
assert.verifySteps([`[("product_ids", "not in", [41])]`]);
|
||||
|
||||
await selectOperator(target, "=");
|
||||
assert.strictEqual(getSelectedOperator(target), "=");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")),
|
||||
[]
|
||||
); // to improve -> should be [xpad]
|
||||
assert.verifySteps([`[("product_ids", "=", [])]`]);
|
||||
|
||||
await selectOperator(target, "!=");
|
||||
assert.strictEqual(getSelectedOperator(target), "!=");
|
||||
assert.strictEqual(getAutocompletValue(target), "");
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_ds_value_cell .o_tag")),
|
||||
[]
|
||||
);
|
||||
assert.verifySteps([`[("product_ids", "!=", [])]`]);
|
||||
});
|
||||
|
||||
QUnit.test("many2many field and operator ilike/not ilike (edit)", async (assert) => {
|
||||
addProductIds();
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_ids", "ilike", "abc")]`,
|
||||
update(domain) {
|
||||
assert.step(domain);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(getSelectedOperator(target), "contains");
|
||||
assert.containsNone(target, ".o-autocomplete--input");
|
||||
assert.containsOnce(target, ".o_ds_value_cell .o_input");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell .o_input").value, "abc");
|
||||
assert.verifySteps([]);
|
||||
|
||||
await editInput(target, ".o_ds_value_cell .o_input", "def");
|
||||
assert.strictEqual(getSelectedOperator(target), "contains");
|
||||
assert.containsOnce(target, ".o_ds_value_cell .o_input");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell .o_input").value, "def");
|
||||
assert.verifySteps([`[("product_ids", "ilike", "def")]`]);
|
||||
|
||||
await selectOperator(target, "not ilike");
|
||||
assert.strictEqual(getSelectedOperator(target), "does not contain");
|
||||
assert.containsOnce(target, ".o_ds_value_cell .o_input");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell .o_input").value, "def");
|
||||
assert.verifySteps([`[("product_ids", "not ilike", "def")]`]);
|
||||
});
|
||||
|
||||
QUnit.test("many2many field and operator set/not set (edit)", async (assert) => {
|
||||
addProductIds();
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
await makeDomainSelector({
|
||||
domain: `[("product_ids", "=", False)]`,
|
||||
update(domain) {
|
||||
assert.step(domain);
|
||||
},
|
||||
});
|
||||
assert.strictEqual(getSelectedOperator(target), "is not set");
|
||||
assert.containsNone(target, ".o_ds_value_cell");
|
||||
assert.verifySteps([]);
|
||||
|
||||
await selectOperator(target, "set");
|
||||
assert.strictEqual(getSelectedOperator(target), "is set");
|
||||
assert.containsNone(target, ".o_ds_value_cell");
|
||||
assert.verifySteps([`[("product_ids", "!=", False)]`]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1551,6 +1551,7 @@ QUnit.module("Search", (hooks) => {
|
||||
serverData.models.foo.fields.country_id = {
|
||||
string: "Country",
|
||||
type: "many2one",
|
||||
relation: "country",
|
||||
};
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
@@ -1825,7 +1826,7 @@ QUnit.module("Search", (hooks) => {
|
||||
assert.deepEqual(getFacetTexts(target), [
|
||||
"Bar = John",
|
||||
"Bar is in ( David , Inaccessible/missing record ID: 5555 )",
|
||||
"Bar is set",
|
||||
"Bar != false",
|
||||
"ID = 2",
|
||||
]);
|
||||
assert.deepEqual(getDomain(controlPanel), [
|
||||
|
||||
Reference in New Issue
Block a user