Browse Source

Merge pull request #672 from Tecnativa/10.0-web_widget_domain_v11

[ADD][web_widget_domain_v11] Domain widget backport
pull/682/head
Pedro M. Baeza 7 years ago
committed by GitHub
parent
commit
21f50a61f1
  1. 4
      web_advanced_search_x2x/__manifest__.py
  2. 14
      web_advanced_search_x2x/static/src/css/web_advanced_search_x2x.less
  3. 70
      web_advanced_search_x2x/static/src/js/web_advanced_search_x2x.js
  4. 9
      web_advanced_search_x2x/static/src/xml/web_advanced_search_x2x.xml
  5. 73
      web_widget_domain_v11/README.rst
  6. 2
      web_widget_domain_v11/__init__.py
  7. 24
      web_widget_domain_v11/__manifest__.py
  8. BIN
      web_widget_domain_v11/static/description/icon.png
  9. 87
      web_widget_domain_v11/static/src/copied-css/domain_selector.less
  10. 97
      web_widget_domain_v11/static/src/copied-css/model_field_selector.less
  11. 585
      web_widget_domain_v11/static/src/copied-js/domain_selector.js
  12. 47
      web_widget_domain_v11/static/src/copied-js/domain_selector_dialog.js
  13. 19
      web_widget_domain_v11/static/src/copied-js/domain_utils.js
  14. 337
      web_widget_domain_v11/static/src/copied-js/model_field_selector.js
  15. 166
      web_widget_domain_v11/static/src/copied-xml/templates.xml
  16. 155
      web_widget_domain_v11/static/src/js/domain_field.js
  17. 26
      web_widget_domain_v11/templates/assets.xml
  18. 19
      web_widget_domain_v11/views/ir_filters.xml

4
web_advanced_search_x2x/__manifest__.py

@ -5,7 +5,7 @@
{
"name": "Search x2x fields",
"version": "10.0.1.0.0",
"version": "10.0.2.0.0",
"author": "Therp BV, "
"Tecnativa, "
"Odoo Community Association (OCA)",
@ -13,7 +13,7 @@
"category": "Usability",
"summary": "Use a search widget in advanced search for x2x fields",
"depends": [
'web',
'web_widget_domain_v11',
],
"data": [
'views/templates.xml',

14
web_advanced_search_x2x/static/src/css/web_advanced_search_x2x.less

@ -1,7 +1,9 @@
.openerp {
.oe-search-options {
.searchview_extended_prop_value {
.oe_form {
.o_search_options {
.o_filters_menu {
.o_filter_condition {
max-width: inherit;
.o_searchview_extended_prop_value {
.ui-autocomplete-input {
.form-control();
}
@ -10,6 +12,10 @@
top: 6px;
right: 2px;
}
.o_form_field_domain {
min-width: 400px;
}
}
}
}

70
web_advanced_search_x2x/static/src/js/web_advanced_search_x2x.js

@ -15,8 +15,8 @@ odoo.define('web_advanced_search_x2x.search_filters', function (require) {
var X2XAdvancedSearchPropositionMixin = {
template: "web_advanced_search_x2x.proposition",
init: function()
{
init: function () {
// Make equal and not equal appear 1st and 2nd
this.operators = _.sortBy(
this.operators,
@ -38,28 +38,35 @@ odoo.define('web_advanced_search_x2x.search_filters', function (require) {
});
return this._super.apply(this, arguments);
},
get_field_desc: function()
{
return this.field;
},
/**
* Add the right relational field to the template.
* Add x2x widget after rendering.
*/
renderElement: function () {
try {
this._x2x_field.destroy();
} catch (error) {}
this.relational = this.x2x_widget_name();
this._super.apply(this, arguments);
if (this.relational) {
renderElement: function() {
var result = this._super.apply(this, arguments);
if (this.x2x_widget_name()) {
this.x2x_field().appendTo(this.$el);
this._x2x_field.$el.on(
"autocompleteopen",
this.proxy('x2x_autocomplete_open')
);
}
delete this.relational;
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.
*
@ -77,12 +84,13 @@ odoo.define('web_advanced_search_x2x.search_filters', function (require) {
this.x2x_field_create_options()
);
this._x2x_field.on(
"change:value",
"domain_selected",
this,
this.proxy("x2x_value_changed")
);
return this._x2x_field;
},
x2x_field_create_options: function () {
return {
attrs: {
@ -95,6 +103,7 @@ odoo.define('web_advanced_search_x2x.search_filters', function (require) {
},
};
},
x2x_value_changed: function () {
switch (this.x2x_widget_name()) {
case "char_domain":
@ -103,10 +112,18 @@ odoo.define('web_advanced_search_x2x.search_filters', function (require) {
break;
}
},
x2x_widget: function () {
var name = this.x2x_widget_name();
return name && core.form_widget_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 "=":
@ -116,6 +133,7 @@ odoo.define('web_advanced_search_x2x.search_filters', function (require) {
return "char_domain";
}
},
x2x_autocomplete_open: function()
{
var widget = this._x2x_field.$input.autocomplete("widget");
@ -123,6 +141,7 @@ odoo.define('web_advanced_search_x2x.search_filters', function (require) {
event.stopPropagation();
});
},
get_domain: function () {
// Special way to get domain if user chose "domain" filter
if (this.get_operator() == "domain") {
@ -141,10 +160,12 @@ odoo.define('web_advanced_search_x2x.search_filters', function (require) {
return this._super.apply(this, arguments);
}
},
get_operator: function () {
return !this.isDestroyed() &&
this.getParent().$('.o_searchview_extended_prop_op').val();
},
get_value: function () {
try {
return this._x2x_field.get_value();
@ -152,6 +173,7 @@ odoo.define('web_advanced_search_x2x.search_filters', function (require) {
return this._super.apply(this, arguments);
}
},
format_label: function (format, field, operator) {
if (this.x2x_widget()) {
var value = String(this._x2x_field.get_value());
@ -180,30 +202,6 @@ odoo.define('web_advanced_search_x2x.search_filters', function (require) {
X2XAdvancedSearchPropositionMixin
);
ExtendedSearchProposition.include({
/**
* Force re-rendering the value widget if needed.
*/
operator_changed: function (event) {
if (this.value instanceof X2XAdvancedSearchProposition) {
this.value_rerender();
}
return this._super.apply(this, arguments);
},
/**
* Re-render proposition's value widget.
*
* @return {jQuery.Deferred}
*/
value_rerender: function () {
this.value._x2x_field && this.value._x2x_field.destroy();
delete this.value._x2x_field;
return this.value.appendTo(
this.$(".o_searchview_extended_prop_value").show().empty()
);
},
});
// Register this search proposition for relational fields
$.each(affected_types, function (index, value) {
core.search_filters_registry.add(value, X2XAdvancedSearchProposition);

9
web_advanced_search_x2x/static/src/xml/web_advanced_search_x2x.xml

@ -3,12 +3,11 @@
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.relational">
<!-- This wrapper fixes CSS styiling -->
<div class="oe_form"/>
<t t-if="widget.x2x_widget_name()">
<div class="x2x_container"/>
</t>
<t t-if="!widget.relational">
<t t-call="SearchView.extended_search.proposition" />
<t t-if="!widget.x2x_widget_name()">
<input type="text"/>
</t>
</t>
</templates>

73
web_widget_domain_v11/README.rst

@ -0,0 +1,73 @@
.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
===============================
Odoo 11.0 Domain Widget Preview
===============================
This module replaces the functionality of the domain widget to use a preview of
the brand new interface that will be found in Odoo 11.0.
Usage
=====
To use this module, you need to:
#. Enable the developer mode.
#· Go to *Settings > Technical > User interface > User-defined Filters* and
choose or create one.
#. Choose a model if there is none.
#. You will be able to choose the domain using the updated domain widget.
Install any addon that makes use of the domain widget (i.e. ``mass_mailing``)
and you will be also able to use the new widget there.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/162/10.0
Known issues / Roadmap
======================
* This addon replaces the built-in ``char_domain`` widget, so it can break
compatibility with other addons that use or extend it.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/web/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.
Credits
=======
Most code copied from https://github.com/odoo/odoo/tree/68176d80ad6053f52ed1c7bcf294ab3664986c46/addons/web/static/src
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Odoo SA <https://www.odoo.com>
* Jairo Llopis <jairo.llopis@tecnativa.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

2
web_widget_domain_v11/__init__.py

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

24
web_widget_domain_v11/__manifest__.py

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
{
"name": "Odoo 11.0 Domain Widget",
"summary": "Updated domain widget",
"version": "10.0.1.0.0",
"category": "Extra Tools",
"website": "https://www.tecnativa.com/",
"author": "Tecnativa, Odoo S.A., Odoo Community Association (OCA)",
"license": "LGPL-3",
"application": False,
"installable": True,
"depends": [
"web",
],
"data": [
"templates/assets.xml",
"views/ir_filters.xml",
],
"qweb": [
"static/src/copied-xml/templates.xml",
],
}

BIN
web_widget_domain_v11/static/description/icon.png

After

Width: 390  |  Height: 390  |  Size: 24 KiB

87
web_widget_domain_v11/static/src/copied-css/domain_selector.less

@ -0,0 +1,87 @@
.o_domain_node {
@o-domain-selector-vspace: 8px;
@o-domain-selector-indent: 32px;
@o-domain-selector-panel-space: 128px;
.o_domain_node_control_panel {
.o-position-absolute(@right: 0);
}
&.o_domain_tree {
.o_domain_tree_operator_caret::after {
.o-caret-down();
}
> .o_domain_node_children_container {
padding-left: @o-domain-selector-indent;
> div {
margin-top: @o-domain-selector-vspace;
}
}
&.o_domain_selector {
&.o_edit_mode {
position: relative;
> .o_domain_node_children_container {
padding-right: @o-domain-selector-panel-space;
}
}
> .o_domain_node_children_container {
padding-left: 0;
}
}
}
&.o_domain_leaf {
&.o_read_mode {
display: inline-block;
margin-right: 4px;
}
> .o_domain_leaf_info {
background: @odoo-brand-lightsecondary;
border: 1px solid darken(@odoo-brand-lightsecondary, 10%);
padding: 2px 4px;
.o_domain_leaf_chain, .o_domain_leaf_value {
font-weight: 700;
}
.o_domain_leaf_operator {
font-style: italic;
}
}
> .o_domain_leaf_edition {
.o-flex-display();
.o-align-items(flex-end);
> * {
width: percentage(1/3);
+ * {
margin-left: 4px;
}
}
.o_domain_leaf_value_tags {
.o-flex-display();
> * {
.o-flex(0, 0, auto);
}
> input {
.o-flex(1, 1, auto);
width: 0;
min-width: 50px;
}
.o_domain_leaf_value_remove_tag_button {
cursor: pointer;
}
}
}
}
}

97
web_widget_domain_v11/static/src/copied-css/model_field_selector.less

@ -0,0 +1,97 @@
.o_field_selector {
position: relative;
.o_field_selector_controls {
.o-position-absolute(0, 0, 1px);
.o-flex-display();
.o-align-items(center);
cursor: pointer;
&::after {
.o-caret-down();
}
}
.o_field_selector_popover {
@o-field-selector-arrow-height: 7px;
.o-position-absolute(@top: 100%, @left: 0);
z-index: 1051;
width: 265px;
margin-top: @o-field-selector-arrow-height;
background: white;
box-shadow: 0 3px 10px rgba(0,0,0,.4);
&:focus {
outline: none;
}
.o_field_selector_popover_header {
color: white;
background: @brand-primary;
font-weight: bold;
padding: 5px 0 5px 0.4em;
.o_field_selector_title {
width: 100%;
.o-text-overflow();
padding: 0px 35px;
text-align: center;
}
.o_field_selector_popover_option {
.o-position-absolute(@top: 0);
padding: 7px 8px 8px 6px;
&.o_prev_page {
left: 0;
border-right: 1px solid darken(@brand-primary, 10%);
}
&.o_field_selector_close {
right: 0;
border-left: 1px solid darken(@brand-primary, 10%);
}
&:hover {
background: darken(@brand-primary, 10%);
}
}
&:before {
.o-position-absolute(@top: -@o-field-selector-arrow-height, @left: @o-field-selector-arrow-height);
content: "";
border-left: @o-field-selector-arrow-height solid rgba(0, 0, 0, 0);
border-right: @o-field-selector-arrow-height solid rgba(0, 0, 0, 0);
border-bottom: @o-field-selector-arrow-height solid @brand-primary;
}
}
.o_field_selector_popover_body {
.o_field_selector_page {
position: relative;
max-height: 320px;
overflow: auto;
margin: 0;
padding: 0;
> .o_field_selector_item {
list-style: none;
position: relative;
padding: 5px 0 5px 0.4em;
cursor: pointer;
font-family: Arial;
font-size: 13px;
color: #444;
border-bottom: 1px solid #eee;
&.active {
background: #f5f5f5;
}
.o_field_selector_item_title {
font-size: 12px;
}
.o_field_selector_relation_icon {
.o-position-absolute(@top: 0, @right: 0, @bottom: 0);
.o-flex-display();
.o-align-items(center);
padding: 10px;
}
}
}
}
}
}

585
web_widget_domain_v11/static/src/copied-js/domain_selector.js

@ -0,0 +1,585 @@
odoo.define("web.DomainSelector", function (require) {
"use strict";
var core = require("web.core");
var datepicker = require("web.datepicker");
var domainUtils = require("web.domainUtils");
var formats = require ("web.formats");
var ModelFieldSelector = require("web.ModelFieldSelector");
var Widget = require("web.Widget");
var _t = core._t;
var _lt = core._lt;
// "child_of", "parent_of", "like", "not like", "=like", "=ilike"
// are only used if user entered them manually or if got from demo data
var operator_mapping = {
"=": _lt("is equal to"),
"!=": _lt("is not equal to"),
">": _lt("greater than"),
"<": _lt("less than"),
">=": _lt("greater than or equal to"),
"<=": _lt("less than or equal to"),
"ilike": _lt("contains"),
"not ilike": _lt("not contains"),
"in": _lt("in"),
"not in": _lt("not in"),
"child_of": _lt("child of"),
"parent_of": _lt("parent of"),
"like": "like",
"not like": "not like",
"=like": "=like",
"=ilike": "=ilike",
// custom
"set": _lt("is set"),
"not set": _lt("is not set"),
};
/// The DomainNode Widget is an abstraction for widgets which can represent and allow
/// edition of a domain (part).
var DomainNode = Widget.extend({
events: {
/// If click on the node add or delete button, notify the parent and let it handle the addition/removal
"click .o_domain_delete_node_button": function (e) {
e.preventDefault();
e.stopPropagation();
this.trigger_up("delete_node_clicked", {child: this});
},
"click .o_domain_add_node_button": function (e) {
e.preventDefault();
e.stopPropagation();
this.trigger_up("add_node_clicked", {newBranch: !!$(e.currentTarget).data("branch"), child: this});
},
},
/// A DomainNode needs a model and domain to work. It can also receives a set of options
/// @param model - a string with the model name
/// @param domain - an array of the prefix representation of the domain (or a string which represents it)
/// @param options - an object with possible values:
/// - readonly, a boolean to indicate if the widget is readonly or not (default to true)
/// - operators, a list of available operators (default to null, which indicates all of supported ones)
/// - debugMode, a boolean which is true if the widget should be in debug mode (default to false)
/// - @see ModelFieldSelector for other options
init: function (parent, model, domain, options) {
this._super.apply(this, arguments);
this.model = model;
this.options = _.extend({
readonly: true,
operators: null,
debugMode: false,
}, options || {});
this.readonly = this.options.readonly;
this.debug = this.options.debugMode;
},
/// The getDomain method is an abstract method which should returns the prefix domain
/// the widget is currently representing (an array).
getDomain: function () {},
});
/// The DomainTree is a DomainNode which can handle subdomains (a domain which is composed
/// of multiple parts). It thus will be composed of other DomainTree instances and/or leaf parts
/// of a domain (@see DomainLeaf).
var DomainTree = DomainNode.extend({
template: "DomainTree",
events: _.extend({}, DomainNode.prototype.events, {
"click .o_domain_tree_operator_selector > ul > li > a": function (e) {
e.preventDefault();
e.stopPropagation();
this.changeOperator($(e.target).data("operator"));
},
}),
custom_events: {
/// If a domain child sends a request to add a child or remove one, call the appropriate methods.
/// Propagates the event until success.
"delete_node_clicked": function (e) {
e.stopped = this.removeChild(e.data.child);
},
"add_node_clicked": function (e) {
var domain = [["id", "=", 1]];
if (e.data.newBranch) {
domain = [this.operator === "&" ? "|" : "&"].concat(domain).concat(domain);
}
e.stopped = this.addChild(domain, e.data.child);
},
},
/// @see DomainNode.init
/// The initialization of a DomainTree creates a "children" array attribute which will contain the
/// the DomainNode children. It also deduces the operator from the domain (default to "&").
/// @see DomainTree._addFlattenedChildren
init: function (parent, model, domain, options) {
this._super.apply(this, arguments);
this._initialize(domainUtils.stringToDomain(domain));
},
/// @see DomainTree.init
_initialize: function (domain) {
this.operator = domain[0];
this.children = [];
// Add flattened children by search the appropriate number of children in the rest
// of the domain (after the operator)
var nbLeafsToFind = 1;
for (var i = 1 ; i < domain.length ; i++) {
if (_.contains(["&", "|"], domain[i])) {
nbLeafsToFind++;
} else if (domain[i] !== "!") {
nbLeafsToFind--;
}
if (!nbLeafsToFind) {
var partLeft = domain.slice(1, i+1);
var partRight = domain.slice(i+1);
if (partLeft.length) {
this._addFlattenedChildren(partLeft);
}
if (partRight.length) {
this._addFlattenedChildren(partRight);
}
break;
}
}
// Mark "!" tree children so that they do not allow to add other children around them
if (this.operator === "!") {
this.children[0].noControlPanel = true;
}
},
start: function () {
this._postRender();
return $.when(this._super.apply(this, arguments), this._renderChildrenTo(this.$childrenContainer));
},
_postRender: function () {
this.$childrenContainer = this.$("> .o_domain_node_children_container");
},
_renderChildrenTo: function ($to) {
var $div = $("<div/>");
return $.when.apply($, _.map(this.children, (function (child) {
return child.appendTo($div);
}).bind(this))).then((function () {
_.each(this.children, function (child) {
child.$el.appendTo($to); // Forced to do it this way so that the children are not misordered
});
}).bind(this));
},
getDomain: function () {
var childDomains = [];
var nbChildren = 0;
_.each(this.children, function (child) {
var childDomain = child.getDomain();
if (childDomain.length) {
nbChildren++;
childDomains = childDomains.concat(child.getDomain());
}
});
var nbChildRequired = this.operator === "!" ? 1 : 2;
var operators = _.times(nbChildren - nbChildRequired + 1, _.constant(this.operator));
return operators.concat(childDomains);
},
changeOperator: function (operator) {
this.operator = operator;
this.trigger_up("domain_changed", {child: this});
},
/// The addChild method adds a domain part to the widget.
/// @param domain - an array of the prefix-like domain to build and add to the widget
/// @param afterNode - the node after which the new domain part must be added (at the end if not given)
/// @trigger_up domain_changed if the child is added
/// @return true if the part was added, false otherwise (the afterNode was not found)
addChild: function (domain, afterNode) {
var i = afterNode ? _.indexOf(this.children, afterNode) : this.children.length;
if (i < 0) return false;
this.children.splice(i+1, 0, instantiateNode(this, this.model, domain, this.options));
this.trigger_up("domain_changed", {child: this});
return true;
},
/// The removeChild method removes a given child from the widget.
/// @param oldChild - the child instance to remove
/// @trigger_up domain_changed if the child is removed
/// @return true if the child was removed, false otherwise (the widget does not own the child)
removeChild: function (oldChild) {
var i = _.indexOf(this.children, oldChild);
if (i < 0) return false;
this.children[i].destroy();
this.children.splice(i, 1);
this.trigger_up("domain_changed", {child: this});
return true;
},
/// The private _addFlattenedChildren method adds a child which represents the given
/// domain. If the child has children and that the child main domain operator is the
/// same as the current widget one, the 2-children prefix hierarchy is then simplified
/// by making the child children the widget own children.
/// @param domain - the domain of the child to add and simplify
_addFlattenedChildren: function (domain) {
var node = instantiateNode(this, this.model, domain, this.options);
if (node === null) {
return;
}
if (!node.children || node.operator !== this.operator) {
this.children.push(node);
return;
}
_.each(node.children, (function (child) {
child.setParent(this);
this.children.push(child);
}).bind(this));
node.destroy();
},
/// This method is ugly but achieves the right behavior without flickering.
/// It will be refactored alongside the new views/widget API.
_redraw: function (domain) {
var oldChildren = this.children.slice();
this._initialize(domain || this.getDomain());
return this._renderChildrenTo($("<div/>")).then((function () {
this.renderElement();
this._postRender();
_.each(this.children, (function (child) { child.$el.appendTo(this.$childrenContainer); }).bind(this));
_.each(oldChildren, function (child) { child.destroy(); });
}).bind(this));
},
});
/// The DomainSelector widget can be used to build prefix char domain. It is the DomainTree
/// specialization to use to have a fully working widget.
///
/// Known limitations:
/// - Some operators like "child_of", "parent_of", "like", "not like", "=like", "=ilike"
/// will come only if you use them from demo data or debug input.
/// - Some kind of domain can not be build right now e.g ("country_id", "in", [1,2,3,4])
/// but you can insert from debug input.
var DomainSelector = DomainTree.extend({
template: "DomainSelector",
events: _.extend({}, DomainTree.prototype.events, {
"click .o_domain_add_first_node_button": function (e) {
this.addChild([["id", "=", 1]]);
},
/// When the debug input changes, the string prefix domain is read. If it is syntax-valid
/// the widget is re-rendered and notifies the parents. If not, a warning is shown to the
/// user and the input is ignored.
"change .o_domain_debug_input": function (e) {
var domain;
try {
domain = domainUtils.stringToDomain($(e.currentTarget).val());
} catch (err) {
this.do_warn(_t("Syntax error"), _t("The domain you entered is not properly formed"));
return;
}
this._redraw(domain).then((function () {
this.trigger_up("domain_changed", {child: this, alreadyRedrawn: true});
}).bind(this));
},
}),
custom_events: _.extend({}, DomainTree.prototype.custom_events, {
/// If a subdomain notifies that it underwent some modifications, the DomainSelector
/// catches the message and performs a full re-rendering.
"domain_changed": function (e) {
e.stopped = false;
if (!e.data.alreadyRedrawn) {
this._redraw();
}
},
}),
_initialize: function (domain) {
// Check if the domain starts with implicit "&" operators and make them
// explicit. As the DomainSelector is a specialization of a DomainTree,
// it is waiting for a tree and not a leaf. So [] and [A] will be made
// explicit with ["&"], ["&", A] so that tree parsing is made correctly.
// Note: the domain is considered to be a valid one
if (domain.length <= 1) {
return this._super(["&"].concat(domain));
}
var expected = 1;
_.each(domain, function (item) {
if (item === "&" || item === "|") {
expected++;
} else if (item !== "!") {
expected--;
}
});
if (expected < 0) {
domain = _.times(Math.abs(expected), _.constant("&")).concat(domain);
}
return this._super(domain);
},
_postRender: function () {
this._super.apply(this, arguments);
// Display technical domain if in debug mode
this.$debugInput = this.$(".o_domain_debug_input");
if (this.$debugInput.length) {
this.$debugInput.val(domainUtils.domainToString(this.getDomain()));
}
},
});
/// The DomainLeaf widget is a DomainNode which handles a domain which cannot be split in
/// another subdomains, i.e. composed of a field chain, an operator and a value.
var DomainLeaf = DomainNode.extend({
template: "DomainLeaf",
events: _.extend({}, DomainNode.prototype.events, {
"change .o_domain_leaf_operator_select": function (e) {
this.onOperatorChange($(e.currentTarget).val());
},
"change .o_domain_leaf_value_input": function (e) {
if (e.currentTarget !== e.target) return;
this.onValueChange($(e.currentTarget).val());
},
// Handle the tags widget part (TODO should be an independant widget)
"click .o_domain_leaf_value_add_tag_button": "on_add_tag",
"keyup .o_domain_leaf_value_tags input": "on_add_tag",
"click .o_domain_leaf_value_remove_tag_button": "on_remove_tag",
}),
custom_events: {
"field_chain_changed": function (e) {
this.onChainChange(e.data.chain);
},
},
/// @see DomainNode.init
init: function (parent, model, domain, options) {
this._super.apply(this, arguments);
domain = domainUtils.stringToDomain(domain);
this.chain = domain[0][0];
this.operator = domain[0][1];
this.value = domain[0][2];
this.operator_mapping = operator_mapping;
},
willStart: function () {
var defs = [this._super.apply(this, arguments)];
if (!this.readonly) {
// In edit mode, instantiate a field selector. This is done here in willStart and prepared by
// appending it to a dummy element because the DomainLeaf rendering need some information which
// cannot be computed before the ModelFieldSelector is fully rendered (TODO).
this.fieldSelector = new ModelFieldSelector(this, this.model, this.chain, this.options);
defs.push(this.fieldSelector.appendTo($("<div/>")).then((function () {
var wDefs = [];
// Set list of operators according to field type
this.operators = this._getOperatorsFromType(this.fieldSelector.selectedField.type);
if (_.contains(["child_of", "parent_of", "like", "not like", "=like", "=ilike"], this.operator)) {
// In case user entered manually or from demo data
this.operators[this.operator] = operator_mapping[this.operator];
} else if (!this.operators[this.operator]) {
this.operators[this.operator] = "?"; // In case the domain uses an unsupported operator for the field type
}
// Set list of values according to field type
this.selectionChoices = null;
if (this.fieldSelector.selectedField.type === "boolean") {
this.selectionChoices = [["1", "set (true)"], ["0", "not set (false)"]];
} else if (this.fieldSelector.selectedField.type === "selection") {
this.selectionChoices = this.fieldSelector.selectedField.selection;
}
// Adapt display value and operator for rendering
this.displayValue = this.value;
try {
var f = this.fieldSelector.selectedField;
if (!f.relation) { // TODO in this case, the value should be m2o input, etc...
this.displayValue = formats.format_value(this.value, this.fieldSelector.selectedField);
}
} catch (err) {/**/}
this.displayOperator = this.operator;
if (this.fieldSelector.selectedField.type === "boolean") {
this.displayValue = this.value ? "1" : "0";
} else if ((this.operator === "!=" || this.operator === "=") && this.value === false) {
this.displayOperator = this.operator === "!=" ? "set" : "not set";
}
// TODO the value could be a m2o input, etc...
if (_.contains(["date", "datetime"], this.fieldSelector.selectedField.type)) {
this.valueWidget = new (this.fieldSelector.selectedField.type === "datetime" ? datepicker.DateTimeWidget : datepicker.DateWidget)(this);
wDefs.push(this.valueWidget.appendTo("<div/>").then((function () {
this.valueWidget.$el.addClass("o_domain_leaf_value_input");
this.valueWidget.set_value(this.value);
this.valueWidget.on("datetime_changed", this, function () {
this.onValueChange(this.valueWidget.get_value());
});
}).bind(this)));
}
return $.when.apply($, wDefs);
}).bind(this)));
}
return $.when.apply($, defs);
},
start: function () {
if (!this.readonly) { // In edit mode ...
this.fieldSelector.$el.prependTo(this.$("> .o_domain_leaf_edition")); // ... place the field selector
if (this.valueWidget) { // ... and place the value widget if any
this.$(".o_domain_leaf_value_input").replaceWith(this.valueWidget.$el);
}
}
return this._super.apply(this, arguments);
},
getDomain: function () {
return [[this.chain, this.operator, this.value]];
},
/// The onChainChange method handles a field chain change in the domain. In that case, the operator
/// should be adapted to a valid one for the new field and the value should also be adapted to the
/// new field and/or operator.
/// @param chain - the new field chain (string)
/// @param silent - true if the method call should not trigger_up a domain_changed event
/// @trigger_up domain_changed event to ask for a re-rendering
onChainChange: function (chain, silent) {
this.chain = chain;
var operators = this._getOperatorsFromType(this.fieldSelector.selectedField.type);
if (operators[this.operator] === undefined) {
this.onOperatorChange("=", true);
}
this.onValueChange(this.value, true);
if (!silent) this.trigger_up("domain_changed", {child: this});
},
/// The onOperatorChange method handles an operator change in the domain. In that case, the value
/// should be adapted to a valid one for the new operator.
/// @param operator - the new operator
/// @param silent - true if the method call should not trigger_up a domain_changed event
/// @trigger_up domain_changed event to ask for a re-rendering
onOperatorChange: function (operator, silent) {
this.operator = operator;
if (_.contains(["set", "not set"], this.operator)) {
this.operator = this.operator === "not set" ? "=" : "!=";
this.value = false;
} else if (_.contains(["in", "not in"], this.operator)) {
this.value = _.isArray(this.value) ? this.value : this.value ? ("" + this.value).split(",") : [];
} else {
if (_.isArray(this.value)) {
this.value = this.value.join(",");
}
this.onValueChange(this.value, true);
}
if (!silent) this.trigger_up("domain_changed", {child: this});
},
/// The onValueChange method handles a formatted value change in the domain. In that case, the value
/// should be adapted to a valid technical one.
/// @param value - the new formatted value
/// @param silent - true if the method call should not trigger_up a domain_changed event
/// @trigger_up domain_changed event to ask for a re-rendering
onValueChange: function (value, silent) {
var couldNotParse = false;
try {
this.value = formats.parse_value(value, this.fieldSelector.selectedField);
} catch (err) {
this.value = value;
couldNotParse = true;
}
if (this.fieldSelector.selectedField.type === "boolean") {
if (!_.isBoolean(this.value)) { // Convert boolean-like value to boolean
this.value = !!parseFloat(this.value);
}
} else if (this.fieldSelector.selectedField.type === "selection") {
if (!_.some(this.fieldSelector.selectedField.selection, (function (option) { return option[0] === this.value; }).bind(this))) {
this.value = this.fieldSelector.selectedField.selection[0][0];
}
} else if (_.contains(["date", "datetime"], this.fieldSelector.selectedField.type)) {
if (couldNotParse || _.isBoolean(this.value)) {
this.value = formats.parse_value(formats.format_value(Date.now(), this.fieldSelector.selectedField), this.fieldSelector.selectedField);
}
} else {
if (_.isBoolean(this.value)) { // Never display "true" or "false" strings from boolean value
this.value = "";
}
}
if (!silent) this.trigger_up("domain_changed", {child: this});
},
/// The private _getOperatorsFromType returns the mapping of "technical operator" to "display operator value"
/// of the operators which are available for the given field type.
_getOperatorsFromType: function (type) {
var operators = {};
switch (type) {
case "boolean":
operators = {
"=": _t("is"),
"!=": _t("is not"),
};
break;
case "char":
case "text":
case "html":
operators = _.pick(operator_mapping, "=", "!=", "ilike", "not ilike", "set", "not set", "in", "not in");
break;
case "many2many":
case "one2many":
case "many2one":
operators = _.pick(operator_mapping, "=", "!=", "ilike", "not ilike", "set", "not set");
break;
case "integer":
case "float":
case "monetary":
operators = _.pick(operator_mapping, "=", "!=", ">", "<", ">=", "<=", "ilike", "not ilike", "set", "not set");
break;
case "selection":
operators = _.pick(operator_mapping, "=", "!=", "set", "not set");
break;
case "date":
case "datetime":
operators = _.pick(operator_mapping, "=", "!=", ">", "<", ">=", "<=", "set", "not set");
break;
default:
operators = _.extend({}, operator_mapping);
break;
}
if (this.options.operators) {
operators = _.pick.apply(_, [operators].concat(this.options.operators));
}
return operators;
},
on_add_tag: function (e) {
if (e.type === "keyup" && e.which !== $.ui.keyCode.ENTER) return;
if (!_.contains(["not in", "in"], this.operator)) return;
var values = _.isArray(this.value) ? this.value.slice() : [];
var $input = this.$(".o_domain_leaf_value_tags input");
var val = $input.val().trim();
if (val && values.indexOf(val) < 0) {
values.push(val);
_.defer(this.onValueChange.bind(this, values));
$input.focus();
}
},
on_remove_tag: function (e) {
var values = _.isArray(this.value) ? this.value.slice() : [];
var val = this.$(e.currentTarget).data("value");
var index = values.indexOf(val);
if (index >= 0) {
values.splice(index, 1);
_.defer(this.onValueChange.bind(this, values));
}
},
});
/// The instantiateNode function instantiates a DomainTree if the given domain contains
/// several parts and a DomainLeaf if it only contains one part. Returns null otherwise.
function instantiateNode(parent, model, domain, options) {
if (domain.length > 1) {
return new DomainTree(parent, model, domain, options);
} else if (domain.length === 1) {
return new DomainLeaf(parent, model, domain, options);
}
return null;
}
return DomainSelector;
});

47
web_widget_domain_v11/static/src/copied-js/domain_selector_dialog.js

@ -0,0 +1,47 @@
odoo.define("web.DomainSelectorDialog", function (require) {
"use strict";
var core = require("web.core");
var Dialog = require("web.Dialog");
var DomainSelector = require("web.DomainSelector");
var _t = core._t;
return Dialog.extend({
init: function (parent, model, domain, options) {
this.model = model;
this.options = _.extend({
readonly: true,
debugMode: false,
}, options || {});
var buttons;
if (this.options.readonly) {
buttons = [
{text: _t("Close"), close: true},
];
} else {
buttons = [
{text: _t("Save"), classes: "btn-primary", close: true, click: function () {
this.trigger_up("domain_selected", {domain: this.domainSelector.getDomain()});
}},
{text: _t("Discard"), close: true},
];
}
this._super(parent, _.extend({}, {
title: _t("Domain"),
buttons: buttons,
}, options || {}));
this.domainSelector = new DomainSelector(this, model, domain, options);
},
start: function () {
this.$el.css("overflow", "visible").closest(".modal-dialog").css("height", "auto"); // This restores default modal height (bootstrap) and allows field selector to overflow
return $.when(
this._super.apply(this, arguments),
this.domainSelector.appendTo(this.$el)
);
},
});
});

19
web_widget_domain_v11/static/src/copied-js/domain_utils.js

@ -0,0 +1,19 @@
odoo.define("web.domainUtils", function (require) {
"use strict";
var pyeval = require("web.pyeval");
function domainToString(domain) {
if (_.isString(domain)) return domain;
return JSON.stringify(domain || []).replace(/false/g, "False").replace(/true/g, "True");
}
function stringToDomain(domain) {
if (!_.isString(domain)) return domain;
return pyeval.eval("domain", domain || "[]");
}
return {
domainToString: domainToString,
stringToDomain: stringToDomain,
};
});

337
web_widget_domain_v11/static/src/copied-js/model_field_selector.js

@ -0,0 +1,337 @@
odoo.define("web.ModelFieldSelector", function (require) {
"use strict";
var core = require("web.core");
var Model = require("web.DataModel");
var Widget = require("web.Widget");
var _t = core._t;
/// The ModelFieldSelector widget can be used to select a particular field chain from a given model.
var ModelFieldSelector = Widget.extend({
template: "FieldSelector",
events: {
// Handle popover opening and closing
"focusin": function () {
clearTimeout(this._hidePopoverTimeout);
this.showPopover();
},
"focusout": function () {
this._hidePopoverTimeout = _.defer(this.hidePopover.bind(this));
},
"click .o_field_selector_close": "hidePopover",
// Handle popover field navigation
"click .o_field_selector_prev_page": "goToPrevPage",
"click .o_field_selector_next_page": function (e) {
e.stopPropagation();
this.goToNextPage(this._getLastPageField($(e.currentTarget).data("name")));
},
"click li.o_field_selector_select_button": function (e) {
e.stopPropagation();
this.selectField(this._getLastPageField($(e.currentTarget).data("name")));
},
// Handle a direct change in the debug input
"change input": function() {
var userChain = this.$input.val();
if (!this.options.followRelations) {
var fields = userChain.split(".");
if (fields.length > 1) {
this.do_warn(_t("Relation not allowed"), _t("You cannot follow relations for this field chain construction"));
userChain = fields[0];
}
}
this.setChain(userChain);
this.validate(true);
this._prefill().then(this.displayPage.bind(this, ""));
this.trigger_up("field_chain_changed", {chain: this.chain});
},
// Handle keyboard and mouse navigation to build the field chain
"mouseover li.o_field_selector_item": function (e) {
this.$("li.o_field_selector_item").removeClass("active");
$(e.currentTarget).addClass("active");
},
"keydown": function (e) {
if (!this.$popover.is(":visible")) return;
var inputHasFocus = this.$input.is(":focus");
switch (e.which) {
case $.ui.keyCode.UP:
case $.ui.keyCode.DOWN:
e.preventDefault();
var $active = this.$("li.o_field_selector_item.active");
var $to = $active[e.which === $.ui.keyCode.DOWN ? "next" : "prev"](".o_field_selector_item");
if ($to.length) {
$active.removeClass("active");
$to.addClass("active");
this.$popover.focus();
var $page = $to.closest(".o_field_selector_page");
var full_height = $page.height();
var el_position = $to.position().top;
var el_height = $to.outerHeight();
var current_scroll = $page.scrollTop();
if (el_position < 0) {
$page.scrollTop(current_scroll - el_height);
} else if (full_height < el_position + el_height) {
$page.scrollTop(current_scroll + el_height);
}
}
break;
case $.ui.keyCode.RIGHT:
if (inputHasFocus) break;
e.preventDefault();
var name = this.$("li.o_field_selector_item.active").data("name");
if (name) {
var field = this._getLastPageField(name);
if (field.relation) {
this.goToNextPage(field);
}
}
break;
case $.ui.keyCode.LEFT:
if (inputHasFocus) break;
e.preventDefault();
this.goToPrevPage();
break;
case $.ui.keyCode.ESCAPE:
e.stopPropagation();
this.hidePopover();
break;
case $.ui.keyCode.ENTER:
if (inputHasFocus) break;
e.preventDefault();
this.selectField(this._getLastPageField(this.$("li.o_field_selector_item.active").data("name")));
break;
}
},
},
/// The ModelFieldSelector requires a model and a initial field chain to work with.
/// @param model - a string with the model name (e.g. "res.partner")
/// @param chain - a string with the initial field chain (e.g. "company_id.name")
/// @param options - an object with several options:
/// - filters: an object which contains suboptions which determine the fields which are used
/// - searchable: a boolean which is true if only the searchable fields have to be used (true by default)
/// - fields: the list of fields info to use when no relation has been followed (default to null,
/// which indicates that the widget has to request the model fields itself)
/// - followRelations: allow to follow relation when building the chain (true by default)
/// - debugMode: a boolean which is true if the widget is in debug mode (false by default)
init: function (parent, model, chain, options) {
this._super.apply(this, arguments);
this.model = model;
this.chain = chain;
this.options = _.extend({
filters: {},
fields: null,
followRelations: true,
debugMode: false,
}, options || {});
this.options.filters = _.extend({
searchable: true,
}, this.options.filters);
this.pages = [];
this.selectedField = false;
this.isSelected = true;
this.dirty = false;
},
willStart: function () {
return $.when(
this._super.apply(this, arguments),
this._prefill()
);
},
start: function () {
this.$input = this.$("input");
this.$popover = this.$(".o_field_selector_popover");
this.displayPage();
return this._super.apply(this, arguments);
},
/// The setChain method saves a new field chain string and displays it in the DOM input element.
/// @param chain - the new field chain string
setChain: function (chain) {
this.chain = chain;
this.$input.val(this.chain);
},
/// The addChainNode method adds a field name to the current field chain.
/// @param fieldName - the new field name to add at the end of the current field chain
addChainNode: function (fieldName) {
this.dirty = true;
if (this.isSelected) {
this.removeChainNode();
this.isSelected = false;
}
if (!this.valid) {
this.setChain("");
this.validate(true);
}
this.setChain((this.chain ? (this.chain + ".") : "") + fieldName);
},
/// The removeChainNode method removes the last field name at the end of the current field chain.
removeChainNode: function () {
this.dirty = true;
this.setChain(this.chain.substring(0, this.chain.lastIndexOf(".")));
},
/// The validate method toggles the valid status of the widget and display the error message if it
/// is not valid.
/// @param valid - a boolean which is true if the widget is valid
validate: function (valid) {
this.$(".o_field_selector_warning").toggleClass("hidden", valid);
this.valid = valid;
},
/// The showPopover method shows the popover to select the field chain. It prepares the popover pages
/// before actually showing it. (if already open, does nothing)
showPopover: function () {
if (this._isOpen) return;
this._isOpen = true;
this._prefill().then((function () {
this.displayPage();
this.$popover.removeClass("hidden");
}).bind(this));
},
/// The hidePopover method closes the popover and mark the field as selected. If the field chain changed,
/// it notifies its parents. (if not open, does nothing)
hidePopover: function () {
if (!this._isOpen) return;
this._isOpen = false;
this.$popover.addClass("hidden");
this.isSelected = true;
if (this.dirty) {
this.trigger_up("field_chain_changed", {chain: this.chain});
this.dirty = false;
}
},
/// The private _prefill method prepares the popover by filling its pages according to the current field chain.
/// @return a deferred which is resolved once the last page is shown
_prefill: function () {
this.pages = [];
return this._pushPageData(this.model).then((function() {
return (this.chain ? processChain.call(this, this.chain.split(".").reverse()) : $.when());
}).bind(this));
function processChain(chain) {
var field = this._getLastPageField(chain.pop());
if (field && field.relation && chain.length > 0) { // Fetch next chain node if any and possible
return this._pushPageData(field.relation).then(processChain.bind(this, chain));
} else if (field && chain.length === 0) { // Last node fetched, save it
this.selectedField = field;
this.validate(true);
} else { // Wrong node chain
this.validate(false);
}
return $.when();
}
},
/// The private _pushPageData method gets the field of a particular model and adds them for the new
/// last popover page.
/// @param model - the model name whose fields have to be fetched
/// @return a deferred which is resolved once the fields have been added
_pushPageData: function (model) {
var def;
if (this.model === model && this.options.fields) {
def = $.when(sortFields(this.options.fields));
} else {
def = fieldsCache.getFields(model, this.options.filters);
}
return def.then((function (fields) {
this.pages.push(fields);
}).bind(this));
},
/// The displayPage method shows the last page content of the popover. It also adapts the title according
/// to the previous page.
/// @param animation - an optional animation class to add to the page
displayPage: function (animation) {
this.$(".o_field_selector_prev_page").toggleClass("hidden", this.pages.length === 1);
var page = _.last(this.pages);
var title = "";
if (this.pages.length > 1) {
var chainParts = this.chain.split(".");
var prevField = _.findWhere(this.pages[this.pages.length - 2], {
name: this.isSelected ? chainParts[chainParts.length - 2] : _.last(chainParts),
});
if (prevField) title = prevField.string;
}
this.$(".o_field_selector_popover_header .o_field_selector_title").text(title);
this.$(".o_field_selector_page").replaceWith(core.qweb.render("FieldSelector.page", {
lines: page,
followRelations: this.options.followRelations,
animation: animation,
debug: this.options.debugMode,
}));
},
/// The goToPrevPage method removes the last page, adapts the field chain and displays the new last page.
goToPrevPage: function () {
if (this.pages.length <= 1) return;
this.pages.pop();
this.removeChainNode();
this.selectedField = this._getLastPageField(_.last(this.chain.split(".")));
this.displayPage("o_animate_slide_left");
},
/// The goToNextPage method adds a new page to the popover following the given field relation and adapts
/// the chain node according to this given field.
/// @param field - the field to add to the chain node
goToNextPage: function (field) {
this.addChainNode(field.name);
this.selectedField = field;
this._pushPageData(field.relation).then(this.displayPage.bind(this, "o_animate_slide_right"));
},
/// The selectField method selects the given field and adapts the chain node according to it. It also closes
/// the popover and thus notifies the parents about the change.
/// @param field - the field to select
selectField: function (field) {
this.addChainNode(field.name);
this.selectedField = field;
this.hidePopover();
},
/// The private _getLastPageField search a field in the last page by its name.
/// @return the field data (an object) found in the last popover page thanks to its name
_getLastPageField: function (name) {
return _.findWhere(_.last(this.pages), {
name: name,
});
},
});
/// Field Selector Cache
///
/// * Stores fields per model used in field selector
/// * Apply filters on the fly
var fieldsCache = {
cache: {},
cacheDefs: {},
getFields: function (model, filters) {
return (this.cacheDefs[model] ? this.cacheDefs[model] : this.updateCache(model)).then((function () {
return this.filter(model, filters);
}).bind(this));
},
updateCache: function (model) {
this.cacheDefs[model] = new Model(model).call("fields_get", [
false,
["store", "searchable", "type", "string", "relation", "selection", "related"],
]).then((function (fields) {
this.cache[model] = sortFields(fields);
}).bind(this));
return this.cacheDefs[model];
},
filter: function (model, filters) {
return _.filter(this.cache[model], function (f) {
return !filters.searchable || f.searchable;
});
},
};
function sortFields(fields) {
return _.chain(fields)
.pairs()
.sortBy(function (p) { return p[1].string; })
.map(function (p) { return _.extend({name: p[0]}, p[1]); })
.value();
}
return ModelFieldSelector;
});

166
web_widget_domain_v11/static/src/copied-xml/templates.xml

@ -0,0 +1,166 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<div t-name="FieldDomain" t-attf-class="o_form_field_domain#{widget.get('effective_readonly') ? '' : ' o_edit_mode'}#{widget.options.in_dialog ? '' : ' o_inline_mode'}">
<!-- domain selector will be instantiated here -->
<div class="o_form_field_domain_panel">
<i class="fa fa-arrow-right"/>
<button class="btn btn-xs btn-default o_domain_show_selection_button" type="button">
<span class="o_domain_records_count"/> record(s)
</button>
<span class="text-warning o_domain_error_message hidden"><i class="fa fa-exclamation-triangle"/> Invalid domain</span>
<button t-if="widget.options.in_dialog &amp;&amp; !widget.get('effective_readonly')"
class="btn btn-xs btn-primary o_form_field_domain_dialog_button">Edit domain</button>
</div>
<div class="o_domain_model_missing o_hidden">Select a model to add a filter.</div>
</div>
<t t-name="DomainNode.ControlPanel">
<div t-if="!widget.readonly &amp;&amp; !widget.noControlPanel" class="btn-group btn-group-sm pull-right o_domain_node_control_panel">
<button class="btn btn-default o_domain_delete_node_button"><i class="fa fa-minus"/></button>
<button class="btn btn-default o_domain_add_node_button"><i class="fa fa-plus"/></button>
<button class="btn btn-default o_domain_add_node_button" data-branch="1"><i class="fa fa-ellipsis-h"/></button>
</div>
</t>
<t t-name="DomainTree.OperatorSelector">
<div t-if="!widget.readonly" class="btn-group o_domain_tree_operator_selector">
<button class="btn btn-xs btn-primary o_domain_tree_operator_caret" data-toggle="dropdown">
<t t-if="widget.operator === '&amp;'">All</t>
<t t-if="widget.operator === '|'">Any</t>
<t t-if="widget.operator === '!'">None</t>
</button>
<ul class="dropdown-menu">
<li><a href="#" data-operator="&amp;">All</a></li>
<li><a href="#" data-operator="|">Any</a></li>
</ul>
</div>
<strong t-else="">
<t t-if="widget.operator === '&amp;'">ALL</t>
<t t-if="widget.operator === '|'">ANY</t>
<t t-if="widget.operator === '!'">NONE</t>
</strong>
</t>
<div t-name="DomainSelector" t-attf-class="o_domain_node o_domain_tree o_domain_selector #{widget.readonly ? 'o_read_mode' : 'o_edit_mode'}">
<t t-if="widget.children.length === 0">
<span>Match <strong>all records</strong></span>
<button t-if="!widget.readonly" class="btn btn-xs btn-primary o_domain_add_first_node_button"><i class="fa fa-plus"/> Add filter</button>
</t>
<t t-else="">
<div>
<t t-if="widget.children.length === 1">Match records with the following rule:</t>
<t t-else="">
<span>Match records with</span>
<t t-call="DomainTree.OperatorSelector"/>
<span>of the following rules:</span>
</t>
</div>
<div class="o_domain_node_children_container"/>
</t>
<input t-if="widget.debug &amp;&amp; !widget.readonly" type="text" class="o_domain_debug_input mt16"/>
</div>
<div t-name="DomainTree" class="o_domain_node o_domain_tree">
<t t-call="DomainNode.ControlPanel"/>
<t t-call="DomainTree.OperatorSelector"/>
<span>of:</span>
<div class="o_domain_node_children_container"/>
</div>
<div t-name="DomainLeaf" t-attf-class="o_domain_node o_domain_leaf #{widget.readonly ? 'o_read_mode' : 'o_edit_mode'}">
<t t-call="DomainNode.ControlPanel"/>
<div t-if="!widget.readonly" class="o_domain_leaf_edition">
<!-- field selector will be instantiated here -->
<select class="o_domain_leaf_operator_select">
<option t-foreach="widget.operators" t-as="key"
t-att-value="key"
t-att-selected="widget.displayOperator === key ? 'selected' : None">
<t t-esc="key_value"/>
</option>
</select>
<div t-attf-class="o_ds_value_cell#{_.contains(['set', 'not set'], widget.displayOperator) ? ' hidden' : ''}">
<t t-if="widget.selectionChoices !== null">
<select class="o_domain_leaf_value_input">
<option t-foreach="widget.selectionChoices" t-as="val"
t-att-value="val[0]"
t-att-selected="_.contains(val, widget.displayValue) ? 'selected' : None">
<t t-esc="val[1]"/>
</option>
</select>
</t>
<t t-else="">
<t t-if="_.contains(['in', 'not in'], widget.operator)">
<div class="o_domain_leaf_value_input">
<span class="badge" t-foreach="widget.displayValue" t-as="val">
<t t-esc="val"/> <i class="o_domain_leaf_value_remove_tag_button fa fa-times" t-att-data-value="val"/>
</span>
</div>
<div class="o_domain_leaf_value_tags">
<input placeholder="Add new value" type="text"/>
<button class="btn btn-xs btn-primary fa fa-plus o_domain_leaf_value_add_tag_button"/>
</div>
</t>
<t t-else="">
<input class="o_domain_leaf_value_input" type="text" t-att-value="widget.displayValue"/>
</t>
</t>
</div>
</div>
<div t-else="" class="o_domain_leaf_info">
<span class="o_domain_leaf_chain"><t t-esc="widget.chain"/></span>
<t t-if="_.isString(widget.value)">
<span class="o_domain_leaf_operator"><t t-esc="widget.operator_mapping[widget.operator]"/></span>
<span class="o_domain_leaf_value text-primary">"<t t-esc="widget.value"/>"</span>
</t>
<t t-if="_.isArray(widget.value)">
<span class="o_domain_leaf_operator"><t t-esc="widget.operator_mapping[widget.operator]"/></span>
<t t-foreach="widget.value" t-as="v">
<span class="o_domain_leaf_value text-primary">"<t t-esc="v"/>"</span>
<t t-if="!v_last"> or </t>
</t>
</t>
<t t-if="_.isNumber(widget.value)">
<span class="o_domain_leaf_operator"><t t-esc="widget.operator_mapping[widget.operator]"/></span>
<span class="o_domain_leaf_value text-primary"><t t-esc="widget.value"></t></span>
</t>
<t t-if="_.isBoolean(widget.value)">
is
<t t-if="widget.operator === '=' &amp;&amp; widget.value === false || widget.operator === '!=' &amp;&amp; widget.value === true">not</t>
set
</t>
</div>
</div>
<div t-name="FieldSelector" class="o_field_selector">
<input type="text" t-att-value="widget.chain" t-att-readonly="!(widget.options.debugMode) and 1 or None"/>
<div class="o_field_selector_controls" tabindex="0">
<i class="fa fa-exclamation-triangle o_field_selector_warning hidden" title="Invalid field chain"/>
</div>
<div class="o_field_selector_popover hidden" tabindex="0">
<div class="o_field_selector_popover_header text-center">
<i class="fa fa-arrow-left o_field_selector_popover_option o_field_selector_prev_page"/>
<div class="o_field_selector_title"/>
<i class="fa fa-times o_field_selector_popover_option o_field_selector_close"/>
</div>
<div class="o_field_selector_popover_body">
<ul class="o_field_selector_page"/>
</div>
</div>
</div>
<ul t-name="FieldSelector.page" t-attf-class="o_field_selector_page #{animation}">
<t t-foreach="lines" t-as="line">
<t t-set="relationToFollow" t-value="followRelations &amp;&amp; line.relation"/>
<li t-attf-class="o_field_selector_item #{relationToFollow and 'o_field_selector_next_page' or 'o_field_selector_select_button'}#{line_index == 0 and ' active' or ''}"
t-att-data-name="line.name">
<t t-esc="line.string"/>
<div t-if="debug" class="text-muted o_field_selector_item_title"><t t-esc="line.name"/> (<t t-esc="line.type"/>)</div>
<i t-if="relationToFollow" class="fa fa-chevron-right o_field_selector_relation_icon"/>
</li>
</t>
</ul>
</template>

155
web_widget_domain_v11/static/src/js/domain_field.js

@ -0,0 +1,155 @@
/* Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
// Many code copied from Odoo, but with modifications https://github.com/odoo/odoo/blob/68176d80ad6053f52ed1c7bcf294ab3664986c46/addons/web/static/src/js/views/form_widgets.js#L396-L528
odoo.define('web_widget_domain_v11.field', function(require){
"use strict";
var core = require('web.core');
var DomainSelector = require("web.DomainSelector");
var DomainSelectorDialog = require("web.DomainSelectorDialog");
var common = require('web.form_common');
var Model = require('web.DataModel');
var pyeval = require('web.pyeval');
var session = require('web.session');
var _t = core._t;
/// The "Domain" field allows the user to construct a technical-prefix domain thanks to
/// a tree-like interface and see the selected records in real time.
/// In debug mode, an input is also there to be able to enter the prefix char domain
/// directly (or to build advanced domains the tree-like interface does not allow to).
var FieldDomain = common.AbstractField.extend(common.ReinitializeFieldMixin).extend({
template: "FieldDomain",
events: {
"click .o_domain_show_selection_button": function (e) {
e.preventDefault();
this._showSelection();
},
"click .o_form_field_domain_dialog_button": function (e) {
e.preventDefault();
this.openDomainDialog();
},
},
custom_events: {
"domain_changed": function (e) {
if (this.options.in_dialog) return;
this.set_value(this.domainSelector.getDomain(), true);
},
"domain_selected": function (e) {
this.set_value(e.data.domain);
},
},
init: function () {
this._super.apply(this, arguments);
this.valid = true;
this.debug = session.debug;
this.options = _.defaults(this.options || {}, {
in_dialog: false,
model: undefined, // this option is mandatory !
fs_filters: {}, // Field selector filters (to only show a subset of available fields @see FieldSelector)
});
},
start: function() {
this.model = _get_model.call(this); // TODO get the model another way ?
this.field_manager.on("view_content_has_changed", this, function () {
var currentModel = this.model;
this.model = _get_model.call(this);
if (currentModel !== this.model) {
this.render_value();
}
});
return this._super.apply(this, arguments);
function _get_model() {
if (this.options.model) {
return this.options.model;
}
if (this.field_manager.fields[this.options.model_field]) {
return this.field_manager.get_field_value(this.options.model_field);
}
}
},
initialize_content: function () {
this._super.apply(this, arguments);
this.$panel = this.$(".o_form_field_domain_panel");
this.$showSelectionButton = this.$panel.find(".o_domain_show_selection_button");
this.$recordsCountDisplay = this.$showSelectionButton.find(".o_domain_records_count");
this.$errorMessage = this.$panel.find(".o_domain_error_message");
this.$modelMissing = this.$(".o_domain_model_missing");
},
set_value: function (value, noDomainSelectorRender) {
this._noDomainSelectorRender = !!noDomainSelectorRender;
this._super.apply(this, arguments);
this._noDomainSelectorRender = false;
},
render_value: function() {
this._super.apply(this, arguments);
// If there is no set model, the field should only display the corresponding error message
this.$panel.toggleClass("o_hidden", !this.model);
this.$modelMissing.toggleClass("o_hidden", !!this.model);
if (!this.model) {
if (this.domainSelector) {
this.domainSelector.destroy();
this.domainSelector = undefined;
}
return;
}
var domain = pyeval.eval("domain", this.get("value") || "[]");
// Recreate domain widget with new domain value
if (!this._noDomainSelectorRender) {
if (this.domainSelector) {
this.domainSelector.destroy();
}
this.domainSelector = new DomainSelector(this, this.model, domain, {
readonly: this.get("effective_readonly") || this.options.in_dialog,
fs_filters: this.options.fs_filters,
debugMode: session.debug,
});
this.domainSelector.prependTo(this.$el);
}
// Show number of selected records
new Model(this.model).call("search_count", [domain], {
context: this.build_context(),
}).then((function (data) {
this.valid = true;
return data;
}).bind(this), (function (error, e) {
e.preventDefault();
this.valid = false;
}).bind(this)).always((function (data) {
this.$recordsCountDisplay.text(data || 0);
this.$showSelectionButton.toggleClass("hidden", !this.valid);
this.$errorMessage.toggleClass("hidden", this.valid);
}).bind(this));
},
is_syntax_valid: function() {
return this.field_manager.get("actual_mode") === "view" || this.valid;
},
_showSelection: function() {
return new common.SelectCreateDialog(this, {
title: _t("Selected records"),
res_model: this.model,
domain: this.get("value") || "[]",
no_create: true,
readonly: true,
disable_multiple_selection: true,
}).open();
},
openDomainDialog: function () {
new DomainSelectorDialog(this, this.model, this.get("value") || "[]", {
readonly: this.get("effective_readonly"),
fs_filters: this.options.fs_filters,
debugMode: session.debug,
}).open();
},
});
// Replace char_domain widget
core.form_widget_registry.add('char_domain', FieldDomain);
});

26
web_widget_domain_v11/templates/assets.xml

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<template id="assets_backend" inherit_id="web.assets_backend">
<xpath expr=".">
<link rel="stylesheet"
href="/web_widget_domain_v11/static/src/copied-css/domain_selector.less"/>
<link rel="stylesheet"
href="/web_widget_domain_v11/static/src/copied-css/model_field_selector.less"/>
<script type="text/javascript"
src="/web_widget_domain_v11/static/src/copied-js/domain_selector_dialog.js"/>
<script type="text/javascript"
src="/web_widget_domain_v11/static/src/copied-js/domain_selector.js"/>
<script type="text/javascript"
src="/web_widget_domain_v11/static/src/copied-js/domain_utils.js"/>
<script type="text/javascript"
src="/web_widget_domain_v11/static/src/copied-js/model_field_selector.js"/>
<script type="text/javascript"
src="/web_widget_domain_v11/static/src/js/domain_field.js"/>
</xpath>
</template>
</odoo>

19
web_widget_domain_v11/views/ir_filters.xml

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<record id="ir_filters_view_form" model="ir.ui.view">
<field name="name">Use domain widget</field>
<field name="model">ir.filters</field>
<field name="inherit_id" ref="base.ir_filters_view_form"/>
<field name="arch" type="xml">
<field name="domain" position="attributes">
<attribute name="widget">char_domain</attribute>
<attribute name="options">{'model_field': 'model_id'}</attribute>
</field>
</field>
</record>
</odoo>
Loading…
Cancel
Save