mirror of https://github.com/OCA/web.git
[MIG] web_advanced_search: Migration to 15.0
parent
386072b61b
commit
f3999c21a8
|
@ -5,17 +5,20 @@
|
||||||
|
|
||||||
{
|
{
|
||||||
"name": "Advanced search",
|
"name": "Advanced search",
|
||||||
"version": "14.0.1.0.1",
|
"summary": "Easier and more powerful searching tools",
|
||||||
"author": "Therp BV, " "Tecnativa, " "Odoo Community Association (OCA)",
|
"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",
|
"license": "AGPL-3",
|
||||||
"category": "Usability",
|
"category": "Usability",
|
||||||
"summary": "Easier and more powerful searching tools",
|
|
||||||
"website": "https://github.com/OCA/web",
|
|
||||||
"depends": ["web"],
|
"depends": ["web"],
|
||||||
"data": ["views/templates.xml"],
|
"assets": {
|
||||||
"qweb": [
|
"web.assets_backend": [
|
||||||
"static/src/xml/web_advanced_search.xml",
|
"web_advanced_search/static/src/js/**/*.js",
|
||||||
],
|
],
|
||||||
"installable": True,
|
"web.assets_qweb": [
|
||||||
"application": False,
|
"web_advanced_search/static/src/xml/**/*.xml",
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,3 +12,7 @@
|
||||||
* `DynApps NV <https://www.dynapps.be>`_:
|
* `DynApps NV <https://www.dynapps.be>`_:
|
||||||
|
|
||||||
* Raf Ven
|
* Raf Ven
|
||||||
|
|
||||||
|
* `Camptocamp <https://www.camptocamp.com>`_
|
||||||
|
|
||||||
|
* Iván Todorovich <ivan.todorovich@camptocamp.com>
|
||||||
|
|
|
@ -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";
|
|
@ -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;
|
|
@ -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;
|
|
@ -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};
|
|
@ -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);
|
|
||||||
});
|
|
|
@ -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,
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
});
|
|
|
@ -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,
|
|
||||||
};
|
|
||||||
});
|
|
|
@ -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);
|
|
||||||
});
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
|
@ -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>
|
|
Loading…
Reference in New Issue