Browse Source

[MIG][REF] web_advanced_search: Rename, refactor, migrate

* Complete migration to v11
* Refactor to use the new v11 decoupled widgets system
* Advanced search is now a high-level feature from the filters menu; it simplifies code a lot, and the UX is even better
* Split README system
* Add fun to ROADMAP
* Addon is renamed to web_advanced_search, since it enhaces the searching experience for all kind of fields now
pull/999/head
Jairo Llopis 6 years ago
committed by Pedro M. Baeza
parent
commit
3226b4cdf2
  1. 7
      web_advanced_search/README.rst
  2. 3
      web_advanced_search/__init__.py
  3. 13
      web_advanced_search/__openerp__.py
  4. 0
      web_advanced_search/i18n/web_advanced_search.pot
  5. 5
      web_advanced_search/readme/CONTRIBUTORS.rst
  6. 1
      web_advanced_search/readme/DESCRIPTION.rst
  7. 14
      web_advanced_search/readme/ROADMAP.rst
  8. 21
      web_advanced_search/readme/USAGE.rst
  9. 13
      web_advanced_search/static/src/css/web_advanced_search.less
  10. 27
      web_advanced_search/static/src/css/web_advanced_search_x2x.less
  11. 86
      web_advanced_search/static/src/js/human_domain.js
  12. 293
      web_advanced_search/static/src/js/web_advanced_search.js
  13. 185
      web_advanced_search/static/src/js/web_advanced_search_x2x.js
  14. 13
      web_advanced_search/static/src/xml/web_advanced_search.xml
  15. 13
      web_advanced_search/static/src/xml/web_advanced_search_x2x.xml
  16. 9
      web_advanced_search/views/templates.xml

7
web_advanced_search/README.rst

@ -1,6 +1,6 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
=========================================
Search for x2x records in advanced search
@ -51,7 +51,6 @@ Contributors
* Vicent Cubells <vicent.cubells@tecnativa.com>
* Jairo Llopis <jairo.llopis@tecnativa.com>
* Rami Alwafaie <rami.alwafaie@initos.com>
* Jose Mª Bernet <josemaria.bernet@guadaltech.es>
Maintainer
----------

3
web_advanced_search/__init__.py

@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

13
web_advanced_search/__openerp__.py

@ -1,24 +1,27 @@
# Copyright 2015 Therp BV <http://therp.nl>
# Copyright 2017 Tecnativa - Vicent Cubells
# Copyright 2018 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Search x2x fields",
"name": "Advanced search",
"version": "11.0.1.0.0",
"author": "Therp BV, "
"Tecnativa, "
"Odoo Community Association (OCA)",
"license": "AGPL-3",
"category": "Usability",
"summary": "Use a search widget in advanced search for x2x fields",
"depends": [],
"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_x2x.xml',
'static/src/xml/web_advanced_search.xml',
],
"auto_install": False,
"installable": True,
"application": False,
}

0
web_advanced_search/i18n/web_advanced_search_x2x.pot → web_advanced_search/i18n/web_advanced_search.pot

5
web_advanced_search/readme/CONTRIBUTORS.rst

@ -0,0 +1,5 @@
* Holger Brunn <hbrunn@therp.nl>
* Vicent Cubells <vicent.cubells@tecnativa.com>
* Jairo Llopis <jairo.llopis@tecnativa.com>
* Rami Alwafaie <rami.alwafaie@initos.com>
* Jose Mª Bernet <josemaria.bernet@guadaltech.es>

1
web_advanced_search/readme/DESCRIPTION.rst

@ -0,0 +1 @@
More powerful and easy to use search, especially for related fields.

14
web_advanced_search/readme/ROADMAP.rst

@ -0,0 +1,14 @@
Improvements to the ``domain`` widget, not exclusively related to this addon:
* Use relational widgets when filtering a relational field
* Allow to filter field names
Improvements to the search view in this addon:
* Use widgets ``one2many_tags`` when searching ``one2many`` fields
* Use widgets ``many2many_tags`` when searching ``many2many`` fields
* Allow to edit current full search using the advanced domain editor
* Allow to edit individually any facet from current search using the
advanced domain editor
* Beautiful, human-readable, domain representation when adding an
advanced filter

21
web_advanced_search/readme/USAGE.rst

@ -0,0 +1,21 @@
To use this module, you need to:
* Open *Filters* in a search view
* Select any relational field
* Select operator `is equal to` or `is not equal to`
* The text field changes to a relational selection field where you
can search for the record in question
* Click *Apply*
To search for properties of linked records (ie invoices for customers
with a credit limit higher than X):
* Open *Filters* in a search view
* Select *Add Advanced Filter*
* Edit the advanced filter
* Click *Save*
Note that you can stack searching for properties: Simply add another
advanced search in the selection search window. You can do
this indefinetely, so it is possible to search for moves belonging
to a journal which has a user who is member of a certain group etc.

13
web_advanced_search/static/src/css/web_advanced_search.less

@ -0,0 +1,13 @@
.o_search_options {
.o_filters_menu {
.o_filter_condition {
max-width: inherit;
.o_searchview_extended_prop_value {
.o_field_domain {
min-width: 30vw;
}
}
}
}
}

27
web_advanced_search/static/src/css/web_advanced_search_x2x.less

@ -1,27 +0,0 @@
.o_search_options {
.o_filters_menu {
.o_filter_condition {
max-width: inherit;
.o_searchview_extended_prop_value {
.ui-autocomplete-input {
.form-control();
}
.oe_m2o_drop_down_button {
top: 6px;
right: 2px;
}
.o_form_field_domain {
min-width: 400px;
}
}
}
}
}
.x2x_container {
min-width: 60ex;
}

86
web_advanced_search/static/src/js/human_domain.js

@ -0,0 +1,86 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
odoo.define("web_advanced_search.human_domain", function (require) {
"use strict";
var DomainSelector = require("web.DomainSelector");
var join_mapping = {
"&": _(" and "),
"|": _(" or "),
"!": _(" is not "),
};
// HACK I should extend classes, but they are not exposed
// TODO Remove file when merged https://github.com/odoo/odoo/pull/25922
var human_domain_methods = {
DomainTree: function () {
var human_domains = [];
_.each(this.children, function (child) {
human_domains.push(
human_domain_methods[child.template].apply(child)
);
});
return _.str.sprintf(
"(%s)",
human_domains.join(join_mapping[this.operator])
);
},
DomainSelector: function () {
var result = human_domain_methods.DomainTree.apply(this, arguments);
// Remove surrounding parenthesis
return result.slice(1, -1);
},
DomainLeaf: function () {
var chain = [],
operator = this.operator_mapping[this.operator],
value = _.str.sprintf('"%s"', this.value);
// Humanize chain
this.chain.split(".").forEach(function (element, index) {
chain.push(
_.findWhere(
this.fieldSelector.pages[index],
{name: element}
).string || element
);
}, this);
// 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 = _.str.sprintf('["%s"]', this.value.join('", "'));
}
return _.str.sprintf(
"%s %s %s",
chain.join("→"),
operator || this.operator,
value
).trim();
},
};
function getHumanDomain (parent, model, domain, options) {
var domain_selector = new DomainSelector(
parent,
model,
domain,
options
);
var dummy_parent = $("<div>");
domain_selector.appendTo(dummy_parent);
var result = human_domain_methods.DomainSelector.apply(
domain_selector
);
domain_selector.destroy();
dummy_parent.destroy();
return result;
}
return {
getHumanDomain: getHumanDomain,
};
});

293
web_advanced_search/static/src/js/web_advanced_search.js

@ -0,0 +1,293 @@
/* Copyright 2015 Therp BV <http://therp.nl>
* Copyright 2017-2018 Jairo Llopis <jairo.llopis@tecnativa.com>
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
odoo.define("web_advanced_search", function (require) {
"use strict";
var core = require("web.core");
var Domain = require("web.Domain");
var DomainSelectorDialog = require("web.DomainSelectorDialog");
var field_registry = require("web.field_registry");
var FieldManagerMixin = require("web.FieldManagerMixin");
var FilterMenu = require("web.FilterMenu");
var human_domain = require("web_advanced_search.human_domain");
var SearchView = require("web.SearchView");
var Widget = require("web.Widget");
var Char = core.search_filters_registry.get("char");
SearchView.include({
custom_events: _.extend({}, SearchView.prototype.custom_events, {
"get_dataset": "_on_get_dataset",
}),
/**
* Add or update a `dataset` attribute in event target
*
* The search view dataset includes things such as the model, which
* is required to make some parts of search views smarter.
*
* @param {OdooEvent} event The target will get the dataset.
*/
_on_get_dataset: function (event) {
event.target.dataset = this.dataset;
event.stopPropagation();
},
});
/**
* An almost dummy search proposition, to use with domain widget
*/
var AdvancedSearchProposition = Widget.extend({
init: function (parent, model, domain) {
this._super(parent);
this.model = model;
this.domain = new Domain(domain);
},
get_filter: function () {
var domain_array = this.domain.toArray();
return {
attrs: {
domain: domain_array,
// TODO Remove when merged
// https://github.com/odoo/odoo/pull/25922
string: human_domain.getHumanDomain(
this,
this.model,
domain_array
),
},
children: [],
tag: "filter",
};
},
});
// Add advanced search features
FilterMenu.include({
custom_events: _.extend({}, FilterMenu.prototype.custom_events, {
"domain_selected": "advanced_search_commit",
}),
events: _.extend({}, FilterMenu.prototype.events, {
"click .o_add_advanced_search": "advanced_search_open",
}),
init: function () {
this._super.apply(this, arguments);
this.trigger_up("get_dataset");
},
/**
* Open advanced search dialog
*
* @returns {$.Deferred} The opening dialog itself.
*/
advanced_search_open: function () {
var domain_selector_dialog = new DomainSelectorDialog(
this,
this.dataset.model,
"[]",
{
debugMode: core.debug,
readonly: false,
}
);
// Add 1st domain node by default
domain_selector_dialog.domainSelector._onAddFirstButtonClick();
return domain_selector_dialog.open();
},
/**
* Apply advanced search on dialog save
*
* @param {OdooEvent} event A `domain_selected` event from the dialog.
*/
advanced_search_commit: function (event) {
_.invoke(this.propositions, "destroy");
var proposition = new AdvancedSearchProposition(
this,
this.dataset.model,
event.data.domain
);
this.propositions = [proposition];
this.commit_search();
},
});
/**
* 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.
*/
var Relational = Char.extend(FieldManagerMixin, {
tagName: "div",
className: "x2x_container",
attributes: {},
init: function () {
this._super.apply(this, arguments);
// To make widgets work, we need a model and an empty record
FieldManagerMixin.init.call(this);
this.trigger_up("get_dataset");
// Make equal and not equal appear 1st and 2nd
this.operators = _.sortBy(
this.operators,
function(op) {
switch(op.value) {
case "=":
return -2;
case "!=":
return -1;
default:
return 0;
}
});
// Create dummy record with only the field the user is searching
var params = {
fieldNames: [this.field.name],
modelName: this.dataset.model,
context: this.dataset.context,
// res_id: "virtual_0",
fields: {},
type: "record",
viewType: "default",
fieldsInfo: {
default: {},
},
};
// See https://stackoverflow.com/a/11508530/1468388
params.fields[this.field.name] = _.omit(this.field, "onChange");
params.fieldsInfo.default[this.field.name] = {};
// Emulate `model.load()`, without RPC-calling `default_get()`
this.datapoint_id = this.model._makeDataPoint(params).id;
this.model.applyDefaultValues(
this.datapoint_id,
{},
params.fieldNames
);
// To generate a new fake ID
this._fake_id = -1;
},
start: function () {
var result = this._super.apply(this, arguments);
// Render the initial widget
result.done($.proxy(this, "show_inputs", $("<input value='='/>")));
return result;
},
destroy: function () {
if (this._field_widget) {
this._field_widget.destroy();
}
this.model.destroy();
delete this.record;
return this._super.apply(this, arguments);
},
_get_record: function () {
return this.model.get(this.datapoint_id);
},
show_inputs: function ($operator) {
// Get widget class to be used
switch ($operator.val()) {
case "=":
case "!=":
this._field_widget_name = "many2one";
break;
default:
this._field_widget_name = "char";
}
var _Widget = field_registry.get(this._field_widget_name);
// Destroy previous widget, if any
if (this._field_widget) {
this._field_widget.destroy();
delete this._field_widget;
}
// Create new widget
var options = {
mode: "edit",
attrs: {
options: {
no_create_edit: true,
no_create: true,
no_open: true,
no_quick_create: true,
},
},
};
this._field_widget = new _Widget(
this,
this.field.name,
this._get_record(),
options
);
this._field_widget.appendTo(this.$el);
return this._super.apply(this, arguments);
},
_applyChanges: function (dataPointID, changes, event) {
// Make char updates look like valid x2one updates
if (_.isNaN(changes[this.field.name].id)) {
changes[this.field.name] = {
id: this._fake_id--,
display_name: event.target.lastSetValue,
};
}
return FieldManagerMixin._applyChanges.apply(this, arguments);
},
_confirmChange: function (id, fields, event) {
this.datapoint_id = id;
return this._field_widget.reset(this._get_record(), event);
},
get_value: function () {
try {
switch (this._field_widget_name) {
case "many2one":
return this._field_widget.value.res_id;
default:
return this._field_widget.value.data.display_name;
}
} catch (error) {
if (error.name === "TypeError") {
return false;
}
}
},
toString: function () {
try {
switch (this._field_widget_name) {
case "many2one":
return this._field_widget.value.data.display_name;
}
return this._super.apply(this, arguments);
} catch (error) {
if (error.name === "TypeError") {
return "";
}
}
},
});
// Register search filter widgets
core.search_filters_registry
.add("many2many", Relational)
.add("many2one", Relational)
.add("one2many", Relational);
return {
AdvancedSearchProposition: AdvancedSearchProposition,
Relational: Relational,
};
});

185
web_advanced_search/static/src/js/web_advanced_search_x2x.js

@ -1,185 +0,0 @@
/* Copyright 2015 Therp BV <http://therp.nl>
* Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
* Copyright 2018 Jose Bernet <josemaria.bernet@guadaltech.es>
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
odoo.define('web_advanced_search_x2x', function (require) {
"use strict";
var core = require('web.core');
var DomainSelector = require('web.DomainSelector');
var Domain = require("web.Domain");
var FieldManagerMixin = require('web.FieldManagerMixin');
var Char = core.search_filters_registry.get("char");
var X2XAdvancedSearchPropositionMixin = {
template: "web_advanced_search_x2x.proposition",
events: {
// If click on the node add or delete button, notify the parent and let
// it handle the addition/removal
"click .o_domain_tree_operator_caret": "_openCaret"
},
_openCaret: function (e) {
var selectorClass = $('.o_domain_tree_operator_selector');
if (selectorClass.hasClass('open')) {
selectorClass.removeClass('open');
} else {
selectorClass.addClass('open');
}
},
init: function (parent, options) {
// Make equal and not equal appear 1st and 2nd
this.relation = options.relation;
this.type = options.type;
this.field_name = options.name;
this.name = parent.name;
this.operators = _.sortBy(
this.operators,
function (op) {
switch (op.value) {
case '=':
return -2;
case '!=':
return -1;
default:
return 0;
}
});
// Append domain operator
this.operators.push({
'value': 'domain', 'text': core._lt('is in selection'),
});
// Avoid hiding filter when using special widgets
this.events = $.extend({}, this.events, {
click: function (event) {
event.stopPropagation();
}
});
return this._super.apply(this, arguments);
},
get_field_desc: function () {
return this.field;
},
/**
* Add x2x widget after rendering.
*/
renderElement: function () {
var result = this._super.apply(this, arguments);
if (this.x2x_widget_name()) {
this.x2x_field().appendTo(this.$el);
}
return result;
},
/**
* Re-render widget when operator changes.
*/
show_inputs: function () {
this.renderElement();
return this._super.apply(this, arguments);
},
/**
* Create a relational field for the user.
*
* @return {Field}
*/
x2x_field: function () {
if (this._x2x_field) {
this._x2x_field.destroy();
delete this._x2x_field;
}
var widget = this.x2x_widget();
if (!widget) return;
this._x2x_field = new DomainSelector(this, this.relation, [], {readonly: false});
return this._x2x_field;
},
x2x_value_changed: function () {
switch (this.x2x_widget_name()) {
case "char":
// Apply domain when selected
this.getParent().getParent().commit_search();
break;
}
},
x2x_widget: function () {
var name = this.x2x_widget_name();
return name && core.search_filters_registry.get(name);
},
/**
* Return the widget that should be used to render this proposition.
*
* If it returns `undefined`, it means you should use a simple
* `<input type="text"/>`.
*/
x2x_widget_name: function () {
switch (this.get_operator()) {
case "=":
case "!=":
return undefined;
case "domain":
return "many2one";
}
},
get_domain: function () {
// Special way to get domain if user chose "domain" filter
if (this.get_operator() == "domain") {
var domain = this._x2x_field.getDomain();
var field_name = this.field_name;
$.each(domain, function (index, value) {
if (domain[index].constructor == Array) {
domain[index][0] = field_name + '.' + domain[index][0]
}
});
return domain;
} else {
return this._super.apply(this, arguments);
}
},
get_operator: function () {
return !this.isDestroyed() &&
this.getParent().$('.o_searchview_extended_prop_op').val();
},
get_value: function () {
try {
if (!this.x2x_widget_name()) {
throw "No x2x widget, fallback to default";
}
var domain = this._x2x_field.getDomain();
return Domain.prototype.arrayToString(domain)
} catch (error) {
return this._super.apply(this, arguments);
}
}
};
var affected_types = ["one2many", "many2one", "many2many"],
X2XAdvancedSearchProposition = Char.extend(
FieldManagerMixin,
X2XAdvancedSearchPropositionMixin
);
// Register this search proposition for relational fields
$.each(affected_types, function (index, value) {
core.search_filters_registry.add(value, X2XAdvancedSearchProposition);
});
return {
X2XAdvancedSearchPropositionMixin: X2XAdvancedSearchPropositionMixin,
X2XAdvancedSearchProposition: X2XAdvancedSearchProposition,
};
});

13
web_advanced_search/static/src/xml/web_advanced_search.xml

@ -0,0 +1,13 @@
<?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-extend="SearchView.FilterMenu">
<t t-jquery=".o_filters_menu" t-operation="append">
<li class="divider"/>
<li>
<a class="o_add_advanced_search">Add Advanced Filter</a>
</li>
</t>
</t>
</templates>

13
web_advanced_search/static/src/xml/web_advanced_search_x2x.xml

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<templates>
<t t-name="web_advanced_search_x2x.proposition">
<t t-if="widget.x2x_widget_name()">
<div class="x2x_container"/>
</t>
<t t-if="!widget.x2x_widget_name()">
<input type="text"/>
</t>
</t>
</templates>

9
web_advanced_search/views/templates.xml

@ -1,12 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
<!-- 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" name="web_advanced_search_x2x assets" inherit_id="web.assets_backend">
<template id="assets_backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/web_advanced_search_x2x/static/src/css/web_advanced_search_x2x.less"/>
<script type="text/javascript" src="/web_advanced_search_x2x/static/src/js/web_advanced_search_x2x.js"/>
<link rel="stylesheet" href="/web_advanced_search/static/src/css/web_advanced_search.less"/>
<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/web_advanced_search.js"/>
</xpath>
</template>

Loading…
Cancel
Save