[IMP] web: Advanced Search
The old submenu "Add Custom Filter" of the menu "Filters" is now replaced by a menu item "Advanced Search". Click on that item will open an "Advanced Search" dialog in which the current search domain is displayed in a domain selector. It is then possible to easily add/remove/edit some parts of that domain before make a new search with the edited domain. Task ID: 3269923 Part-of: odoo/odoo#118057 Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com> Co-authored-by: Michaël Mattiello <mcm@odoo.com>
This commit is contained in:
co-authored by
Michaël Mattiello
parent
01f90daef0
commit
1c374b9b05
@@ -9,10 +9,8 @@ import {
|
||||
triggerEvent,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import {
|
||||
applyFilter,
|
||||
applyGroup,
|
||||
editConditionValue,
|
||||
toggleAddCustomFilter,
|
||||
removeFacet,
|
||||
toggleAddCustomGroup,
|
||||
toggleComparisonMenu,
|
||||
toggleFavoriteMenu,
|
||||
@@ -82,11 +80,7 @@ QUnit.module("Board", (hooks) => {
|
||||
};
|
||||
|
||||
LegacyFavoriteMenu.registry.add("add-to-board-menu", LegacyAddToBoard, 10);
|
||||
favoriteMenuRegistry.add(
|
||||
"add-to-board",
|
||||
addToBoardItem,
|
||||
{ sequence: 10 }
|
||||
);
|
||||
favoriteMenuRegistry.add("add-to-board", addToBoardItem, { sequence: 10 });
|
||||
serverData = { models };
|
||||
target = getFixture();
|
||||
});
|
||||
@@ -167,7 +161,12 @@ QUnit.module("Board", (hooks) => {
|
||||
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
|
||||
serverData.views = {
|
||||
"partner,false,list": '<list><field name="foo"/></list>',
|
||||
"partner,false,search": "<search></search>",
|
||||
"partner,false,search": `
|
||||
<search>
|
||||
<filter name="filter_on_a" string="Filter on a" domain="[['display_name', 'ilike', 'a']]"/>
|
||||
<filter name="filter_on_b" string="Filter on b" domain="[['display_name', 'ilike', 'b']]"/>
|
||||
</search>
|
||||
`,
|
||||
};
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
@@ -204,9 +203,7 @@ QUnit.module("Board", (hooks) => {
|
||||
var filter_count = 0;
|
||||
// Add a first filter
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
await editConditionValue(target, 0, "a");
|
||||
await applyFilter(target);
|
||||
await toggleMenuItem(target, "Filter on a");
|
||||
|
||||
// Add it to dashboard
|
||||
await toggleFavoriteMenu(target);
|
||||
@@ -214,13 +211,12 @@ QUnit.module("Board", (hooks) => {
|
||||
await testUtils.dom.click($(".o_add_to_board .dropdown-menu button"));
|
||||
|
||||
// Remove it
|
||||
await testUtils.dom.click(target.querySelector(".o_facet_remove"));
|
||||
await removeFacet(target);
|
||||
|
||||
// Add the second filter
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
await editConditionValue(target, 0, "b");
|
||||
await applyFilter(target);
|
||||
await toggleMenuItem(target, "Filter on b");
|
||||
|
||||
// Add it to dashboard
|
||||
await toggleFavoriteMenu(target);
|
||||
await testUtils.dom.triggerEvent(
|
||||
@@ -230,7 +226,7 @@ QUnit.module("Board", (hooks) => {
|
||||
await testUtils.dom.click(target.querySelector(".o_add_to_board .dropdown-menu button"));
|
||||
});
|
||||
|
||||
QUnit.test("save a action domain to dashboard", async function (assert) {
|
||||
QUnit.test("save an action domain to dashboard", async function (assert) {
|
||||
// View domains are to be added to the dashboard domain
|
||||
assert.expect(1);
|
||||
|
||||
@@ -242,7 +238,11 @@ QUnit.module("Board", (hooks) => {
|
||||
|
||||
serverData.views = {
|
||||
"partner,false,list": '<list><field name="foo"/></list>',
|
||||
"partner,false,search": "<search></search>",
|
||||
"partner,false,search": `
|
||||
<search>
|
||||
<filter name="filter" string="Filter" domain="[['display_name', 'ilike', 'b']]"/>
|
||||
</search>
|
||||
`,
|
||||
};
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
@@ -264,9 +264,8 @@ QUnit.module("Board", (hooks) => {
|
||||
|
||||
// Add a filter
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
await editConditionValue(target, 0, "b");
|
||||
await applyFilter(target);
|
||||
await toggleMenuItem(target, "Filter");
|
||||
|
||||
// Add it to dashboard
|
||||
await toggleFavoriteMenu(target);
|
||||
await testUtils.dom.triggerEvent(
|
||||
@@ -282,7 +281,7 @@ QUnit.module("Board", (hooks) => {
|
||||
|
||||
serverData.views = {
|
||||
"partner,false,pivot": '<pivot><field name="foo"/></pivot>',
|
||||
"partner,false,search": '<search/>',
|
||||
"partner,false,search": "<search/>",
|
||||
};
|
||||
registry.category("services").add("user", makeFakeUserService());
|
||||
const webClient = await createWebClient({ serverData });
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "../l10n/translation";
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { Dialog } from "../dialog/dialog";
|
||||
import { DomainSelector } from "../domain_selector/domain_selector";
|
||||
import { _t } from "../l10n/translation";
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
|
||||
export class DomainSelectorDialog extends Component {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
value: this.props.initialValue,
|
||||
domain: this.props.domain,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,15 +23,15 @@ export class DomainSelectorDialog extends Component {
|
||||
readonly: this.props.readonly,
|
||||
isDebugMode: this.props.isDebugMode,
|
||||
defaultLeafValue: this.props.defaultLeafValue,
|
||||
value: this.state.value,
|
||||
update: (value) => {
|
||||
this.state.value = value;
|
||||
value: this.state.domain,
|
||||
update: (domain) => {
|
||||
this.state.domain = domain;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async onSave() {
|
||||
await this.props.onSelected(this.state.value);
|
||||
async onConfirm() {
|
||||
await this.props.onConfirm(this.state.domain);
|
||||
this.props.close();
|
||||
}
|
||||
onDiscard() {
|
||||
@@ -46,18 +45,17 @@ DomainSelectorDialog.components = {
|
||||
};
|
||||
DomainSelectorDialog.props = {
|
||||
close: Function,
|
||||
className: { type: String, optional: true },
|
||||
onConfirm: Function,
|
||||
resModel: String,
|
||||
readonly: { type: Boolean, optional: true },
|
||||
isDebugMode: { type: Boolean, optional: true },
|
||||
className: { type: String, optional: true },
|
||||
defaultLeafValue: { type: Array, optional: true },
|
||||
initialValue: { type: String, optional: true },
|
||||
onSelected: { type: Function, optional: true },
|
||||
domain: { type: String, optional: true },
|
||||
isDebugMode: { type: Boolean, optional: true },
|
||||
readonly: { type: Boolean, optional: true },
|
||||
text: { type: String, optional: true },
|
||||
};
|
||||
DomainSelectorDialog.defaultProps = {
|
||||
initialValue: "",
|
||||
onSelected: () => {},
|
||||
readonly: true,
|
||||
domain: "[]",
|
||||
isDebugMode: false,
|
||||
readonly: false,
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<button class="btn btn-secondary" t-on-click="() => props.close()">Close</button>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<button class="btn btn-primary" t-on-click="onSave">Save</button>
|
||||
<button class="btn btn-primary" t-on-click="onConfirm">Confirm</button>
|
||||
<button class="btn btn-secondary" t-on-click="onDiscard">Discard</button>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
@@ -276,6 +276,6 @@
|
||||
|
||||
CustomFilterItem.components = { DatePicker, DateTimePicker, Dropdown };
|
||||
CustomFilterItem.props = { fields: Object };
|
||||
CustomFilterItem.template = "web.CustomFilterItem";
|
||||
CustomFilterItem.template = "web.Legacy.CustomFilterItem";
|
||||
|
||||
export default CustomFilterItem;
|
||||
|
||||
@@ -369,6 +369,114 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="web.Legacy.CustomFilterItem" owl="1">
|
||||
<Dropdown class="'o_add_custom_filter_menu'">
|
||||
<t t-set-slot="toggler">
|
||||
Add Custom Filter
|
||||
</t>
|
||||
<t t-foreach="conditions" t-as="condition" t-key="condition_index">
|
||||
<div class=" o_filter_condition dropdown-item-text position-relative">
|
||||
<t t-set="fieldType" t-value="fields[condition.field].type"/>
|
||||
<t t-set="selectedOperator" t-value="OPERATORS[FIELD_TYPES[fieldType]][condition.operator]"/>
|
||||
<span t-if="!condition_first" class="o_or_filter">or</span>
|
||||
<select class="o_input o_generator_menu_field"
|
||||
t-on-change="ev => this.onFieldSelect(condition, ev)"
|
||||
>
|
||||
<option t-foreach="fields" t-as="field" t-key="field_index"
|
||||
t-att-value="field.name"
|
||||
t-att-selected="field_index === condition.field"
|
||||
t-esc="field.string"
|
||||
/>
|
||||
</select>
|
||||
<select class="o_input o_generator_menu_operator"
|
||||
t-on-change="ev => this.onOperatorSelect(condition, ev)"
|
||||
>
|
||||
<option t-foreach="OPERATORS[FIELD_TYPES[fieldType]]" t-as="operator" t-key="operator_index"
|
||||
t-att-value="operator.symbol"
|
||||
t-att-selected="operator_index === condition.operator"
|
||||
t-esc="operator.description"
|
||||
/>
|
||||
</select>
|
||||
<span t-if="!('value' in selectedOperator)" class="o_generator_menu_value">
|
||||
<t t-if="fieldType === 'date'">
|
||||
<DatePicker
|
||||
date="condition.value[0]"
|
||||
onDateTimeChanged="date => this.onDateTimeChanged(condition, 0, date)"
|
||||
/>
|
||||
<DatePicker t-if="selectedOperator.symbol === 'between'"
|
||||
date="condition.value[1]"
|
||||
onDateTimeChanged="date => this.onDateTimeChanged(condition, 1, date)"
|
||||
/>
|
||||
</t>
|
||||
<t t-elif="fieldType === 'datetime'">
|
||||
<DateTimePicker
|
||||
date="condition.value[0]"
|
||||
onDateTimeChanged="date => this.onDateTimeChanged(condition, 0, date)"
|
||||
/>
|
||||
<DateTimePicker t-if="selectedOperator.symbol === 'between'"
|
||||
date="condition.value[1]"
|
||||
onDateTimeChanged="date => this.onDateTimeChanged(condition, 1, date)"
|
||||
/>
|
||||
</t>
|
||||
<select t-elif="fieldType === 'selection'" class="o_input"
|
||||
t-on-change="ev => this.onValueChange(condition, ev)"
|
||||
>
|
||||
<option t-foreach="fields[condition.field].selection" t-as="option" t-key="option_index"
|
||||
t-att-value="option[0]"
|
||||
t-esc="option[1]"
|
||||
/>
|
||||
</select>
|
||||
<!-- @todo (DAM) I think that the localization should be better consisered below -->
|
||||
<input t-elif="fieldType === 'float'"
|
||||
class="o_input"
|
||||
step="0.01"
|
||||
t-att-type="DECIMAL_POINT === '.' ? 'number' : 'text'"
|
||||
t-attf-title="Number using {{ DECIMAL_POINT }} as decimal separator."
|
||||
t-attf-pattern="[0-9]+([\\{{ DECIMAL_POINT }}][0-9]+)?"
|
||||
t-model="condition.displayedValue"
|
||||
t-on-change="ev => this.onValueChange(condition, ev)"
|
||||
/>
|
||||
<input t-elif="['integer', 'id'].includes(fieldType)"
|
||||
class="o_input"
|
||||
step="1"
|
||||
type="number"
|
||||
t-model="condition.displayedValue"
|
||||
t-on-change="ev => this.onValueChange(condition, ev)"
|
||||
/>
|
||||
<input t-else=""
|
||||
type="text"
|
||||
class="o_input"
|
||||
t-model="condition.displayedValue"
|
||||
t-on-change="ev => this.onValueChange(condition, ev)"
|
||||
/>
|
||||
</span>
|
||||
<i t-if="conditions.length gt 1"
|
||||
class="fa fa-trash-o o_generator_menu_delete"
|
||||
role="image"
|
||||
aria-label="Delete"
|
||||
title="Delete"
|
||||
t-on-click="() => this.onRemoveCondition(condition_index)"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
<div class="px-3 py-2">
|
||||
<button type="button"
|
||||
class="btn btn-primary o_apply_filter me-2"
|
||||
t-on-click="onApply"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-secondary o_add_condition"
|
||||
t-on-click.stop="addNewCondition"
|
||||
>
|
||||
<i class="fa fa-plus-circle"/>
|
||||
<t>Add a condition</t>
|
||||
</button>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</t>
|
||||
|
||||
<t t-name="web.Legacy.ActionMenus" owl="1">
|
||||
<div class="o_cp_action_menus" t-on-item-selected.stop="_onItemSelected">
|
||||
<DropdownMenu t-if="printItems.length"
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
</t>
|
||||
|
||||
<t t-name="web.legacy.FilterMenu" t-inherit="web.FilterMenu" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//CustomFilterItem" position="attributes">
|
||||
<attribute name="fields">props.fields</attribute>
|
||||
<xpath expr="//DropdownItem" position="replace">
|
||||
<CustomFilterItem fields="props.fields"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { DomainSelectorDialog } from "@web/core/domain_selector_dialog/domain_selector_dialog";
|
||||
|
||||
export class AdvancedSearchDialog extends DomainSelectorDialog {
|
||||
get dialogTitle() {
|
||||
return _t("Advanced Search");
|
||||
}
|
||||
|
||||
async onConfirm() {
|
||||
const confirmed = await this.props.onConfirm(this.state.domain);
|
||||
if (confirmed) {
|
||||
this.props.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
AdvancedSearchDialog.template = "web.AdvancedSearchDialog";
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.AdvancedSearchDialog" t-inherit="web.DomainSelectorDialog" owl="1">
|
||||
<xpath expr="//t[@t-else='']/button[hasclass('btn-primary')]" position="replace">
|
||||
<button class="btn btn-primary" t-on-click="onConfirm">Search</button>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,325 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { DatePicker, DateTimePicker } from "@web/core/datepicker/datepicker";
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { serializeDate, serializeDateTime } from "@web/core/l10n/dates";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
|
||||
const { DateTime } = luxon;
|
||||
|
||||
const formatters = registry.category("formatters");
|
||||
const parsers = registry.category("parsers");
|
||||
|
||||
const FIELD_TYPES = {
|
||||
boolean: "boolean",
|
||||
char: "char",
|
||||
date: "date",
|
||||
datetime: "datetime",
|
||||
float: "number",
|
||||
id: "id",
|
||||
integer: "number",
|
||||
html: "char",
|
||||
many2many: "char",
|
||||
many2one: "char",
|
||||
monetary: "number",
|
||||
one2many: "char",
|
||||
text: "char",
|
||||
selection: "selection",
|
||||
};
|
||||
|
||||
// FilterMenu parameters
|
||||
const FIELD_OPERATORS = {
|
||||
boolean: [
|
||||
{ symbol: "=", description: _lt("is Yes"), value: true },
|
||||
{ symbol: "!=", description: _lt("is No"), value: true },
|
||||
],
|
||||
char: [
|
||||
{ symbol: "ilike", description: _lt("contains") },
|
||||
{ symbol: "not ilike", description: _lt("doesn't contain") },
|
||||
{ symbol: "=", description: _lt("is equal to") },
|
||||
{ symbol: "!=", description: _lt("is not equal to") },
|
||||
{ symbol: "!=", description: _lt("is set"), value: false },
|
||||
{ symbol: "=", description: _lt("is not set"), value: false },
|
||||
],
|
||||
date: [
|
||||
{ symbol: "=", description: _lt("is equal to") },
|
||||
{ symbol: "!=", description: _lt("is not equal to") },
|
||||
{ symbol: ">", description: _lt("is after") },
|
||||
{ symbol: "<", description: _lt("is before") },
|
||||
{ symbol: ">=", description: _lt("is after or equal to") },
|
||||
{ symbol: "<=", description: _lt("is before or equal to") },
|
||||
{ symbol: "between", description: _lt("is between") },
|
||||
{ symbol: "!=", description: _lt("is set"), value: false },
|
||||
{ symbol: "=", description: _lt("is not set"), value: false },
|
||||
],
|
||||
datetime: [
|
||||
{ symbol: "between", description: _lt("is between") },
|
||||
{ symbol: "=", description: _lt("is equal to") },
|
||||
{ symbol: "!=", description: _lt("is not equal to") },
|
||||
{ symbol: ">", description: _lt("is after") },
|
||||
{ symbol: "<", description: _lt("is before") },
|
||||
{ symbol: ">=", description: _lt("is after or equal to") },
|
||||
{ symbol: "<=", description: _lt("is before or equal to") },
|
||||
{ symbol: "!=", description: _lt("is set"), value: false },
|
||||
{ symbol: "=", description: _lt("is not set"), value: false },
|
||||
],
|
||||
id: [{ symbol: "=", description: _lt("is") }],
|
||||
number: [
|
||||
{ symbol: "=", description: _lt("is equal to") },
|
||||
{ symbol: "!=", description: _lt("is not equal to") },
|
||||
{ symbol: ">", description: _lt("greater than") },
|
||||
{ symbol: "<", description: _lt("less than") },
|
||||
{ symbol: ">=", description: _lt("greater than or equal to") },
|
||||
{ symbol: "<=", description: _lt("less than or equal to") },
|
||||
{ symbol: "!=", description: _lt("is set"), value: false },
|
||||
{ symbol: "=", description: _lt("is not set"), value: false },
|
||||
],
|
||||
selection: [
|
||||
{ symbol: "=", description: _lt("is") },
|
||||
{ symbol: "!=", description: _lt("is not") },
|
||||
{ symbol: "!=", description: _lt("is set"), value: false },
|
||||
{ symbol: "=", description: _lt("is not set"), value: false },
|
||||
],
|
||||
};
|
||||
|
||||
function parseField(field, value) {
|
||||
if (FIELD_TYPES[field.type] === "char") {
|
||||
return value;
|
||||
}
|
||||
const type = field.type === "id" ? "integer" : field.type;
|
||||
const parse = parsers.contains(type) ? parsers.get(type) : (v) => v;
|
||||
return parse(value);
|
||||
}
|
||||
|
||||
function formatField(field, value) {
|
||||
if (FIELD_TYPES[field.type] === "char") {
|
||||
return value;
|
||||
}
|
||||
const type = field.type === "id" ? "integer" : field.type;
|
||||
const format = formatters.contains(type) ? formatters.get(type) : (v) => v;
|
||||
return format(value, { digits: field.digits });
|
||||
}
|
||||
|
||||
export class CustomFilterItem extends Component {
|
||||
setup() {
|
||||
this.conditions = useState([]);
|
||||
// Format, filter and sort the fields props
|
||||
this.fields = Object.values(this.env.searchModel.searchViewFields)
|
||||
.filter((field) => this.validateField(field))
|
||||
.concat({ string: "ID", type: "id", name: "id" })
|
||||
.sort(({ string: a }, { string: b }) => (a > b ? 1 : a < b ? -1 : 0));
|
||||
|
||||
// Give access to constants variables to the template.
|
||||
this.OPERATORS = FIELD_OPERATORS;
|
||||
this.FIELD_TYPES = FIELD_TYPES;
|
||||
|
||||
// Add first condition
|
||||
this.addNewCondition();
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Protected
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Populate the conditions list with a new condition having as properties:
|
||||
* - the last condition or the first available field
|
||||
* - the last condition or the first available operator
|
||||
* - a null or empty array value
|
||||
*/
|
||||
addNewCondition() {
|
||||
const lastCondition = [...this.conditions].pop();
|
||||
const condition = lastCondition
|
||||
? Object.assign({}, lastCondition)
|
||||
: {
|
||||
field: 0,
|
||||
operator: 0,
|
||||
};
|
||||
this.setDefaultValue(condition);
|
||||
this.conditions.push(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} field
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validateField(field) {
|
||||
return (
|
||||
!field.deprecated && field.searchable && FIELD_TYPES[field.type] && field.name !== "id"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} condition
|
||||
*/
|
||||
setDefaultValue(condition) {
|
||||
const field = this.fields[condition.field];
|
||||
const genericType = FIELD_TYPES[field.type];
|
||||
const operator = FIELD_OPERATORS[genericType][condition.operator];
|
||||
// Logical value
|
||||
switch (genericType) {
|
||||
case "id":
|
||||
case "number": {
|
||||
condition.value = 0;
|
||||
break;
|
||||
}
|
||||
case "date":
|
||||
case "datetime": {
|
||||
condition.value = [DateTime.local()];
|
||||
if (operator.symbol === "between") {
|
||||
condition.value.push(DateTime.local());
|
||||
}
|
||||
if (genericType === "datetime") {
|
||||
condition.value[0] = condition.value[0].set({ hour: 0, minute: 0, second: 0 });
|
||||
if (operator.symbol === "between") {
|
||||
condition.value[1] = condition.value[1].set({
|
||||
hour: 23,
|
||||
minute: 59,
|
||||
second: 59,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "selection": {
|
||||
const [firstValue] = this.fields[condition.field].selection[0];
|
||||
condition.value = firstValue;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
condition.value = "";
|
||||
}
|
||||
}
|
||||
// Displayed value (no needed for dates: they are handled by the DatePicker component)
|
||||
if (!["date", "datetime"].includes(field.type)) {
|
||||
condition.displayedValue = formatField(field, condition.value);
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Handlers
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Convert all conditions to prefilters.
|
||||
*/
|
||||
onApply() {
|
||||
const preFilters = this.conditions.map((condition) => {
|
||||
const field = this.fields[condition.field];
|
||||
const genericType = this.FIELD_TYPES[field.type];
|
||||
const operator = this.OPERATORS[genericType][condition.operator];
|
||||
const descriptionArray = [field.string, operator.description.toString()];
|
||||
const domainArray = [];
|
||||
let domainValue;
|
||||
// Field type specifics
|
||||
if ("value" in operator) {
|
||||
domainValue = [operator.value];
|
||||
// No description to push here
|
||||
} else if (["date", "datetime"].includes(genericType)) {
|
||||
const serialize = genericType === "date" ? serializeDate : serializeDateTime;
|
||||
domainValue = condition.value.map(serialize);
|
||||
descriptionArray.push(
|
||||
`"${condition.value
|
||||
.map((val) => formatField(field, val))
|
||||
.join(" " + this.env._t("and") + " ")}"`
|
||||
);
|
||||
} else {
|
||||
domainValue = [condition.value];
|
||||
descriptionArray.push(`"${condition.value}"`);
|
||||
}
|
||||
// Operator specifics
|
||||
if (operator.symbol === "between") {
|
||||
domainArray.push(
|
||||
[field.name, ">=", domainValue[0]],
|
||||
[field.name, "<=", domainValue[1]]
|
||||
);
|
||||
} else {
|
||||
domainArray.push([field.name, operator.symbol, domainValue[0]]);
|
||||
}
|
||||
const preFilter = {
|
||||
description: descriptionArray.join(" "),
|
||||
domain: new Domain(domainArray).toString(),
|
||||
type: "filter",
|
||||
};
|
||||
return preFilter;
|
||||
});
|
||||
|
||||
this.env.searchModel.createNewFilters(preFilters);
|
||||
|
||||
// remove conditions
|
||||
while (this.conditions.length) {
|
||||
this.conditions.pop();
|
||||
}
|
||||
|
||||
this.addNewCondition();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} condition
|
||||
* @param {number} valueIndex
|
||||
* @param {Date} ev
|
||||
*/
|
||||
onDateTimeChanged(condition, valueIndex, date) {
|
||||
condition.value[valueIndex] = date;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} condition
|
||||
* @param {Event} ev
|
||||
*/
|
||||
onFieldSelect(condition, ev) {
|
||||
Object.assign(condition, {
|
||||
field: ev.target.selectedIndex,
|
||||
operator: 0,
|
||||
});
|
||||
this.setDefaultValue(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} condition
|
||||
* @param {Event} ev
|
||||
*/
|
||||
onOperatorSelect(condition, ev) {
|
||||
condition.operator = ev.target.selectedIndex;
|
||||
this.setDefaultValue(condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} condition
|
||||
*/
|
||||
onRemoveCondition(conditionIndex) {
|
||||
this.conditions.splice(conditionIndex, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} condition
|
||||
* @param {Event} ev
|
||||
*/
|
||||
onValueChange(condition, ev) {
|
||||
if (!ev.target.value) {
|
||||
return this.setDefaultValue(condition);
|
||||
}
|
||||
const field = this.fields[condition.field];
|
||||
try {
|
||||
const parsed = parseField(field, ev.target.value);
|
||||
const formatted = formatField(field, parsed);
|
||||
// Only updates values if it can be correctly parsed and formatted.
|
||||
condition.value = parsed;
|
||||
condition.displayedValue = formatted;
|
||||
} catch {
|
||||
// Parsing error: nothing is done
|
||||
}
|
||||
// Only reset the target's value if it is not a selection field.
|
||||
if (field.type !== "selection") {
|
||||
ev.target.value = condition.displayedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomFilterItem.props = {};
|
||||
CustomFilterItem.components = { DatePicker, DateTimePicker, Dropdown };
|
||||
CustomFilterItem.template = "web.CustomFilterItem";
|
||||
@@ -1,112 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.CustomFilterItem" owl="1">
|
||||
<Dropdown class="'o_add_custom_filter_menu'">
|
||||
<t t-set-slot="toggler">
|
||||
Add Custom Filter
|
||||
</t>
|
||||
<t t-foreach="conditions" t-as="condition" t-key="condition_index">
|
||||
<div class=" o_filter_condition dropdown-item-text position-relative">
|
||||
<t t-set="fieldType" t-value="fields[condition.field].type"/>
|
||||
<t t-set="selectedOperator" t-value="OPERATORS[FIELD_TYPES[fieldType]][condition.operator]"/>
|
||||
<span t-if="!condition_first" class="o_or_filter">or</span>
|
||||
<select class="o_input o_generator_menu_field"
|
||||
t-on-change="ev => this.onFieldSelect(condition, ev)"
|
||||
>
|
||||
<option t-foreach="fields" t-as="field" t-key="field_index"
|
||||
t-att-value="field.name"
|
||||
t-att-selected="field_index === condition.field"
|
||||
t-esc="field.string"
|
||||
/>
|
||||
</select>
|
||||
<select class="o_input o_generator_menu_operator"
|
||||
t-on-change="ev => this.onOperatorSelect(condition, ev)"
|
||||
>
|
||||
<option t-foreach="OPERATORS[FIELD_TYPES[fieldType]]" t-as="operator" t-key="operator_index"
|
||||
t-att-value="operator.symbol"
|
||||
t-att-selected="operator_index === condition.operator"
|
||||
t-esc="operator.description"
|
||||
/>
|
||||
</select>
|
||||
<span t-if="!('value' in selectedOperator)" class="o_generator_menu_value">
|
||||
<t t-if="fieldType === 'date'">
|
||||
<DatePicker
|
||||
date="condition.value[0]"
|
||||
onDateTimeChanged="date => this.onDateTimeChanged(condition, 0, date)"
|
||||
/>
|
||||
<DatePicker t-if="selectedOperator.symbol === 'between'"
|
||||
date="condition.value[1]"
|
||||
onDateTimeChanged="date => this.onDateTimeChanged(condition, 1, date)"
|
||||
/>
|
||||
</t>
|
||||
<t t-elif="fieldType === 'datetime'">
|
||||
<DateTimePicker
|
||||
date="condition.value[0]"
|
||||
onDateTimeChanged="date => this.onDateTimeChanged(condition, 0, date)"
|
||||
/>
|
||||
<DateTimePicker t-if="selectedOperator.symbol === 'between'"
|
||||
date="condition.value[1]"
|
||||
onDateTimeChanged="date => this.onDateTimeChanged(condition, 1, date)"
|
||||
/>
|
||||
</t>
|
||||
<select t-elif="fieldType === 'selection'" class="o_input"
|
||||
t-on-change="ev => this.onValueChange(condition, ev)"
|
||||
>
|
||||
<option t-foreach="fields[condition.field].selection" t-as="option" t-key="option_index"
|
||||
t-att-value="option[0]"
|
||||
t-esc="option[1]"
|
||||
/>
|
||||
</select>
|
||||
<!-- @todo (DAM) I think that the localization should be better consisered below -->
|
||||
<input t-elif="fieldType === 'float'"
|
||||
class="o_input"
|
||||
step="0.01"
|
||||
t-att-type="DECIMAL_POINT === '.' ? 'number' : 'text'"
|
||||
t-attf-title="Number using {{ DECIMAL_POINT }} as decimal separator."
|
||||
t-attf-pattern="[0-9]+([\\{{ DECIMAL_POINT }}][0-9]+)?"
|
||||
t-model="condition.displayedValue"
|
||||
t-on-change="ev => this.onValueChange(condition, ev)"
|
||||
/>
|
||||
<input t-elif="['integer', 'id'].includes(fieldType)"
|
||||
class="o_input"
|
||||
step="1"
|
||||
type="number"
|
||||
t-model="condition.displayedValue"
|
||||
t-on-change="ev => this.onValueChange(condition, ev)"
|
||||
/>
|
||||
<input t-else=""
|
||||
type="text"
|
||||
class="o_input"
|
||||
t-model="condition.displayedValue"
|
||||
t-on-change="ev => this.onValueChange(condition, ev)"
|
||||
/>
|
||||
</span>
|
||||
<i t-if="conditions.length gt 1"
|
||||
class="fa fa-trash-o o_generator_menu_delete"
|
||||
role="image"
|
||||
aria-label="Delete"
|
||||
title="Delete"
|
||||
t-on-click="() => this.onRemoveCondition(condition_index)"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
<div class="px-3 py-2">
|
||||
<button type="button"
|
||||
class="btn btn-primary o_apply_filter me-2"
|
||||
t-on-click="onApply"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-secondary o_add_condition"
|
||||
t-on-click.stop="addNewCondition"
|
||||
>
|
||||
<i class="fa fa-plus-circle"/>
|
||||
<t>Add a condition</t>
|
||||
</button>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,17 +1,19 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { SearchDropdownItem } from "@web/search/search_dropdown_item/search_dropdown_item";
|
||||
import { CustomFilterItem } from "./custom_filter_item";
|
||||
import { FACET_ICONS } from "../utils/misc";
|
||||
import { useBus } from "@web/core/utils/hooks";
|
||||
|
||||
import { AdvancedSearchDialog } from "./advanced_search_dialog";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { Domain } from "@web/core/domain";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { SearchDropdownItem } from "@web/search/search_dropdown_item/search_dropdown_item";
|
||||
import { FACET_ICONS } from "../utils/misc";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class FilterMenu extends Component {
|
||||
setup() {
|
||||
this.icon = FACET_ICONS.filter;
|
||||
|
||||
this.dialogService = useService("dialog");
|
||||
this.notification = useService("notification");
|
||||
useBus(this.env.searchModel, "update", this.render);
|
||||
}
|
||||
|
||||
@@ -24,6 +26,21 @@ export class FilterMenu extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
onAdvancedSearchClick() {
|
||||
const domains = [this.env.searchModel._getDomain({ withGlobal: false })];
|
||||
if (this.env.searchModel.comparison && !this.env.searchModel.globalComparison) {
|
||||
const { range } = this.env.searchModel.getFullComparison();
|
||||
domains.push(range);
|
||||
}
|
||||
const domain = Domain.and(domains).toString();
|
||||
this.dialogService.add(AdvancedSearchDialog, {
|
||||
domain,
|
||||
onConfirm: (domain) => this.onConfirm(domain),
|
||||
resModel: this.env.searchModel.resModel,
|
||||
isDebugMode: !!this.env.debug,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} param0
|
||||
* @param {number} param0.itemId
|
||||
@@ -36,9 +53,21 @@ export class FilterMenu extends Component {
|
||||
this.env.searchModel.toggleSearchItem(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
async onConfirm(domain) {
|
||||
const isValid = await this.env.searchModel.isValidDomain(domain);
|
||||
if (!isValid) {
|
||||
this.notification.add(this.env._t("Domain is invalid. Please correct it"), {
|
||||
type: "danger",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
this.env.searchModel.createAdvancedDomain(domain);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
FilterMenu.components = { CustomFilterItem, Dropdown, SearchDropdownItem };
|
||||
FilterMenu.components = { Dropdown, DropdownItem, SearchDropdownItem };
|
||||
FilterMenu.template = "web.FilterMenu";
|
||||
FilterMenu.props = {
|
||||
class: { type: String, optional: true },
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<t t-if="items.length">
|
||||
<div role="separator" class="dropdown-divider"/>
|
||||
</t>
|
||||
<CustomFilterItem/>
|
||||
<DropdownItem onSelected.bind="onAdvancedSearchClick" parentClosingMode="'none'">Advanced Search</DropdownItem>
|
||||
</Dropdown>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { FACET_ICONS } from "./utils/misc";
|
||||
import { EventBus, toRaw } from "@odoo/owl";
|
||||
const { DateTime } = luxon;
|
||||
|
||||
/** @typedef {import("@web/core/domain").DomainRepr} DomainRepr */
|
||||
/** @typedef {import("../views/relational_model").OrderTerm} OrderTerm */
|
||||
|
||||
/**
|
||||
@@ -414,6 +415,10 @@ export class SearchModel extends EventBus {
|
||||
return deepCopy(this._domain);
|
||||
}
|
||||
|
||||
get domainEvalContext() {
|
||||
return Object.assign({}, this.globalContext, this.userService.context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Comparison}
|
||||
*/
|
||||
@@ -534,6 +539,51 @@ export class SearchModel extends EventBus {
|
||||
this._notify();
|
||||
}
|
||||
|
||||
createAdvancedDomain(domain) {
|
||||
this.blockNotification = true;
|
||||
|
||||
const groups = this._getGroups();
|
||||
const context = this._getContext();
|
||||
for (const group of groups) {
|
||||
const firstSearchItem = this.searchItems[group.activeItems[0].searchItemId];
|
||||
const { type } = firstSearchItem;
|
||||
for (const activeItem of group.activeItems) {
|
||||
if (type === "favorite") {
|
||||
const activeItemGroupBys = this._getSearchItemGroupBys(activeItem);
|
||||
for (const activeItemGroupBy of activeItemGroupBys) {
|
||||
const [fieldName, interval] = activeItemGroupBy.split(":");
|
||||
this.createNewGroupBy(fieldName, { interval, invisible: true });
|
||||
}
|
||||
const index = this.query.length - activeItemGroupBys.length;
|
||||
this.query = [...this.query.slice(index), ...this.query.slice(0, index)];
|
||||
}
|
||||
}
|
||||
if (
|
||||
[
|
||||
"comparison",
|
||||
"field",
|
||||
"field_property",
|
||||
"filter",
|
||||
"dateFilter",
|
||||
"favorite",
|
||||
].includes(type)
|
||||
) {
|
||||
this.deactivateGroup(group.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.blockNotification = false;
|
||||
this.createNewFilters([
|
||||
{
|
||||
description: `${this.env._t("Advanced Search")}`,
|
||||
domain,
|
||||
invisible: true,
|
||||
type: "filter",
|
||||
context,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new filter of type 'favorite' and activate it.
|
||||
* A new group containing only that filter is created.
|
||||
@@ -594,8 +644,11 @@ export class SearchModel extends EventBus {
|
||||
* Create a new filter of type 'groupBy' or 'dateGroupBy' and activate it.
|
||||
* It is added to the unique group of groupbys.
|
||||
* @param {string} fieldName
|
||||
* @param {Object} [param]
|
||||
* @param {string} [param.interval=DEFAULT_INTERVAL]
|
||||
* @param {boolean} [param.invisible=false]
|
||||
*/
|
||||
createNewGroupBy(fieldName) {
|
||||
createNewGroupBy(fieldName, { interval, invisible } = {}) {
|
||||
const field = this.searchViewFields[fieldName];
|
||||
const { string, type: fieldType } = field;
|
||||
const firstGroupBy = Object.values(this.searchItems).find((f) => f.type === "groupBy");
|
||||
@@ -608,9 +661,12 @@ export class SearchModel extends EventBus {
|
||||
id: this.nextId,
|
||||
custom: true,
|
||||
};
|
||||
if (invisible) {
|
||||
preSearchItem.invisible = true;
|
||||
}
|
||||
if (["date", "datetime"].includes(fieldType)) {
|
||||
this.searchItems[this.nextId] = Object.assign(
|
||||
{ type: "dateGroupBy", defaultIntervalId: DEFAULT_INTERVAL },
|
||||
{ type: "dateGroupBy", defaultIntervalId: interval || DEFAULT_INTERVAL },
|
||||
preSearchItem
|
||||
);
|
||||
this.toggleDateGroupBy(this.nextId);
|
||||
@@ -769,6 +825,22 @@ export class SearchModel extends EventBus {
|
||||
return sections.sort((s1, s2) => s1.index - s2.index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DomainRepr} domain
|
||||
* @returns {boolean}
|
||||
*/
|
||||
async isValidDomain(domain) {
|
||||
try {
|
||||
domain = new Domain(domain);
|
||||
domain = domain.toList(this.domainEvalContext);
|
||||
await this.orm.silent.searchRead(this.resModel, domain, ["id"], { limit: 1 });
|
||||
// TODO: invent a python method that check domain
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
search() {
|
||||
this.trigger("update");
|
||||
}
|
||||
@@ -1494,9 +1566,7 @@ export class SearchModel extends EventBus {
|
||||
let domain;
|
||||
try {
|
||||
domain = Domain.and(domains);
|
||||
return params.raw
|
||||
? domain
|
||||
: domain.toList(Object.assign({}, this.globalContext, this.userService.context));
|
||||
return params.raw ? domain : domain.toList(this.domainEvalContext);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`${this.env._t("Failed to evaluate the domain")} ${domain.toString()}.\n${
|
||||
@@ -1854,7 +1924,7 @@ export class SearchModel extends EventBus {
|
||||
*/
|
||||
_getOrderBy() {
|
||||
const groups = this._getGroups();
|
||||
let orderBy = [];
|
||||
const orderBy = [];
|
||||
for (const group of groups) {
|
||||
for (const activeItem of group.activeItems) {
|
||||
const { searchItemId } = activeItem;
|
||||
|
||||
@@ -137,10 +137,10 @@ export class DomainField extends Component {
|
||||
onEditDialogBtnClick() {
|
||||
this.addDialog(DomainSelectorDialog, {
|
||||
resModel: this.getResModel(this.props),
|
||||
initialValue: this.props.record.data[this.props.name] || "[]",
|
||||
domain: this.props.record.data[this.props.name] || "[]",
|
||||
readonly: this.props.readonly,
|
||||
isDebugMode: !!this.env.debug,
|
||||
onSelected: (value) => this.props.record.update({ [this.props.name]: value }),
|
||||
onConfirm: (value) => this.props.record.update({ [this.props.name]: value }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { applyFilter, toggleMenu } from "@web/../tests/search/helpers";
|
||||
import { DatePicker, DateTimePicker } from "@web/core/datepicker/datepicker";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { uiService } from "@web/core/ui/ui_service";
|
||||
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
|
||||
import ActionModel from "web.ActionModel";
|
||||
import CustomFilterItem from "web.CustomFilterItem";
|
||||
import { createComponent } from "web.test_utils";
|
||||
import { editSelect } from "web.test_utils_fields";
|
||||
import { registerCleanup } from "../helpers/cleanup";
|
||||
import { makeTestEnv } from "../helpers/mock_env";
|
||||
import { makeFakeLocalizationService } from "../helpers/mock_services";
|
||||
@@ -81,40 +76,43 @@ function useFRLocale() {
|
||||
}
|
||||
|
||||
var symbolMap = {
|
||||
'1': '૧',
|
||||
'2': '૨',
|
||||
'3': '૩',
|
||||
'4': '૪',
|
||||
'5': '૫',
|
||||
'6': '૬',
|
||||
'7': '૭',
|
||||
'8': '૮',
|
||||
'9': '૯',
|
||||
'0': '૦'
|
||||
1: "૧",
|
||||
2: "૨",
|
||||
3: "૩",
|
||||
4: "૪",
|
||||
5: "૫",
|
||||
6: "૬",
|
||||
7: "૭",
|
||||
8: "૮",
|
||||
9: "૯",
|
||||
0: "૦",
|
||||
};
|
||||
var numberMap = {
|
||||
'૧': '1',
|
||||
'૨': '2',
|
||||
'૩': '3',
|
||||
'૪': '4',
|
||||
'૫': '5',
|
||||
'૬': '6',
|
||||
'૭': '7',
|
||||
'૮': '8',
|
||||
'૯': '9',
|
||||
'૦': '0'
|
||||
"૧": "1",
|
||||
"૨": "2",
|
||||
"૩": "3",
|
||||
"૪": "4",
|
||||
"૫": "5",
|
||||
"૬": "6",
|
||||
"૭": "7",
|
||||
"૮": "8",
|
||||
"૯": "9",
|
||||
"૦": "0",
|
||||
};
|
||||
|
||||
function useGULocale() {
|
||||
if (!window.moment.locales().includes("gu")) {
|
||||
const originalLocale = window.moment.locale();
|
||||
window.moment.defineLocale("gu", {
|
||||
months: 'જાન્યુઆરી_ફેબ્રુઆરી_માર્ચ_એપ્રિલ_મે_જૂન_જુલાઈ_ઑગસ્ટ_સપ્ટેમ્બર_ઑક્ટ્બર_નવેમ્બર_ડિસેમ્બર'.split('_'),
|
||||
monthsShort: 'જાન્યુ._ફેબ્રુ._માર્ચ_એપ્રિ._મે_જૂન_જુલા._ઑગ._સપ્ટે._ઑક્ટ્._નવે._ડિસે.'.split('_'),
|
||||
months: "જાન્યુઆરી_ફેબ્રુઆરી_માર્ચ_એપ્રિલ_મે_જૂન_જુલાઈ_ઑગસ્ટ_સપ્ટેમ્બર_ઑક્ટ્બર_નવેમ્બર_ડિસેમ્બર".split(
|
||||
"_"
|
||||
),
|
||||
monthsShort:
|
||||
"જાન્યુ._ફેબ્રુ._માર્ચ_એપ્રિ._મે_જૂન_જુલા._ઑગ._સપ્ટે._ઑક્ટ્._નવે._ડિસે.".split("_"),
|
||||
monthsParseExact: true,
|
||||
week: {
|
||||
dow: 0, // Sunday is the first day of the week.
|
||||
doy: 6 // The week that contains Jan 1st is the first week of the year.
|
||||
doy: 6, // The week that contains Jan 1st is the first week of the year.
|
||||
},
|
||||
preparse: function (string) {
|
||||
return string.replace(/[૧૨૩૪૫૬૭૮૯૦]/g, function (match) {
|
||||
@@ -284,9 +282,9 @@ QUnit.module("Components", ({ beforeEach }) => {
|
||||
|
||||
await mountPicker(DatePicker, {
|
||||
date: DateTime.fromFormat("09/01/1997", "dd/MM/yyyy", {
|
||||
zone: "utc" ,
|
||||
zone: "utc",
|
||||
locale: useGULocale(),
|
||||
}),
|
||||
}),
|
||||
format: "dd MMM, yyyy",
|
||||
onDateTimeChanged: (date) => {
|
||||
assert.step("datetime-changed");
|
||||
@@ -623,7 +621,7 @@ QUnit.module("Components", ({ beforeEach }) => {
|
||||
date: DateTime.fromFormat("10/03/2023 13:14:27", "dd/MM/yyyy HH:mm:ss"),
|
||||
format: "dd.MM,yyyy",
|
||||
});
|
||||
let input = target.querySelector(".o_datepicker_input");
|
||||
const input = target.querySelector(".o_datepicker_input");
|
||||
|
||||
assert.strictEqual(input.value, "10.03,2023");
|
||||
|
||||
@@ -632,41 +630,6 @@ QUnit.module("Components", ({ beforeEach }) => {
|
||||
assert.strictEqual(input.value, "10.03,2023");
|
||||
});
|
||||
|
||||
QUnit.test("custom filter date", async function (assert) {
|
||||
assert.expect(3);
|
||||
class MockedSearchModel extends ActionModel {
|
||||
dispatch(method, ...args) {
|
||||
assert.strictEqual(method, "createNewFilters");
|
||||
const preFilters = args[0];
|
||||
const preFilter = preFilters[0];
|
||||
assert.strictEqual(
|
||||
preFilter.description,
|
||||
'A date is equal to "05/05/2005"',
|
||||
"description should be in localized format"
|
||||
);
|
||||
assert.deepEqual(
|
||||
preFilter.domain,
|
||||
'[["date_field","=","2005-05-05"]]',
|
||||
"domain should be in UTC format"
|
||||
);
|
||||
}
|
||||
}
|
||||
const searchModel = new MockedSearchModel();
|
||||
const date_field = { name: "date_field", string: "A date", type: "date", searchable: true };
|
||||
await createComponent(CustomFilterItem, {
|
||||
props: {
|
||||
fields: { date_field },
|
||||
},
|
||||
env: { searchModel },
|
||||
});
|
||||
await toggleMenu(target, "Add Custom Filter");
|
||||
await editSelect(target.querySelector(".o_generator_menu_field"), "date_field");
|
||||
const valueInput = target.querySelector(".o_generator_menu_value .o_input");
|
||||
await click(valueInput);
|
||||
await editSelect(valueInput, "05/05/2005");
|
||||
await applyFilter(target);
|
||||
});
|
||||
|
||||
QUnit.test("start with no value", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
|
||||
@@ -0,0 +1,659 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component, xml } from "@odoo/owl";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import {
|
||||
click,
|
||||
editInput,
|
||||
getFixture,
|
||||
getNodesTextContent,
|
||||
patchDate,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import {
|
||||
editFavoriteName,
|
||||
getFacetTexts,
|
||||
makeWithSearch,
|
||||
openAdvancedSearchDialog,
|
||||
saveFavorite,
|
||||
setupControlPanelFavoriteMenuRegistry,
|
||||
setupControlPanelServiceRegistry,
|
||||
toggleComparisonMenu,
|
||||
toggleFavoriteMenu,
|
||||
toggleFilterMenu,
|
||||
toggleGroupByMenu,
|
||||
toggleMenuItem,
|
||||
toggleMenuItemOption,
|
||||
toggleSaveFavorite,
|
||||
} from "./helpers";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { fieldService } from "@web/core/field_service";
|
||||
|
||||
async function makeTestComponent(props) {
|
||||
class TestComponent extends Component {
|
||||
setup() {
|
||||
this.id = 1; // used for t-key. We want to be sure that the control panel is rendered.
|
||||
}
|
||||
}
|
||||
TestComponent.components = { ControlPanel, MainComponentsContainer };
|
||||
TestComponent.template = xml`
|
||||
<div class="o_test_component">
|
||||
<MainComponentsContainer/>
|
||||
<ControlPanel t-key="id++"/>
|
||||
</div>
|
||||
`;
|
||||
return makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: TestComponent,
|
||||
searchViewId: 1,
|
||||
searchMenuTypes: ["filter", "groupBy", "comparison", "favorite"],
|
||||
...props,
|
||||
});
|
||||
}
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
|
||||
QUnit.module("Search", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
fields: {
|
||||
date_field: {
|
||||
name: "date_field",
|
||||
string: "A date",
|
||||
type: "date",
|
||||
searchable: true,
|
||||
},
|
||||
date_time_field: {
|
||||
name: "date_time_field",
|
||||
string: "DateTime",
|
||||
type: "datetime",
|
||||
searchable: true,
|
||||
},
|
||||
boolean_field: {
|
||||
name: "boolean_field",
|
||||
string: "Boolean Field",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
searchable: true,
|
||||
},
|
||||
binary_field: {
|
||||
name: "binary_field",
|
||||
string: "Binary Field",
|
||||
type: "binary",
|
||||
searchable: true,
|
||||
},
|
||||
char_field: {
|
||||
name: "char_field",
|
||||
string: "Char Field",
|
||||
type: "char",
|
||||
default: "foo",
|
||||
trim: true,
|
||||
searchable: true,
|
||||
},
|
||||
float_field: {
|
||||
name: "float_field",
|
||||
string: "Floaty McFloatface",
|
||||
type: "float",
|
||||
searchable: true,
|
||||
},
|
||||
color: {
|
||||
name: "color",
|
||||
string: "Color",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["black", "Black"],
|
||||
["white", "White"],
|
||||
],
|
||||
searchable: true,
|
||||
},
|
||||
user_id: {
|
||||
string: "User",
|
||||
type: "many2one",
|
||||
relation: "user",
|
||||
store: true,
|
||||
sortable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
fields: {
|
||||
id: { string: "Id", type: "integer" },
|
||||
name: { string: "Product Name", type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 37,
|
||||
display_name: "Me",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
views: {
|
||||
"foo,1,search": `
|
||||
<search>
|
||||
<field name="char_field"/>
|
||||
<filter name="simple_filter" string="Simple Filter" domain="[('float_field', '=', 2)]"/>
|
||||
<filter name="dynamic_filter" string="Dynamic Filter" domain="[('user_id', '=', uid)]"/>
|
||||
<filter name="date_filter" string="Date Filter" date="date_field"/>
|
||||
<filter name="simple_group_by" string="Simple GroupBy" context="{'group_by': 'boolean_field'}"/>
|
||||
<filter name="date_group_by" string="Date GroupBy" context="{'group_by': 'date_time_field'}"/>
|
||||
</search>
|
||||
`,
|
||||
},
|
||||
};
|
||||
setupControlPanelServiceRegistry();
|
||||
setupControlPanelFavoriteMenuRegistry();
|
||||
registry.category("services").add("popover", popoverService);
|
||||
registry.category("services").add("field", fieldService);
|
||||
target = getFixture();
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.module("AdvancedSearchDialog");
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
await makeTestComponent();
|
||||
await toggleFilterMenu(target);
|
||||
assert.containsNone(target, ".modal");
|
||||
|
||||
await openAdvancedSearchDialog(target);
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsOnce(target, ".modal .modal-header h4");
|
||||
assert.strictEqual(target.querySelector(".modal header h4").innerText, "Advanced Search");
|
||||
assert.containsN(target, ".modal footer button", 2);
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".modal footer button")), [
|
||||
"Search",
|
||||
"Discard",
|
||||
]);
|
||||
|
||||
await click(target, ".modal footer button:nth-child(2)");
|
||||
assert.containsNone(target, ".modal");
|
||||
assert.containsOnce(target, ".o_filter_menu .dropdown-menu");
|
||||
|
||||
await openAdvancedSearchDialog(target);
|
||||
assert.containsOnce(target, ".modal");
|
||||
|
||||
await click(target, ".modal footer button:nth-child(1)");
|
||||
assert.containsNone(target, ".modal");
|
||||
assert.containsNone(target, ".o_filter_menu .dropdown-menu");
|
||||
});
|
||||
|
||||
QUnit.test("start with an empty query", async function (assert) {
|
||||
await makeTestComponent();
|
||||
await toggleFilterMenu(target);
|
||||
await openAdvancedSearchDialog(target);
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsOnce(target, ".modal .modal-body .o_domain_selector");
|
||||
assert.containsOnce(target, ".modal .modal-body .o_domain_selector span");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".modal .modal-body .o_domain_selector span").innerText,
|
||||
"Match all records"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".modal .modal-body .o_domain_selector button.o_domain_add_first_node_button"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("start with an empty query and a global domain", async function (assert) {
|
||||
await makeTestComponent({ domain: [[0, "=", 1]] });
|
||||
await toggleFilterMenu(target);
|
||||
await openAdvancedSearchDialog(target);
|
||||
// the global domain should not be part of the domain passed to domain selector
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsOnce(target, ".modal .modal-body .o_domain_selector");
|
||||
assert.containsOnce(target, ".modal .modal-body .o_domain_selector span");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".modal .modal-body .o_domain_selector span").innerText,
|
||||
"Match all records"
|
||||
);
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".modal .modal-body .o_domain_selector button.o_domain_add_first_node_button"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("start with a simple domain and modify it", async function (assert) {
|
||||
const testComponent = await makeTestComponent({
|
||||
context: { search_default_simple_filter: true },
|
||||
});
|
||||
await toggleFilterMenu(target);
|
||||
assert.deepEqual(getFacetTexts(target), ["Simple Filter"]);
|
||||
assert.containsNone(target, ".o_domain_selector");
|
||||
|
||||
await openAdvancedSearchDialog(target);
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.containsOnce(target, ".modal .modal-body .o_domain_selector");
|
||||
assert.containsOnce(target, ".o_field_selector");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_field_selector").innerText,
|
||||
"Floaty McFloatface"
|
||||
);
|
||||
assert.containsOnce(target, ".o_domain_leaf_operator_select");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "equal");
|
||||
assert.containsOnce(target, ".o_domain_leaf_value_input");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, "2");
|
||||
|
||||
await editInput(target, ".o_domain_leaf_value_input", 4);
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, "4");
|
||||
assert.deepEqual(testComponent.props.domain, [["float_field", "=", 2]]);
|
||||
|
||||
await click(target, ".modal footer button:nth-child(1)");
|
||||
assert.deepEqual(getFacetTexts(target), ["Advanced Search"]);
|
||||
assert.deepEqual(testComponent.props.domain, [["float_field", "=", 4]]);
|
||||
});
|
||||
|
||||
QUnit.test("start with a dynamic domain (evaluation is done)", async function (assert) {
|
||||
const testComponent = await makeTestComponent({
|
||||
context: {
|
||||
search_default_dynamic_filter: true,
|
||||
},
|
||||
mockRPC(_, { args, method }) {
|
||||
if (method === "create_or_replace") {
|
||||
assert.strictEqual(args[0].domain, `[("user_id", "=", 4)]`); // the domain is not saved dynamically
|
||||
return 9; // fake serverId to simulate the creation of
|
||||
// the favorite in db.
|
||||
}
|
||||
},
|
||||
});
|
||||
assert.deepEqual(getFacetTexts(target), ["Dynamic Filter"]);
|
||||
assert.deepEqual(testComponent.props.domain, [["user_id", "=", 7]]);
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await openAdvancedSearchDialog(target);
|
||||
|
||||
assert.strictEqual(target.querySelector(".o_field_selector").innerText, "User");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_operator_select").value, "equal");
|
||||
assert.strictEqual(target.querySelector(".o_ds_value_cell input").value, "7");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"start with a simple domain and a simple group by, group by not lost after search",
|
||||
async function (assert) {
|
||||
const testComponent = await makeTestComponent({
|
||||
context: {
|
||||
search_default_simple_filter: true,
|
||||
search_default_simple_group_by: true,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(getFacetTexts(target), ["Simple Filter", "Simple GroupBy"]);
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await openAdvancedSearchDialog(target);
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, "2");
|
||||
assert.deepEqual(testComponent.props.domain, [["float_field", "=", 2]]);
|
||||
assert.deepEqual(testComponent.props.groupBy, ["boolean_field"]);
|
||||
|
||||
await editInput(target, ".o_domain_leaf_value_input", 4);
|
||||
await click(target, ".modal footer button:nth-child(1)");
|
||||
assert.deepEqual(getFacetTexts(target), ["Simple GroupBy", "Advanced Search"]);
|
||||
assert.deepEqual(testComponent.props.domain, [["float_field", "=", 4]]);
|
||||
assert.deepEqual(testComponent.props.groupBy, ["boolean_field"]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("start with a favorite only", async function (assert) {
|
||||
const irFilters = [
|
||||
{
|
||||
context: "{ 'some_key': 'some_value', 'group_by': ['color'] }",
|
||||
domain: "[('char_field', 'ilike', 'abc')]",
|
||||
id: 1,
|
||||
is_default: true,
|
||||
name: "My favorite",
|
||||
sort: "[]",
|
||||
user_id: [2, "Mitchell Admin"],
|
||||
},
|
||||
];
|
||||
const testComponent = await makeTestComponent({
|
||||
irFilters,
|
||||
});
|
||||
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
|
||||
assert.deepEqual(testComponent.props.domain, [["char_field", "ilike", "abc"]]);
|
||||
assert.deepEqual(testComponent.props.groupBy, ["color"]);
|
||||
assert.strictEqual(testComponent.props.context.some_key, "some_value");
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await openAdvancedSearchDialog(target);
|
||||
await editInput(target.querySelector(".o_domain_leaf_value_input"), null, "xyz");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, "xyz");
|
||||
|
||||
await click(target, ".modal footer button:nth-child(1)");
|
||||
assert.deepEqual(getFacetTexts(target), ["Color", "Advanced Search"]);
|
||||
assert.deepEqual(testComponent.props.domain, [["char_field", "ilike", "xyz"]]);
|
||||
assert.deepEqual(testComponent.props.groupBy, ["color"]);
|
||||
assert.strictEqual(testComponent.props.context.some_key, "some_value");
|
||||
});
|
||||
|
||||
QUnit.test("group_bys retrieved from favorite", async function (assert) {
|
||||
const irFilters = [
|
||||
{
|
||||
context: "{ 'some_key': 'some_value', 'group_by': ['color'] }",
|
||||
domain: "[('char_field', 'ilike', 'abc')]",
|
||||
id: 1,
|
||||
is_default: true,
|
||||
name: "My favorite",
|
||||
sort: "[]",
|
||||
user_id: [2, "Mitchell Admin"],
|
||||
},
|
||||
];
|
||||
const testComponent = await makeTestComponent({
|
||||
irFilters,
|
||||
});
|
||||
|
||||
await toggleGroupByMenu(target);
|
||||
await toggleMenuItem(target, "Simple GroupBy");
|
||||
assert.deepEqual(getFacetTexts(target), ["My favorite", "Simple GroupBy"]);
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleMenuItem(target, "Simple Filter");
|
||||
assert.deepEqual(getFacetTexts(target), ["My favorite", "Simple GroupBy", "Simple Filter"]);
|
||||
assert.deepEqual(testComponent.props.domain, [
|
||||
"&",
|
||||
["char_field", "ilike", "abc"],
|
||||
["float_field", "=", 2],
|
||||
]);
|
||||
assert.deepEqual(testComponent.props.groupBy, ["color", "boolean_field"]);
|
||||
assert.strictEqual(testComponent.props.context.some_key, "some_value");
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await openAdvancedSearchDialog(target);
|
||||
await editInput(target.querySelector(".o_domain_leaf_value_input"), null, "xyz");
|
||||
assert.strictEqual(target.querySelector(".o_domain_leaf_value_input").value, "xyz");
|
||||
|
||||
await click(target, ".modal footer button:nth-child(1)");
|
||||
assert.deepEqual(getFacetTexts(target), ["Color>Simple GroupBy", "Advanced Search"]);
|
||||
assert.deepEqual(testComponent.props.domain, [
|
||||
"&",
|
||||
["char_field", "ilike", "xyz"],
|
||||
["float_field", "=", 2],
|
||||
]);
|
||||
assert.deepEqual(testComponent.props.groupBy, ["color", "boolean_field"]);
|
||||
assert.strictEqual(testComponent.props.context.some_key, "some_value");
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"comparison retrieved from favorite (comparison in searchMenuTypes)",
|
||||
async function (assert) {
|
||||
patchDate(2023, 2, 1, 14, 0, 0);
|
||||
const testComponent = await makeTestComponent({
|
||||
async mockRPC(_, { method }) {
|
||||
if (method === "create_or_replace") {
|
||||
return 9;
|
||||
}
|
||||
},
|
||||
});
|
||||
await toggleFilterMenu(target);
|
||||
await toggleMenuItem(target, "Date Filter");
|
||||
await toggleMenuItemOption(target, "Date Filter", "March");
|
||||
|
||||
await toggleComparisonMenu(target);
|
||||
await toggleMenuItem(target, "Date Filter: Previous Period");
|
||||
|
||||
await toggleFavoriteMenu(target);
|
||||
await toggleSaveFavorite(target);
|
||||
await editFavoriteName(target, "My new favorite");
|
||||
await saveFavorite(target, "My new favorite");
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ["My new favorite"]);
|
||||
assert.deepEqual(testComponent.props.domain, []);
|
||||
assert.deepEqual(testComponent.props.comparison, {
|
||||
domains: [
|
||||
{
|
||||
arrayRepr: [
|
||||
"&",
|
||||
["date_field", ">=", "2023-03-01"],
|
||||
["date_field", "<=", "2023-03-31"],
|
||||
],
|
||||
description: "March 2023",
|
||||
},
|
||||
{
|
||||
arrayRepr: [
|
||||
"&",
|
||||
["date_field", ">=", "2023-02-01"],
|
||||
["date_field", "<=", "2023-02-28"],
|
||||
],
|
||||
description: "February 2023",
|
||||
},
|
||||
],
|
||||
fieldName: "date_field",
|
||||
});
|
||||
assert.deepEqual(testComponent.props.groupBy, []);
|
||||
assert.strictEqual(testComponent.props.context.comparison, undefined);
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await openAdvancedSearchDialog(target);
|
||||
await click(target, ".modal footer button:nth-child(1)");
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ["Advanced Search"]);
|
||||
assert.deepEqual(testComponent.props.domain, [
|
||||
"&",
|
||||
["date_field", ">=", "2023-03-01"],
|
||||
["date_field", "<=", "2023-03-31"],
|
||||
]);
|
||||
assert.deepEqual(testComponent.props.groupBy, []);
|
||||
assert.strictEqual(testComponent.props.context.comparison, undefined);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"comparison retrieved from favorite (comparison not in searchMenuTypes)",
|
||||
async function (assert) {
|
||||
const testComponent = await makeTestComponent({
|
||||
irFilters: [
|
||||
{
|
||||
context: `{
|
||||
'group_by': [],
|
||||
'comparison': {
|
||||
'comparisonId': 'previous_period',
|
||||
'fieldName': 'create_date',
|
||||
'fieldDescription':'Creation Date',
|
||||
'range': ['&', ['create_date', '>=', '2023-02-28 23:00:00'], ['create_date', '<=', '2023-03-31 21:59:59']],
|
||||
'rangeDescription': 'March 2023',
|
||||
'comparisonRange': ['&', ['create_date', '>=', '2023-01-31 23:00:00'], ['create_date', '<=', '2023-02-28 22:59:59']],
|
||||
'comparisonRangeDescription': 'February 2023'}
|
||||
}`,
|
||||
domain: "[]",
|
||||
id: 1,
|
||||
is_default: true,
|
||||
name: "My favorite",
|
||||
sort: "[]",
|
||||
user_id: [2, "Mitchell Admin"],
|
||||
},
|
||||
],
|
||||
searchMenuTypes: ["filter", "favorite"],
|
||||
});
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
|
||||
assert.deepEqual(testComponent.props.domain, []); // a problem with comparison?
|
||||
assert.strictEqual(testComponent.props.comparison, null);
|
||||
assert.deepEqual(testComponent.props.groupBy, []);
|
||||
assert.strictEqual(testComponent.props.context.comparison, undefined);
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await openAdvancedSearchDialog(target);
|
||||
await click(target, ".modal footer button:nth-child(1)");
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ["Advanced Search"]);
|
||||
assert.deepEqual(testComponent.props.domain, []);
|
||||
assert.deepEqual(testComponent.props.groupBy, []);
|
||||
assert.strictEqual(testComponent.props.context.comparison, undefined);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("search items created for advanced search are invisible", async function (assert) {
|
||||
const irFilters = [
|
||||
{
|
||||
context: "{ 'group_by': ['color'] }",
|
||||
domain: "[]]",
|
||||
id: 1,
|
||||
is_default: true,
|
||||
name: "My favorite",
|
||||
sort: "[]",
|
||||
user_id: [2, "Mitchell Admin"],
|
||||
},
|
||||
];
|
||||
const testComponent = await makeTestComponent({
|
||||
irFilters,
|
||||
});
|
||||
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
|
||||
assert.deepEqual(testComponent.props.domain, []);
|
||||
assert.deepEqual(testComponent.props.groupBy, ["color"]);
|
||||
|
||||
await toggleGroupByMenu(target);
|
||||
const visibleGroupBys = getNodesTextContent(
|
||||
target.querySelectorAll(".o_group_by_menu .dropdown-item")
|
||||
);
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
const visibleFilters = getNodesTextContent(
|
||||
target.querySelectorAll(".o_filter_menu .dropdown-item")
|
||||
);
|
||||
await openAdvancedSearchDialog(target);
|
||||
await click(target, ".modal footer button:nth-child(1)");
|
||||
assert.deepEqual(getFacetTexts(target), ["Color", "Advanced Search"]);
|
||||
assert.deepEqual(testComponent.props.domain, []);
|
||||
assert.deepEqual(testComponent.props.groupBy, ["color"]);
|
||||
|
||||
await toggleGroupByMenu(target);
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_group_by_menu .dropdown-item")),
|
||||
visibleGroupBys
|
||||
);
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_filter_menu .dropdown-item")),
|
||||
visibleFilters
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Several date filters and a comparison", async function (assert) {
|
||||
serverData.views["foo,1,search"] = `
|
||||
<search>
|
||||
<filter name="date_filter" string="Date" date="date_field"/>
|
||||
<filter name="date_time_filter" string="Datetime" date="date_time_field"/>
|
||||
</search>
|
||||
`;
|
||||
const testComponent = await makeTestComponent({
|
||||
domain: [[0, "=", 1]],
|
||||
context: {
|
||||
search_default_date_filter: true,
|
||||
search_default_date_time_filter: true,
|
||||
},
|
||||
});
|
||||
await toggleComparisonMenu(target);
|
||||
await toggleMenuItem(target, "Date: Previous Period");
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), [
|
||||
"Date: April 2023orDatetime: April 2023",
|
||||
"Date: Previous Period",
|
||||
]);
|
||||
assert.deepEqual(testComponent.props.domain, [
|
||||
"&",
|
||||
[0, "=", 1],
|
||||
"&",
|
||||
["date_time_field", ">=", "2023-03-31 23:00:00"],
|
||||
["date_time_field", "<=", "2023-04-30 22:59:59"],
|
||||
]);
|
||||
assert.deepEqual(testComponent.props.comparison, {
|
||||
domains: [
|
||||
{
|
||||
arrayRepr: [
|
||||
"&",
|
||||
"&",
|
||||
[0, "=", 1],
|
||||
"&",
|
||||
["date_time_field", ">=", "2023-03-31 23:00:00"],
|
||||
["date_time_field", "<=", "2023-04-30 22:59:59"],
|
||||
"&",
|
||||
["date_field", ">=", "2023-04-01"],
|
||||
["date_field", "<=", "2023-04-30"],
|
||||
],
|
||||
description: "April 2023",
|
||||
},
|
||||
{
|
||||
arrayRepr: [
|
||||
"&",
|
||||
"&",
|
||||
[0, "=", 1],
|
||||
"&",
|
||||
["date_time_field", ">=", "2023-03-31 23:00:00"],
|
||||
["date_time_field", "<=", "2023-04-30 22:59:59"],
|
||||
"&",
|
||||
["date_field", ">=", "2023-03-01"],
|
||||
["date_field", "<=", "2023-03-31"],
|
||||
],
|
||||
description: "March 2023",
|
||||
},
|
||||
],
|
||||
fieldName: "date_field",
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await openAdvancedSearchDialog(target);
|
||||
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_field_selector_chain_part")),
|
||||
["DateTime", "DateTime", "A date", "A date"]
|
||||
); // 0 not found!
|
||||
|
||||
await click(target, ".modal footer button:nth-child(1)");
|
||||
|
||||
assert.deepEqual(testComponent.props.domain, [
|
||||
"&",
|
||||
[0, "=", 1],
|
||||
"&",
|
||||
"&",
|
||||
["date_time_field", ">=", "2023-03-31 23:00:00"],
|
||||
["date_time_field", "<=", "2023-04-30 22:59:59"],
|
||||
"&",
|
||||
["date_field", ">=", "2023-04-01"],
|
||||
["date_field", "<=", "2023-04-30"],
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("an invalid domain cannot be saved", async function (assert) {
|
||||
assert.expect(9);
|
||||
patchWithCleanup(odoo, {
|
||||
debug: true,
|
||||
});
|
||||
registry.category("services").add(
|
||||
"notification",
|
||||
{
|
||||
start() {
|
||||
return {
|
||||
add(message, options) {
|
||||
assert.strictEqual(message, "Domain is invalid. Please correct it");
|
||||
assert.deepEqual(options, { type: "danger" });
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
const testComponent = await makeTestComponent({});
|
||||
assert.deepEqual(testComponent.props.domain, []);
|
||||
assert.deepEqual(getFacetTexts(target), []);
|
||||
assert.containsNone(target, ".modal");
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await openAdvancedSearchDialog(target);
|
||||
assert.containsOnce(target, ".modal");
|
||||
|
||||
await editInput(target, "textarea.o_domain_debug_input", "[");
|
||||
await click(target, ".modal footer button:nth-child(1)");
|
||||
assert.containsOnce(target, ".modal");
|
||||
assert.deepEqual(testComponent.props.domain, []);
|
||||
assert.deepEqual(getFacetTexts(target), []);
|
||||
});
|
||||
});
|
||||
@@ -1,777 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
nextTick,
|
||||
patchDate,
|
||||
patchTimeZone,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import {
|
||||
addCondition,
|
||||
applyFilter,
|
||||
editConditionField,
|
||||
editConditionOperator,
|
||||
editConditionValue,
|
||||
getFacetTexts,
|
||||
isItemSelected,
|
||||
makeWithSearch,
|
||||
removeFacet,
|
||||
setupControlPanelServiceRegistry,
|
||||
toggleAddCustomFilter,
|
||||
toggleFilterMenu,
|
||||
toggleMenuItem,
|
||||
} from "./helpers";
|
||||
|
||||
function getDomain(controlPanel) {
|
||||
return controlPanel.env.searchModel.domain;
|
||||
}
|
||||
|
||||
let target;
|
||||
let serverData;
|
||||
QUnit.module("Search", (hooks) => {
|
||||
hooks.beforeEach(async () => {
|
||||
serverData = {
|
||||
models: {
|
||||
foo: {
|
||||
fields: {
|
||||
date_field: {
|
||||
name: "date_field",
|
||||
string: "A date",
|
||||
type: "date",
|
||||
searchable: true,
|
||||
},
|
||||
date_time_field: {
|
||||
name: "date_time_field",
|
||||
string: "DateTime",
|
||||
type: "datetime",
|
||||
searchable: true,
|
||||
},
|
||||
boolean_field: {
|
||||
name: "boolean_field",
|
||||
string: "Boolean Field",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
searchable: true,
|
||||
},
|
||||
char_field: {
|
||||
name: "char_field",
|
||||
string: "Char Field",
|
||||
type: "char",
|
||||
default: "foo",
|
||||
trim: true,
|
||||
searchable: true,
|
||||
},
|
||||
float_field: {
|
||||
name: "float_field",
|
||||
string: "Floaty McFloatface",
|
||||
type: "float",
|
||||
digits: [999, 1],
|
||||
searchable: true,
|
||||
},
|
||||
color: {
|
||||
name: "color",
|
||||
string: "Color",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["black", "Black"],
|
||||
["white", "White"],
|
||||
],
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
records: {},
|
||||
},
|
||||
},
|
||||
views: {
|
||||
"foo,false,search": `<search/>`,
|
||||
},
|
||||
};
|
||||
setupControlPanelServiceRegistry();
|
||||
patchWithCleanup(browser, {
|
||||
setTimeout: (fn) => fn(),
|
||||
clearTimeout: () => {},
|
||||
});
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.module("CustomFilterItem");
|
||||
|
||||
QUnit.test("basic rendering", async function (assert) {
|
||||
assert.expect(14);
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
|
||||
const customFilterItem = target.querySelector(".o_add_custom_filter_menu");
|
||||
|
||||
assert.strictEqual(customFilterItem.innerText.trim(), "Add Custom Filter");
|
||||
|
||||
await toggleAddCustomFilter(target);
|
||||
|
||||
// Single condition
|
||||
assert.containsOnce(customFilterItem, ".o_filter_condition");
|
||||
assert.containsOnce(
|
||||
customFilterItem,
|
||||
".o_filter_condition > select.o_generator_menu_field"
|
||||
);
|
||||
assert.containsOnce(
|
||||
customFilterItem,
|
||||
".o_filter_condition > select.o_generator_menu_operator"
|
||||
);
|
||||
assert.containsOnce(customFilterItem, ".o_filter_condition > span.o_generator_menu_value");
|
||||
assert.containsNone(customFilterItem, ".o_filter_condition .o_or_filter");
|
||||
assert.containsNone(customFilterItem, ".o_filter_condition .o_generator_menu_delete");
|
||||
|
||||
// no deletion allowed on single condition
|
||||
assert.containsNone(customFilterItem, ".o_filter_condition > i.o_generator_menu_delete");
|
||||
|
||||
// Buttons
|
||||
assert.containsOnce(customFilterItem, "button.o_apply_filter");
|
||||
assert.containsOnce(customFilterItem, "button.o_add_condition");
|
||||
|
||||
assert.containsOnce(customFilterItem, ".o_filter_condition");
|
||||
|
||||
await click(customFilterItem, "button.o_add_condition");
|
||||
|
||||
assert.containsN(customFilterItem, ".o_filter_condition", 2);
|
||||
assert.containsOnce(customFilterItem, ".o_filter_condition .o_or_filter");
|
||||
assert.containsN(customFilterItem, ".o_filter_condition .o_generator_menu_delete", 2);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
'should have Date and ID field proposed in that order in "Add custom Filter" submenu',
|
||||
async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
searchViewFields: {
|
||||
date_field: {
|
||||
name: "date_field",
|
||||
string: "Date",
|
||||
type: "date",
|
||||
store: true,
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
foo: { string: "Foo", type: "char", store: true, sortable: true },
|
||||
},
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
const optionEls = target.querySelectorAll(
|
||||
".o_filter_condition > select.o_generator_menu_field option"
|
||||
);
|
||||
assert.strictEqual(optionEls[0].innerText.trim(), "Date");
|
||||
assert.strictEqual(optionEls[1].innerText.trim(), "ID");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("deactivate a new custom filter works", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
patchDate(2020, 1, 5, 12, 20, 0);
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
searchViewFields: {
|
||||
date_field: {
|
||||
name: "date_field",
|
||||
string: "Date",
|
||||
type: "date",
|
||||
store: true,
|
||||
sortable: true,
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
await applyFilter(target);
|
||||
|
||||
assert.ok(isItemSelected(target, 'Date is equal to "02/05/2020"'));
|
||||
assert.deepEqual(getFacetTexts(target), ['Date is equal to "02/05/2020"']);
|
||||
|
||||
await toggleMenuItem(target, 'Date is equal to "02/05/2020"');
|
||||
|
||||
assert.notOk(isItemSelected(target, 'Date is equal to "02/05/2020"'));
|
||||
assert.deepEqual(getFacetTexts(target), []);
|
||||
});
|
||||
|
||||
QUnit.test("custom OR filter presets new condition from preceding", async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
|
||||
// Retrieve second selectable values for field and operator dropdowns
|
||||
const secondFieldName = target.querySelector(
|
||||
".o_generator_menu_field option:nth-of-type(2)"
|
||||
).value;
|
||||
const secondOperator = target.querySelector(
|
||||
".o_generator_menu_operator option:nth-of-type(2)"
|
||||
).value;
|
||||
|
||||
// Check if they really exist…
|
||||
assert.ok(!!secondFieldName);
|
||||
assert.ok(!!secondOperator);
|
||||
|
||||
// Add first filter condition
|
||||
await editConditionField(target, 0, secondFieldName);
|
||||
await editConditionOperator(target, 0, secondOperator);
|
||||
|
||||
// Add a second conditon on the filter being created
|
||||
await addCondition(target);
|
||||
|
||||
// Check the defaults for field and operator dropdowns
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_filter_condition:nth-of-type(2) .o_generator_menu_field")
|
||||
.value,
|
||||
secondFieldName
|
||||
);
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_filter_condition:nth-of-type(2) .o_generator_menu_operator")
|
||||
.value,
|
||||
secondOperator
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("add a custom filter works", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const controlPanel = await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
searchViewFields: {},
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
// choose ID field in 'Add Custom filter' menu and value 1
|
||||
await editConditionField(target, 0, "id");
|
||||
await editConditionValue(target, 0, 1);
|
||||
|
||||
await applyFilter(target);
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ['ID is "1"']);
|
||||
assert.deepEqual(getDomain(controlPanel), [["id", "=", 1]]);
|
||||
});
|
||||
|
||||
QUnit.test("adding a simple filter works", async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
const controlPanel = await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
searchViewFields: {
|
||||
boolean_field: {
|
||||
name: "boolean_field",
|
||||
string: "Boolean Field",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), []);
|
||||
assert.deepEqual(getDomain(controlPanel), []);
|
||||
|
||||
assert.containsNone(target, ".o_menu_item");
|
||||
assert.containsOnce(target, ".o_add_custom_filter_menu button.dropdown-toggle");
|
||||
// the 'Add Custom Filter' menu should be closed;
|
||||
assert.containsNone(target, ".o_add_custom_filter_menu .dropdown-menu");
|
||||
|
||||
await toggleAddCustomFilter(target);
|
||||
// the 'Add Custom Filter' menu should be open;
|
||||
assert.containsOnce(target, ".o_add_custom_filter_menu .dropdown-menu");
|
||||
|
||||
await applyFilter(target);
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ["Boolean Field is Yes"]);
|
||||
assert.deepEqual(getDomain(controlPanel), [["boolean_field", "=", true]]);
|
||||
|
||||
assert.containsOnce(target, ".o_menu_item");
|
||||
assert.containsOnce(target, ".o_add_custom_filter_menu button.dropdown-toggle");
|
||||
// the 'Add Custom Filter' menu should still be opened;
|
||||
assert.containsOnce(target, ".o_add_custom_filter_menu .dropdown-menu");
|
||||
});
|
||||
|
||||
QUnit.test("selection field: default and updated value", async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
const controlPanel = await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
|
||||
// Default value
|
||||
await toggleFilterMenu(target);
|
||||
|
||||
assert.containsN(target, ".o_menu_item", 0);
|
||||
assert.containsN(target, ".dropdown-divider", 0);
|
||||
|
||||
await toggleAddCustomFilter(target);
|
||||
await editConditionField(target, 0, "color");
|
||||
await applyFilter(target);
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ['Color is "black"']);
|
||||
assert.deepEqual(getDomain(controlPanel), [["color", "=", "black"]]);
|
||||
|
||||
assert.containsN(target, ".o_menu_item", 1);
|
||||
assert.containsN(target, ".dropdown-divider", 1);
|
||||
|
||||
// deactivate custom filter
|
||||
await removeFacet(target);
|
||||
|
||||
// Updated value
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
await editConditionField(target, 0, "color");
|
||||
await editConditionValue(target, 0, "white");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_generator_menu_value input,.o_generator_menu_value select")
|
||||
.value,
|
||||
"white"
|
||||
);
|
||||
await applyFilter(target);
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ['Color is "white"']);
|
||||
assert.deepEqual(getDomain(controlPanel), [["color", "=", "white"]]);
|
||||
|
||||
assert.containsN(target, ".o_menu_item", 2);
|
||||
assert.containsN(target, ".dropdown-divider", 2);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"commit search with an extended proposition with field char does not cause a crash",
|
||||
async function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
const controlPanel = await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
searchViewFields: {
|
||||
many2one_field: {
|
||||
name: "many2one_field",
|
||||
string: "AAA",
|
||||
type: "many2one",
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const steps = [
|
||||
{
|
||||
value: `a`,
|
||||
domain: [["many2one_field", "ilike", `a`]],
|
||||
facetContent: `AAA contains "a"`,
|
||||
},
|
||||
{
|
||||
value: `"a"`,
|
||||
domain: [["many2one_field", "ilike", `"a"`]],
|
||||
facetContent: `AAA contains ""a""`,
|
||||
},
|
||||
{
|
||||
value: `'a'`,
|
||||
domain: [["many2one_field", "ilike", `'a'`]],
|
||||
facetContent: `AAA contains "'a'"`,
|
||||
},
|
||||
{
|
||||
value: `'`,
|
||||
domain: [["many2one_field", "ilike", `'`]],
|
||||
facetContent: `AAA contains "'"`,
|
||||
},
|
||||
{
|
||||
value: `"`,
|
||||
domain: [["many2one_field", "ilike", `"`]],
|
||||
facetContent: `AAA contains """`,
|
||||
},
|
||||
{
|
||||
value: `\\`,
|
||||
domain: [["many2one_field", "ilike", `\\`]],
|
||||
facetContent: `AAA contains "\\"`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const step of steps) {
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
await editConditionValue(target, 0, step.value);
|
||||
await applyFilter(target);
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), [step.facetContent]);
|
||||
assert.deepEqual(getDomain(controlPanel), step.domain);
|
||||
|
||||
await removeFacet(target);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("custom filter date with equal operator", async function (assert) {
|
||||
patchTimeZone(-240);
|
||||
patchDate(2017, 1, 22, 12, 30, 0);
|
||||
|
||||
const controlPanel = await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
|
||||
await editConditionField(target, 0, "date_field");
|
||||
await editConditionOperator(target, 0, "=");
|
||||
await editConditionValue(target, 0, "01/01/2017");
|
||||
await applyFilter(target);
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), ['A date is equal to "01/01/2017"']);
|
||||
assert.deepEqual(getDomain(controlPanel), [["date_field", "=", "2017-01-01"]]);
|
||||
});
|
||||
|
||||
QUnit.test("custom filter datetime with equal operator", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
patchTimeZone(-240);
|
||||
patchDate(2017, 1, 22, 12, 30, 0);
|
||||
|
||||
const controlPanel = await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
|
||||
await editConditionField(target, 0, "date_time_field");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_generator_menu_field").value,
|
||||
"date_time_field"
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".o_generator_menu_operator").value, "between");
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_generator_menu_value input")].map((v) => v.value),
|
||||
["02/22/2017 00:00:00", "02/22/2017 23:59:59"]
|
||||
);
|
||||
|
||||
await editConditionOperator(target, 0, "=");
|
||||
await editConditionValue(target, 0, "02/22/2017 11:00:00"); // in TZ
|
||||
await applyFilter(target);
|
||||
|
||||
assert.deepEqual(
|
||||
getFacetTexts(target),
|
||||
['DateTime is equal to "02/22/2017 11:00:00"'],
|
||||
"description should be in localized format"
|
||||
);
|
||||
assert.deepEqual(
|
||||
getDomain(controlPanel),
|
||||
[["date_time_field", "=", "2017-02-22 15:00:00"]],
|
||||
"domain should be in UTC format"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("custom filter datetime between operator", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
patchTimeZone(-240);
|
||||
patchDate(2017, 1, 22, 12, 30, 0);
|
||||
|
||||
const controlPanel = await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
await editConditionField(target, 0, "date_time_field");
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_generator_menu_field").value,
|
||||
"date_time_field"
|
||||
);
|
||||
assert.strictEqual(target.querySelector(".o_generator_menu_operator").value, "between");
|
||||
assert.deepEqual(
|
||||
[...target.querySelectorAll(".o_generator_menu_value input")].map((v) => v.value),
|
||||
["02/22/2017 00:00:00", "02/22/2017 23:59:59"]
|
||||
);
|
||||
|
||||
await editConditionValue(target, 0, "02/22/2017 11:00:00", 0); // in TZ
|
||||
await editConditionValue(target, 0, "02-22-2017 17:00:00", 1); // in TZ
|
||||
await applyFilter(target);
|
||||
|
||||
assert.deepEqual(
|
||||
getFacetTexts(target),
|
||||
['DateTime is between "02/22/2017 11:00:00 and 02/22/2017 17:00:00"'],
|
||||
"description should be in localized format"
|
||||
);
|
||||
assert.deepEqual(
|
||||
getDomain(controlPanel),
|
||||
[
|
||||
"&",
|
||||
["date_time_field", ">=", "2017-02-22 15:00:00"],
|
||||
["date_time_field", "<=", "2017-02-22 21:00:00"],
|
||||
],
|
||||
"domain should be in UTC format"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("input value parsing", async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
await addCondition(target);
|
||||
|
||||
await editConditionField(target, 0, "float_field");
|
||||
await editConditionField(target, 1, "id");
|
||||
|
||||
const [floatInput, idInput] = target.querySelectorAll(".o_generator_menu_value .o_input");
|
||||
|
||||
// Default values
|
||||
await editConditionValue(target, 0, "0.0");
|
||||
assert.strictEqual(floatInput.value, "0.0");
|
||||
|
||||
await editConditionValue(target, 1, "0");
|
||||
assert.strictEqual(idInput.value, "0");
|
||||
|
||||
// Float parsing
|
||||
await editConditionValue(target, 0, "4.2");
|
||||
assert.strictEqual(floatInput.value, "4.2");
|
||||
|
||||
await editConditionValue(target, 0, "DefinitelyValidFloat");
|
||||
// "DefinitelyValidFloat" cannot be entered in a input type number so that the input value is reset to 0
|
||||
assert.strictEqual(floatInput.value, "4.2");
|
||||
|
||||
// Number parsing
|
||||
await editConditionValue(target, 1, "4");
|
||||
assert.strictEqual(idInput.value, "4");
|
||||
|
||||
await editConditionValue(target, 1, "4.2");
|
||||
assert.strictEqual(idInput.value, "4");
|
||||
|
||||
await editConditionValue(target, 1, "DefinitelyValidID");
|
||||
// "DefinitelyValidID" cannot be entered in a input type number so that the input value is reset to 0
|
||||
assert.strictEqual(idInput.value, "0");
|
||||
});
|
||||
|
||||
QUnit.test("input value parsing with language", async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
|
||||
// Needs to be done after services have been started
|
||||
patchWithCleanup(localization, {
|
||||
decimalPoint: ",",
|
||||
thousandsSep: "",
|
||||
grouping: [3, 0],
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
|
||||
await editConditionField(target, 0, "float_field");
|
||||
|
||||
const [floatInput] = target.querySelectorAll(".o_generator_menu_value .o_input");
|
||||
|
||||
// Default values
|
||||
assert.strictEqual(floatInput.value, "0,0");
|
||||
|
||||
// Float parsing
|
||||
await editConditionValue(target, 0, "4,");
|
||||
assert.strictEqual(floatInput.value, "4,0");
|
||||
|
||||
await editConditionValue(target, 0, "4,2");
|
||||
assert.strictEqual(floatInput.value, "4,2");
|
||||
|
||||
await editConditionValue(target, 0, "4,2,");
|
||||
assert.strictEqual(floatInput.value, "42,0"); // because of the en localization fallback in parsers.
|
||||
|
||||
await editConditionValue(target, 0, "DefinitelyValidFloat");
|
||||
|
||||
// The input here is a string, resulting in a parsing error instead of 0
|
||||
assert.strictEqual(floatInput.value, "42,0");
|
||||
});
|
||||
|
||||
QUnit.test("add custom filter with multiple values", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const controlPanel = await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await addCondition(target);
|
||||
}
|
||||
|
||||
await editConditionField(target, 0, "date_field");
|
||||
await editConditionValue(target, 0, "01/09/1997");
|
||||
|
||||
await editConditionField(target, 1, "boolean_field");
|
||||
await editConditionOperator(target, 1, "!=");
|
||||
|
||||
await editConditionField(target, 2, "char_field");
|
||||
await editConditionValue(target, 2, "I will be deleted anyway");
|
||||
|
||||
await editConditionField(target, 3, "float_field");
|
||||
await editConditionValue(target, 3, 7.2);
|
||||
|
||||
await editConditionField(target, 4, "id");
|
||||
await editConditionValue(target, 4, 9);
|
||||
|
||||
const thirdcondition = target.querySelectorAll(".o_filter_condition")[2];
|
||||
|
||||
await click(thirdcondition, ".o_generator_menu_delete");
|
||||
await applyFilter(target);
|
||||
|
||||
assert.deepEqual(getFacetTexts(target), [
|
||||
[
|
||||
'A date is equal to "01/09/1997"',
|
||||
"Boolean Field is No",
|
||||
'Floaty McFloatface is equal to "7.2"',
|
||||
'ID is "9"',
|
||||
].join("or"),
|
||||
]);
|
||||
assert.deepEqual(getDomain(controlPanel), [
|
||||
"|",
|
||||
["date_field", "=", "1997-01-09"],
|
||||
"|",
|
||||
["boolean_field", "!=", true],
|
||||
"|",
|
||||
["float_field", "=", 7.2],
|
||||
["id", "=", 9],
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("delete button is visible", async function (assert) {
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_generator_menu_delete",
|
||||
"There is no delete button by default"
|
||||
);
|
||||
|
||||
await addCondition(target);
|
||||
assert.containsN(
|
||||
target,
|
||||
".o_generator_menu_delete",
|
||||
2,
|
||||
"A delete button has been added to each condition"
|
||||
);
|
||||
assert.containsN(
|
||||
target,
|
||||
"i.o_generator_menu_delete.fa-trash-o",
|
||||
2,
|
||||
"The delete button is shown as a trash icon"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("condition value is not lost on deep render", async function (assert) {
|
||||
const component = await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchViewId: false,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
|
||||
await toggleFilterMenu(target);
|
||||
await toggleAddCustomFilter(target);
|
||||
|
||||
await editConditionField(target, 0, "char_field");
|
||||
await editConditionValue(target, 0, "Coucou", 0, false);
|
||||
|
||||
let charInput = target.querySelector(".o_generator_menu_value .o_input");
|
||||
assert.strictEqual(charInput.value, "Coucou");
|
||||
|
||||
component.render(true);
|
||||
await nextTick();
|
||||
|
||||
charInput = target.querySelector(".o_generator_menu_value .o_input");
|
||||
assert.strictEqual(charInput.value, "Coucou");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,11 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { getFixture, patchDate, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import {
|
||||
getFixture,
|
||||
getNodesTextContent,
|
||||
patchDate,
|
||||
patchWithCleanup,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { ControlPanel } from "@web/search/control_panel/control_panel";
|
||||
import {
|
||||
@@ -8,11 +13,14 @@ import {
|
||||
isItemSelected,
|
||||
isOptionSelected,
|
||||
makeWithSearch,
|
||||
openAdvancedSearchDialog,
|
||||
setupControlPanelServiceRegistry,
|
||||
toggleFilterMenu,
|
||||
toggleMenuItem,
|
||||
toggleMenuItemOption,
|
||||
} from "./helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { AdvancedSearchDialog } from "@web/search/filter_menu/advanced_search_dialog";
|
||||
|
||||
function getDomain(controlPanel) {
|
||||
return controlPanel.env.searchModel.domain;
|
||||
@@ -57,8 +65,6 @@ QUnit.module("Search", (hooks) => {
|
||||
QUnit.module("FilterMenu");
|
||||
|
||||
QUnit.test("simple rendering with no filter", async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
@@ -69,7 +75,8 @@ QUnit.module("Search", (hooks) => {
|
||||
await toggleFilterMenu(target);
|
||||
assert.containsNone(target, ".o_menu_item");
|
||||
assert.containsNone(target, ".dropdown-divider");
|
||||
assert.containsOnce(target, ".o_add_custom_filter_menu");
|
||||
assert.containsOnce(target, ".dropdown-item");
|
||||
assert.strictEqual(target.querySelector(".dropdown-item").innerText, "Advanced Search");
|
||||
});
|
||||
|
||||
QUnit.test("simple rendering with a single filter", async function (assert) {
|
||||
@@ -91,7 +98,11 @@ QUnit.module("Search", (hooks) => {
|
||||
assert.containsOnce(target, ".o_menu_item[role=menuitemcheckbox]");
|
||||
assert.deepEqual(target.querySelector(".o_menu_item").ariaChecked, "false");
|
||||
assert.containsOnce(target, ".dropdown-divider");
|
||||
assert.containsOnce(target, ".o_add_custom_filter_menu");
|
||||
assert.containsOnce(target, ".dropdown-item:not(.o_menu_item)");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".dropdown-item:not(.o_menu_item)").innerText,
|
||||
"Advanced Search"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('toggle a "simple" filter in filter menu works', async function (assert) {
|
||||
@@ -584,4 +595,39 @@ QUnit.module("Search", (hooks) => {
|
||||
assert.strictEqual(e.innerText.trim(), String(index + 1));
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test("Open 'Advanced Search' dialog", async function (assert) {
|
||||
assert.expect(3);
|
||||
registry.category("services").add(
|
||||
"dialog",
|
||||
{
|
||||
start() {
|
||||
return {
|
||||
add: (dialogClass, props) => {
|
||||
assert.strictEqual(dialogClass, AdvancedSearchDialog);
|
||||
assert.deepEqual(Object.keys(props), [
|
||||
"domain",
|
||||
"onConfirm",
|
||||
"resModel",
|
||||
"isDebugMode",
|
||||
]);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{ force: true }
|
||||
);
|
||||
await makeWithSearch({
|
||||
serverData,
|
||||
resModel: "foo",
|
||||
Component: ControlPanel,
|
||||
searchMenuTypes: ["filter"],
|
||||
});
|
||||
await toggleFilterMenu(target);
|
||||
assert.deepEqual(
|
||||
getNodesTextContent(target.querySelectorAll(".o_filter_menu .dropdown-item")),
|
||||
["Advanced Search"]
|
||||
);
|
||||
await openAdvancedSearchDialog(target);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -198,6 +198,13 @@ export async function toggleFilterMenu(el) {
|
||||
await click(findItem(el, `.o_filter_menu button.dropdown-toggle`));
|
||||
}
|
||||
|
||||
export async function openAdvancedSearchDialog(el) {
|
||||
await click(findItem(el, `.o_filter_menu .dropdown-item:not(.o_menu_item)`));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// for legacy code
|
||||
//////////////////////////////////////////////////////
|
||||
export async function toggleAddCustomFilter(el) {
|
||||
await mouseEnter(findItem(el, `.o_add_custom_filter_menu .dropdown-toggle`));
|
||||
}
|
||||
@@ -243,6 +250,8 @@ export async function removeCondition(el, index) {
|
||||
await click(findItem(condition, ".o_generator_menu_delete"));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
|
||||
/** Group by menu */
|
||||
|
||||
export async function toggleGroupByMenu(el) {
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
} from "./helpers";
|
||||
|
||||
import { Component, onWillUpdateProps, onWillStart, useState, xml } from "@odoo/owl";
|
||||
import { dialogService } from "@web/core/dialog/dialog_service";
|
||||
import { notificationService } from "@web/core/notifications/notification_service";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
@@ -56,6 +58,7 @@ QUnit.module("Search", (hooks) => {
|
||||
},
|
||||
};
|
||||
serviceRegistry.add("hotkey", hotkeyService);
|
||||
serviceRegistry.add("notification", notificationService);
|
||||
serviceRegistry.add("orm", ormService);
|
||||
serviceRegistry.add("view", viewService);
|
||||
target = getFixture();
|
||||
@@ -111,7 +114,7 @@ QUnit.module("Search", (hooks) => {
|
||||
});
|
||||
assert.deepEqual(domain, [[0, "=", 1]]);
|
||||
assert.deepEqual(groupBy, ["birthday"]);
|
||||
assert.deepEqual(orderBy, [{name: "bar", asc: true}]);
|
||||
assert.deepEqual(orderBy, [{ name: "bar", asc: true }]);
|
||||
}
|
||||
}
|
||||
TestComponent.template = xml`<div class="o_test_component">Test component content</div>`;
|
||||
@@ -123,7 +126,7 @@ QUnit.module("Search", (hooks) => {
|
||||
domain: [[0, "=", 1]],
|
||||
groupBy: ["birthday"],
|
||||
context: { key: "val" },
|
||||
orderBy: [{name: "bar", asc: true}],
|
||||
orderBy: [{ name: "bar", asc: true }],
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -241,6 +244,8 @@ QUnit.module("Search", (hooks) => {
|
||||
async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
|
||||
class TestComponent extends Component {}
|
||||
TestComponent.components = { FilterMenu, GroupByMenu };
|
||||
TestComponent.template = xml`
|
||||
@@ -274,6 +279,8 @@ QUnit.module("Search", (hooks) => {
|
||||
async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
serviceRegistry.add("dialog", dialogService);
|
||||
|
||||
class TestComponent extends Component {
|
||||
setup() {
|
||||
owl.onWillStart(() => {
|
||||
|
||||
@@ -24,10 +24,8 @@ import {
|
||||
triggerScroll,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import {
|
||||
applyFilter,
|
||||
editSearch,
|
||||
getFacetTexts,
|
||||
toggleAddCustomFilter,
|
||||
toggleFilterMenu,
|
||||
toggleGroupByMenu,
|
||||
toggleMenuItem,
|
||||
@@ -593,7 +591,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
serverData.views = {
|
||||
"partner,false,search": `
|
||||
<search>
|
||||
<field name="datetime" />
|
||||
<filter name="filter" string="Filter" domain="[[0, '=', 1]]"/>
|
||||
</search>`,
|
||||
"partner,false,list": `
|
||||
<tree>
|
||||
@@ -629,13 +627,7 @@ QUnit.module("Fields", (hooks) => {
|
||||
const modal = target.querySelector(".modal");
|
||||
|
||||
await toggleFilterMenu(modal);
|
||||
await toggleAddCustomFilter(modal);
|
||||
assert.strictEqual(
|
||||
modal.querySelector(".o_generator_menu_field").value,
|
||||
"datetime",
|
||||
"datetime field should be selected"
|
||||
);
|
||||
await applyFilter(modal);
|
||||
await toggleMenuItem(modal, "Filter");
|
||||
|
||||
assert.strictEqual($("tr.o_data_row").length, 0, "should display 0 records");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user