[MIG] web_advanced_search: Migration to 15.0

pull/2357/head
Ivàn Todorovich 2022-02-21 17:41:16 -03:00 committed by Raf Ven
parent 1458e515e3
commit 08bd431f11
17 changed files with 442 additions and 476 deletions

View File

@ -5,17 +5,20 @@
{
"name": "Advanced search",
"version": "14.0.1.0.1",
"author": "Therp BV, " "Tecnativa, " "Odoo Community Association (OCA)",
"summary": "Easier and more powerful searching tools",
"version": "15.0.1.0.0",
"author": "Therp BV, Tecnativa, Camptocamp, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/web",
"maintainers": ["ivantodorovich"],
"license": "AGPL-3",
"category": "Usability",
"summary": "Easier and more powerful searching tools",
"website": "https://github.com/OCA/web",
"depends": ["web"],
"data": ["views/templates.xml"],
"qweb": [
"static/src/xml/web_advanced_search.xml",
],
"installable": True,
"application": False,
"assets": {
"web.assets_backend": [
"web_advanced_search/static/src/js/**/*.js",
],
"web.assets_qweb": [
"web_advanced_search/static/src/xml/**/*.xml",
],
},
}

View File

@ -12,3 +12,7 @@
* `DynApps NV <https://www.dynapps.be>`_:
* Raf Ven
* `Camptocamp <https://www.camptocamp.com>`_
* Iván Todorovich <ivan.todorovich@camptocamp.com>

View File

@ -0,0 +1,68 @@
/** @odoo-module **/
import {getHumanDomain} from "./utils.esm";
import config from "web.config";
import DomainSelectorDialog from "web.DomainSelectorDialog";
import Domain from "web.Domain";
import {useModel} from "web.Model";
const {Component, hooks} = owl;
const {useRef} = hooks;
export default class AdvancedFilterItem extends Component {
setup() {
this.itemRef = useRef("dropdown-item");
this.model = useModel("searchModel");
}
/**
* Prevent propagation of dropdown-item-selected event, so that it
* doesn't reaches the FilterMenu onFilterSelected event handler.
*/
mounted() {
$(this.itemRef.el).on("dropdown-item-selected", (event) =>
event.stopPropagation()
);
}
/**
* Open advanced search dialog
*
* @returns {DomainSelectorDialog} The opened dialog itself.
*/
onClick() {
const dialog = new DomainSelectorDialog(
this,
this.model.config.modelName,
"[]",
{
debugMode: config.isDebug(),
readonly: false,
}
);
// Add 1st domain node by default
dialog.opened(() => dialog.domainSelector._onAddFirstButtonClick());
// Configure handler
dialog.on("domain_selected", this, function (e) {
const preFilter = {
description: getHumanDomain(dialog.domainSelector),
domain: Domain.prototype.arrayToString(e.data.domain),
type: "filter",
};
this.model.dispatch("createNewFilters", [preFilter]);
});
return dialog.open();
}
/**
* Mocks _trigger_up to redirect Odoo legacy events to OWL events.
*
* @private
* @param {OdooEvent} event
*/
_trigger_up(event) {
const {name, data} = event;
data.__targetWidget = event.target;
this.trigger(name.replace(/_/g, "-"), data);
}
}
AdvancedFilterItem.template = "web_advanced_search.AdvancedFilterItem";

View File

@ -0,0 +1,85 @@
/** @odoo-module **/
import {patch} from "@web/core/utils/patch";
import CustomFilterItem from "web.CustomFilterItem";
import {RecordPicker} from "./RecordPicker.esm";
patch(CustomFilterItem.prototype, "web_advanced_search.CustomFilterItem", {
/**
* Ideally we'd want this in setup, but CustomFilterItem does its initialization
* in the constructor, which can't be patched.
*
* Doing it here works just as well.
*
* @override
*/
async willStart() {
this.OPERATORS.relational = this.OPERATORS.char;
this.FIELD_TYPES.many2one = "relational";
return this._super(...arguments);
},
/**
* @override
*/
_setDefaultValue(condition) {
const res = this._super(...arguments);
const fieldType = this.fields[condition.field].type;
const genericType = this.FIELD_TYPES[fieldType];
if (genericType === "relational") {
condition.value = 0;
condition.displayedValue = "";
}
return res;
},
/**
* Add displayed value to preFilters for "relational" types.
*
* @override
*/
onApply() {
// To avoid the complete override, we patch this.conditions.map()
const originalMapFn = this.conditions.map;
const self = this;
this.conditions.map = function () {
const preFilters = originalMapFn.apply(this, arguments);
for (const condition of this) {
const field = self.fields[condition.field];
const type = self.FIELD_TYPES[field.type];
if (type === "relational") {
const idx = this.indexOf(condition);
const preFilter = preFilters[idx];
const operator = self.OPERATORS[type][condition.operator];
const descriptionArray = [
field.string,
operator.description,
`"${condition.displayedValue}"`,
];
preFilter.description = descriptionArray.join(" ");
}
}
return preFilters;
};
const res = this._super(...arguments);
// Restore original map()
this.conditions.map = originalMapFn;
return res;
},
/**
* @private
* @param {Object} condition
* @param {OwlEvent} ev
*/
onRelationalChanged(condition, ev) {
condition.value = ev.detail.id;
condition.displayedValue = ev.detail.display_name;
},
});
patch(CustomFilterItem, "web_advanced_search.CustomFilterItem", {
components: {
...CustomFilterItem.components,
RecordPicker,
},
});
export default CustomFilterItem;

View File

@ -0,0 +1,14 @@
/** @odoo-module **/
import {patch} from "@web/core/utils/patch";
import FilterMenu from "web.FilterMenu";
import AdvancedFilterItem from "./AdvancedFilterItem.esm";
patch(FilterMenu, "web_advanced_search.FilterMenu", {
components: {
...FilterMenu.components,
AdvancedFilterItem,
},
});
export default FilterMenu;

View File

@ -0,0 +1,147 @@
/** @odoo-module **/
import BasicModel from "web.BasicModel";
import {ComponentAdapter} from "web.OwlCompatibility";
import {FieldMany2One} from "web.relational_fields";
import FieldManagerMixin from "web.FieldManagerMixin";
import {SelectCreateDialog} from "web.view_dialogs";
const {Component} = owl;
const {xml} = owl.tags;
export const FakeMany2oneFieldWidget = FieldMany2One.extend(FieldManagerMixin, {
/**
* @override
*/
init: function (parent) {
this.componentAdapter = parent;
const options = this.componentAdapter.props.attrs;
// Create a dummy record with only a dummy m2o field to search on
const model = new BasicModel("dummy");
const params = {
fieldNames: ["dummy"],
modelName: "dummy",
context: {},
type: "record",
viewType: "default",
fieldsInfo: {default: {dummy: {}}},
fields: {
dummy: {
string: options.string,
relation: options.model,
context: options.context,
domain: options.domain,
type: "many2one",
},
},
};
// Emulate `model.load()`, without RPC-calling `default_get()`
this.dataPointID = model._makeDataPoint(params).id;
model.generateDefaultValues(this.dataPointID, {});
this._super(this.componentAdapter, "dummy", this._get_record(model), {
mode: "edit",
attrs: {
options: {
no_create_edit: true,
no_create: true,
no_open: true,
no_quick_create: true,
},
},
});
FieldManagerMixin.init.call(this, model);
},
/**
* Get record
*
* @param {BasicModel} model
*/
_get_record: function (model) {
return model.get(this.dataPointID);
},
/**
* @override
*/
_confirmChange: function (id, fields, event) {
this.componentAdapter.trigger("change", event.data.changes[fields[0]]);
this.dataPointID = id;
return this.reset(this._get_record(this.model), event);
},
/**
* Stop propagation of the autocompleteselect event.
* Otherwise, the filter's dropdown will be closed after a selection.
*
* @override to stop propagating autocompleteselect event
*/
start: function () {
this._super(...arguments);
this.$input.on("autocompleteselect", (event) => event.stopPropagation());
},
/**
* Stop propagation of the 'Search more..' dialog click event.
* Otherwise, the filter's dropdown will be closed after a selection.
*
* @override
*/
_searchCreatePopup: function (view, ids, context, dynamicFilters) {
const options = this._getSearchCreatePopupOptions(
view,
ids,
context,
dynamicFilters
);
const dialog = new SelectCreateDialog(
this,
_.extend({}, this.nodeOptions, options)
);
// Hack to stop click event propagation
dialog._opened.then(() =>
dialog.$el
.get(0)
.addEventListener("click", (event) => event.stopPropagation())
);
return dialog.open();
},
});
export class FakeMany2oneFieldWidgetAdapter extends ComponentAdapter {
async updateWidget() {
/* eslint-disable no-empty-function */
}
async renderWidget() {
/* eslint-disable no-empty-function */
}
}
/**
* A record selector widget.
*
* Underneath, it implements and extends the `FieldManagerMixin`, and acts as if it
* were a reduced dummy controller. Some actions "mock" the underlying model, since
* sometimes we use a char widget to fill related fields (which is not supported by
* that widget), and fields need an underlying model implementation, which can only
* hold fake data, given a search view has no data on it by definition.
*
* @extends Component
*/
export class RecordPicker extends Component {
setup() {
this.attrs = {
string: this.props.string,
model: this.props.model,
domain: this.props.domain,
context: this.props.context,
};
this.FakeMany2oneFieldWidget = FakeMany2oneFieldWidget;
}
}
RecordPicker.template = xml`
<div>
<FakeMany2oneFieldWidgetAdapter
Component="FakeMany2oneFieldWidget"
class="d-block"
attrs="attrs"
/>
</div>`;
RecordPicker.components = {FakeMany2oneFieldWidgetAdapter};

View File

@ -1,66 +0,0 @@
odoo.define("web_advanced_search.AdvancedFilterItem", function (require) {
"use strict";
const config = require("web.config");
const DropdownMenuItem = require("web.DropdownMenuItem");
const patchMixin = require("web.patchMixin");
const DomainSelectorDialog = require("web.DomainSelectorDialog");
const Domain = require("web.Domain");
const human_domain = require("web_advanced_search.human_domain");
const {useModel} = require("web/static/src/js/model.js");
class AdvancedFilterItem extends DropdownMenuItem {
constructor() {
super(...arguments);
this.model = useModel("searchModel");
this._modelName = this.model.config.modelName;
}
/**
* Open advanced search dialog
*
* @returns {DomainSelectorDialog} The opened dialog itself.
*/
advanced_search_open() {
const domain_selector_dialog = new DomainSelectorDialog(
this,
this._modelName,
"[]",
{
debugMode: config.isDebug(),
readonly: false,
}
);
domain_selector_dialog.opened(() => {
// Add 1st domain node by default
domain_selector_dialog.domainSelector._onAddFirstButtonClick();
});
domain_selector_dialog.on("domain_selected", this, function (e) {
const preFilter = {
description: human_domain.getHumanDomain(
domain_selector_dialog.domainSelector
),
domain: Domain.prototype.arrayToString(e.data.domain),
type: "filter",
};
this.model.dispatch("createNewFilters", [preFilter]);
});
return domain_selector_dialog.open();
}
/**
* Mocks _trigger_up to redirect Odoo legacy events to OWL events.
*
* @private
* @param {OdooEvent} ev
*/
_trigger_up(ev) {
const evType = ev.name;
const payload = ev.data;
payload.__targetWidget = ev.target;
this.trigger(evType.replace(/_/g, "-"), payload);
}
}
AdvancedFilterItem.template = "web_advanced_search.AdvancedFilterItem";
return patchMixin(AdvancedFilterItem);
});

View File

@ -1,140 +0,0 @@
odoo.define("web_advanced_search.CustomFilterItem", function (require) {
"use strict";
const CustomFilterItem = require("web.CustomFilterItem");
const FieldMany2One = require("web.relational_fields").FieldMany2One;
const Relational = require("web_advanced_search.RelationalOwl");
const {FIELD_TYPES} = require("web.searchUtils");
const {useListener} = require("web.custom_hooks");
const Domain = require("web.Domain");
const field_utils = require("web.field_utils");
CustomFilterItem.patch("web_advanced_search.CustomFilterItem", (T) => {
class AdvancedCustomFilterItem extends T {
constructor() {
super(...arguments);
this.state.field = false;
this.OPERATORS.relational = this.OPERATORS.char;
this.FIELD_TYPES.many2one = "relational";
useListener("m2xchange", this._onM2xDataChanged);
}
_addDefaultCondition() {
super._addDefaultCondition(...arguments);
const condition =
this.state.conditions[this.state.conditions.length - 1];
condition.index = _.uniqueId("condition_");
}
/**
* @private
* @param {Object} condition
*/
_setDefaultValue(condition) {
const fieldType = this.fields[condition.field].type;
const genericType = FIELD_TYPES[fieldType];
if (genericType === "relational") {
condition.displayedValue = "";
} else {
super._setDefaultValue(...arguments);
}
}
/**
* @private
* @param {Object} condition
* @param {Event} ev
*/
_onFieldSelect(condition, ev) {
super._onFieldSelect(...arguments);
this.state.field = this.fields[ev.target.selectedIndex];
this.state.fieldindex = ev.target.selectedIndex;
this.state.conditionIndex = condition.index;
}
/**
* @private
* @param {Object} condition
* @param {Event} ev
*/
_onOperatorSelect(condition, ev) {
this.trigger("operatorChange");
this.state.operator = ev.target[ev.target.selectedIndex].value;
super._onOperatorSelect(...arguments);
}
_onM2xDataChanged(event) {
const fieldindex = this.fields
.map((field) => field.name)
.indexOf(event.detail.field);
const condition = this.state.conditions.filter(
(con) =>
con.field === fieldindex &&
con.index === this.state.conditionIndex
);
if (condition.length) {
condition[0].value = event.detail.changes.id;
condition[0].displayedValue = event.detail.changes.display_name;
}
}
_onApply() {
/* Patch onApply to add displayedValue to discriptionArray */
const preFilters = this.state.conditions.map((condition) => {
const field = this.fields[condition.field];
const type = this.FIELD_TYPES[field.type];
const operator = this.OPERATORS[type][condition.operator];
const descriptionArray = [field.string, operator.description];
const domainArray = [];
let domainValue = [];
// Field type specifics
if ("value" in operator) {
domainValue = [operator.value];
// No description to push here
} else if (["date", "datetime"].includes(type)) {
domainValue = condition.value.map((val) =>
field_utils.parse[type](val, {type}, {timezone: true})
);
const dateValue = condition.value.map((val) =>
field_utils.format[type](val, {type}, {timezone: false})
);
descriptionArray.push(
`"${dateValue.join(" " + this.env._t("and") + " ")}"`
);
} else {
domainValue = [condition.value];
descriptionArray.push(
`"${condition.displayedValue || 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: Domain.prototype.arrayToString(domainArray),
type: "filter",
};
return preFilter;
});
this.model.dispatch("createNewFilters", preFilters);
// Reset state
this.state.open = false;
this.state.conditions = [];
this._addDefaultCondition();
}
}
return AdvancedCustomFilterItem;
});
// Extends HomeMenuWrapper components
CustomFilterItem.components = Object.assign({}, CustomFilterItem.components, {
FieldMany2One,
Relational,
});
});

View File

@ -1,18 +0,0 @@
odoo.define("web_advanced_search.FilterMenu", function (require) {
"use strict";
const FilterMenu = require("web.FilterMenu");
const patchMixin = require("web.patchMixin");
const PatchableFilterMenu = patchMixin(FilterMenu);
const AdvancedFilterItem = require("web_advanced_search.AdvancedFilterItem");
PatchableFilterMenu.patch("web_advanced_search.FilterMenu", (T) => {
class AdvancedFilterMenu extends T {}
AdvancedFilterMenu.components = Object.assign({}, FilterMenu.components, {
AdvancedFilterItem,
});
return AdvancedFilterMenu;
});
FilterMenu.components = PatchableFilterMenu.components;
});

View File

@ -1,61 +0,0 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* Copyright 2020 Tecnativa - Alexandre Díaz
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
odoo.define("web_advanced_search.human_domain", function () {
"use strict";
const join_mapping = {
"&": _(" and "),
"|": _(" or "),
"!": _(" is not "),
};
const human_domain_methods = {
DomainTree: function () {
const human_domains = [];
_.each(this.children, (child) => {
human_domains.push(human_domain_methods[child.template].apply(child));
});
return `(${human_domains.join(join_mapping[this.operator])})`;
},
DomainSelector: function () {
const result = human_domain_methods.DomainTree.apply(this, arguments);
// Remove surrounding parenthesis
return result.slice(1, -1);
},
DomainLeaf: function () {
const chain = [];
let operator = this.operator_mapping[this.operator],
value = `"${this.value}"`;
// Humanize chain
const chain_splitted = this.chain.split(".");
const len = chain_splitted.length;
for (let x = 0; x < len; ++x) {
const element = chain_splitted[x];
chain.push(
_.findWhere(this.fieldSelector.pages[x], {name: element}).string ||
element
);
}
// Special beautiness for some values
if (this.operator === "=" && _.isBoolean(this.value)) {
operator = this.operator_mapping[this.value ? "set" : "not set"];
value = "";
} else if (_.isArray(this.value)) {
value = `["${this.value.join('", "')}"]`;
}
return `${chain.join("→")} ${operator || this.operator} ${value}`.trim();
},
};
function getHumanDomain(domain_selector) {
return human_domain_methods.DomainSelector.apply(domain_selector);
}
return {
getHumanDomain: getHumanDomain,
};
});

View File

@ -1,111 +0,0 @@
odoo.define("web_advanced_search.RelationalOwl", function (require) {
"use strict";
const BasicModel = require("web.BasicModel");
const patchMixin = require("web.patchMixin");
const {ComponentAdapter} = require("web.OwlCompatibility");
const relationalFields = require("web.relational_fields");
const FieldMany2One = relationalFields.FieldMany2One;
const FieldManagerMixin = require("web.FieldManagerMixin");
const {useListener} = require("web.custom_hooks");
/* global owl */
const {Component} = owl;
const {xml} = owl.tags;
const AdvancedSearchWidget = FieldMany2One.extend(FieldManagerMixin, {
init: function (parent) {
const field = parent.__owl__.parent.field;
const model = new BasicModel(field.relation);
// Create dummy record with only the field the user is searching
const params = {
fieldNames: [field.name],
modelName: field.relation,
context: field.context,
type: "record",
viewType: "default",
fieldsInfo: {
default: {},
},
fields: {
[field.name]: _.omit(
field,
// User needs all records, to actually produce a new domain
"domain",
// Onchanges make no sense in this context, there's no record
"onChange"
),
},
};
if (field.type.endsWith("2many")) {
// X2many fields behave like m2o in the search context
params.fields[field.name].type = "many2one";
}
params.fieldsInfo.default[field.name] = {};
// Emulate `model.load()`, without RPC-calling `default_get()`
this.dataPointID = model._makeDataPoint(params).id;
model.generateDefaultValues(this.dataPointID, {});
this._super(parent, field.name, this._get_record(model), {
mode: "edit",
attrs: {
options: {
no_create_edit: true,
no_create: true,
no_open: true,
no_quick_create: true,
},
},
});
FieldManagerMixin.init.call(this, model);
},
_get_record: function (model) {
return model.get(this.dataPointID);
},
/**
* @override
*/
_confirmChange: function (id, fields, event) {
this.trigger_up("m2xchange", {
data: event.data,
changes: event.data.changes[fields[0]],
field: fields[0],
});
this.dataPointID = id;
return this.reset(this._get_record(this.model), event);
},
});
/**
* A search field for relational fields.
*
* It implements and extends the `FieldManagerMixin`, and acts as if it
* were a reduced dummy controller. Some actions "mock" the underlying
* model, since sometimes we use a char widget to fill related fields
* (which is not supported by that widget), and fields need an underlying
* model implementation, which can only hold fake data, given a search view
* has no data on it by definition.
*/
class Relational extends Component {
// eslint-disable-next-line no-unused-vars
constructor(parent, component, props) {
super(...arguments);
this.field = parent.state.field;
this.operator = parent.state.operator;
this.FieldWidget = false;
this.set_widget();
useListener("operatorChange", this.set_widget);
}
/**
* @override
*/
set_widget() {
this.FieldWidget = AdvancedSearchWidget;
}
}
Relational.template = xml`
<div>
<ComponentAdapter Component="FieldWidget" />
</div>`;
Relational.components = {ComponentAdapter};
return patchMixin(Relational);
});

View File

@ -0,0 +1,59 @@
/** @odoo-module **/
/*
Copyright 2018 Tecnativa - Jairo Llopis
Copyright 2020 Tecnativa - Alexandre Díaz
Copyright 2022 Camptocamp SA - Iván Todorovich
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
*/
import {_t} from "web.core";
const JOIN_MAPPING = {
"&": _t(" and "),
"|": _t(" or "),
"!": _t(" is not "),
};
const HUMAN_DOMAIN_METHODS = {
DomainTree: function () {
const human_domains = [];
_.each(this.children, (child) => {
human_domains.push(HUMAN_DOMAIN_METHODS[child.template].apply(child));
});
return `(${human_domains.join(JOIN_MAPPING[this.operator])})`;
},
DomainSelector: function () {
const result = HUMAN_DOMAIN_METHODS.DomainTree.apply(this, arguments);
// Remove surrounding parenthesis
return result.slice(1, -1);
},
DomainLeaf: function () {
const chain = [];
let operator = this.operator_mapping[this.operator],
value = `"${this.value}"`;
// Humanize chain
const chain_splitted = this.chain.split(".");
const len = chain_splitted.length;
for (let x = 0; x < len; ++x) {
const element = chain_splitted[x];
chain.push(
_.findWhere(this.fieldSelector.pages[x], {name: element}).string ||
element
);
}
// Special beautiness for some values
if (this.operator === "=" && _.isBoolean(this.value)) {
operator = this.operator_mapping[this.value ? "set" : "not set"];
value = "";
} else if (_.isArray(this.value)) {
value = `["${this.value.join('", "')}"]`;
}
return `${chain.join("→")} ${operator || this.operator} ${value}`.trim();
},
};
export function getHumanDomain(domainSelector) {
return HUMAN_DOMAIN_METHODS.DomainSelector.apply(domainSelector);
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2017-2018 Jairo Llopis <jairo.llopis@tecnativa.com>
Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<templates>
<t t-name="web_advanced_search.AdvancedFilterItem" owl="1">
<DropdownItem
t-ref="dropdown-item"
t-on-click="onClick"
class="o_add_advanced_search"
>
Add Advanced Filter
</DropdownItem>
</t>
</templates>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2017-2018 Jairo Llopis <jairo.llopis@tecnativa.com>
Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<templates>
<t t-inherit="web.CustomFilterItem" t-inherit-mode="extension" owl="1">
<xpath expr="//select[@t-elif]" position="after">
<t
t-elif="fieldType === 'many2one' and ['=', '!='].includes(selectedOperator.symbol)"
>
<RecordPicker
model="fields[condition.field].relation"
string="fields[condition.field].string"
context="fields[condition.field].context"
t-on-change="onRelationalChanged(condition)"
/>
</t>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2017-2018 Jairo Llopis <jairo.llopis@tecnativa.com>
Copyright 2022 Camptocamp SA (https://www.camptocamp.com).
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<templates>
<t t-inherit="web.legacy.FilterMenu" t-inherit-mode="extension" owl="1">
<CustomFilterItem position="after">
<AdvancedFilterItem />
</CustomFilterItem>
</t>
</templates>

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2017-2018 Jairo Llopis <jairo.llopis@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<templates>
<t t-inherit="web.FilterMenu" t-inherit-mode="extension" owl="1">
<xpath expr="//CustomFilterItem" position="after">
<li t-if="items.length" class="dropdown-divider" role="separator" />
<AdvancedFilterItem fields="props.fields" />
</xpath>
</t>
<t t-inherit="web.CustomFilterItem" t-inherit-mode="extension" owl="1">
<xpath expr="//select[@t-elif]" position="after">
<t t-elif="fieldType === 'many2one'">
<t
t-if="selectedOperator.symbol === '=' || selectedOperator.symbol === '!='"
>
<Relational />
</t>
<input
t-else=""
type="text"
class="o_input"
t-att-value="condition.displayedValue"
t-on-input="_onValueInput(condition)"
/>
</t>
</xpath>
</t>
<t t-name="web_advanced_search.AdvancedFilterItem" owl="1">
<div class="o_generator_menu">
<button
type="button"
class="o_add_advanced_search dropdown-item"
aria-expanded="false"
t-on-click="advanced_search_open"
>
<t>Add Advanced Filter</t>
</button>
</div>
</t>
</templates>

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2017-2018 Jairo Llopis <jairo.llopis@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<template id="assets_backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script
type="text/javascript"
src="/web_advanced_search/static/src/js/control_panel/advanced_filter_item.js"
/>
<script
type="text/javascript"
src="/web_advanced_search/static/src/js/control_panel/filter_menu.js"
/>
<script
type="text/javascript"
src="/web_advanced_search/static/src/js/control_panel/custom_filter_item.js"
/>
<script
type="text/javascript"
src="/web_advanced_search/static/src/js/human_domain.js"
/>
<script
type="text/javascript"
src="/web_advanced_search/static/src/js/relational.js"
/>
</xpath>
</template>
</odoo>