From f3999c21a8dfc412240e764e807038322f5eabf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A0n=20Todorovich?= Date: Mon, 21 Feb 2022 17:41:16 -0300 Subject: [PATCH] [MIG] web_advanced_search: Migration to 15.0 --- web_advanced_search/__manifest__.py | 23 +-- web_advanced_search/readme/CONTRIBUTORS.rst | 4 + .../static/src/js/AdvancedFilterItem.esm.js | 68 ++++++++ .../static/src/js/CustomFilterItem.esm.js | 85 ++++++++++ .../static/src/js/FilterMenu.esm.js | 14 ++ .../static/src/js/RecordPicker.esm.js | 147 ++++++++++++++++++ .../js/control_panel/advanced_filter_item.js | 66 -------- .../js/control_panel/custom_filter_item.js | 140 ----------------- .../src/js/control_panel/filter_menu.js | 18 --- .../static/src/js/human_domain.js | 61 -------- .../static/src/js/relational.js | 111 ------------- .../static/src/js/utils.esm.js | 59 +++++++ .../static/src/xml/AdvancedFilterItem.xml | 17 ++ .../static/src/xml/CustomFilterItem.xml | 22 +++ .../static/src/xml/FilterMenu.xml | 13 ++ .../static/src/xml/web_advanced_search.xml | 41 ----- web_advanced_search/views/templates.xml | 29 ---- 17 files changed, 442 insertions(+), 476 deletions(-) create mode 100644 web_advanced_search/static/src/js/AdvancedFilterItem.esm.js create mode 100644 web_advanced_search/static/src/js/CustomFilterItem.esm.js create mode 100644 web_advanced_search/static/src/js/FilterMenu.esm.js create mode 100644 web_advanced_search/static/src/js/RecordPicker.esm.js delete mode 100644 web_advanced_search/static/src/js/control_panel/advanced_filter_item.js delete mode 100644 web_advanced_search/static/src/js/control_panel/custom_filter_item.js delete mode 100644 web_advanced_search/static/src/js/control_panel/filter_menu.js delete mode 100644 web_advanced_search/static/src/js/human_domain.js delete mode 100644 web_advanced_search/static/src/js/relational.js create mode 100644 web_advanced_search/static/src/js/utils.esm.js create mode 100644 web_advanced_search/static/src/xml/AdvancedFilterItem.xml create mode 100644 web_advanced_search/static/src/xml/CustomFilterItem.xml create mode 100644 web_advanced_search/static/src/xml/FilterMenu.xml delete mode 100644 web_advanced_search/static/src/xml/web_advanced_search.xml delete mode 100644 web_advanced_search/views/templates.xml diff --git a/web_advanced_search/__manifest__.py b/web_advanced_search/__manifest__.py index c980699d3..72a249d64 100644 --- a/web_advanced_search/__manifest__.py +++ b/web_advanced_search/__manifest__.py @@ -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", + ], + }, } diff --git a/web_advanced_search/readme/CONTRIBUTORS.rst b/web_advanced_search/readme/CONTRIBUTORS.rst index fbe3b818d..07528e22a 100644 --- a/web_advanced_search/readme/CONTRIBUTORS.rst +++ b/web_advanced_search/readme/CONTRIBUTORS.rst @@ -12,3 +12,7 @@ * `DynApps NV `_: * Raf Ven + +* `Camptocamp `_ + + * Iván Todorovich diff --git a/web_advanced_search/static/src/js/AdvancedFilterItem.esm.js b/web_advanced_search/static/src/js/AdvancedFilterItem.esm.js new file mode 100644 index 000000000..73360f387 --- /dev/null +++ b/web_advanced_search/static/src/js/AdvancedFilterItem.esm.js @@ -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"; diff --git a/web_advanced_search/static/src/js/CustomFilterItem.esm.js b/web_advanced_search/static/src/js/CustomFilterItem.esm.js new file mode 100644 index 000000000..7ed4d085c --- /dev/null +++ b/web_advanced_search/static/src/js/CustomFilterItem.esm.js @@ -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; diff --git a/web_advanced_search/static/src/js/FilterMenu.esm.js b/web_advanced_search/static/src/js/FilterMenu.esm.js new file mode 100644 index 000000000..255e50e4a --- /dev/null +++ b/web_advanced_search/static/src/js/FilterMenu.esm.js @@ -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; diff --git a/web_advanced_search/static/src/js/RecordPicker.esm.js b/web_advanced_search/static/src/js/RecordPicker.esm.js new file mode 100644 index 000000000..83011139d --- /dev/null +++ b/web_advanced_search/static/src/js/RecordPicker.esm.js @@ -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` +
+ +
`; +RecordPicker.components = {FakeMany2oneFieldWidgetAdapter}; diff --git a/web_advanced_search/static/src/js/control_panel/advanced_filter_item.js b/web_advanced_search/static/src/js/control_panel/advanced_filter_item.js deleted file mode 100644 index be480eef1..000000000 --- a/web_advanced_search/static/src/js/control_panel/advanced_filter_item.js +++ /dev/null @@ -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); -}); diff --git a/web_advanced_search/static/src/js/control_panel/custom_filter_item.js b/web_advanced_search/static/src/js/control_panel/custom_filter_item.js deleted file mode 100644 index cebbdffc8..000000000 --- a/web_advanced_search/static/src/js/control_panel/custom_filter_item.js +++ /dev/null @@ -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, - }); -}); diff --git a/web_advanced_search/static/src/js/control_panel/filter_menu.js b/web_advanced_search/static/src/js/control_panel/filter_menu.js deleted file mode 100644 index 97438eefb..000000000 --- a/web_advanced_search/static/src/js/control_panel/filter_menu.js +++ /dev/null @@ -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; -}); diff --git a/web_advanced_search/static/src/js/human_domain.js b/web_advanced_search/static/src/js/human_domain.js deleted file mode 100644 index 9dbc49e4d..000000000 --- a/web_advanced_search/static/src/js/human_domain.js +++ /dev/null @@ -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, - }; -}); diff --git a/web_advanced_search/static/src/js/relational.js b/web_advanced_search/static/src/js/relational.js deleted file mode 100644 index 18c276f7b..000000000 --- a/web_advanced_search/static/src/js/relational.js +++ /dev/null @@ -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` -
- -
`; - Relational.components = {ComponentAdapter}; - return patchMixin(Relational); -}); diff --git a/web_advanced_search/static/src/js/utils.esm.js b/web_advanced_search/static/src/js/utils.esm.js new file mode 100644 index 000000000..f1dfeedec --- /dev/null +++ b/web_advanced_search/static/src/js/utils.esm.js @@ -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); +} diff --git a/web_advanced_search/static/src/xml/AdvancedFilterItem.xml b/web_advanced_search/static/src/xml/AdvancedFilterItem.xml new file mode 100644 index 000000000..4c91b46bf --- /dev/null +++ b/web_advanced_search/static/src/xml/AdvancedFilterItem.xml @@ -0,0 +1,17 @@ + + + + + + Add Advanced Filter + + + diff --git a/web_advanced_search/static/src/xml/CustomFilterItem.xml b/web_advanced_search/static/src/xml/CustomFilterItem.xml new file mode 100644 index 000000000..30d0c7801 --- /dev/null +++ b/web_advanced_search/static/src/xml/CustomFilterItem.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/web_advanced_search/static/src/xml/FilterMenu.xml b/web_advanced_search/static/src/xml/FilterMenu.xml new file mode 100644 index 000000000..34dd3d4b8 --- /dev/null +++ b/web_advanced_search/static/src/xml/FilterMenu.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/web_advanced_search/static/src/xml/web_advanced_search.xml b/web_advanced_search/static/src/xml/web_advanced_search.xml deleted file mode 100644 index 5f50fa148..000000000 --- a/web_advanced_search/static/src/xml/web_advanced_search.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - -