Browse Source
Merge pull request #672 from Tecnativa/10.0-web_widget_domain_v11
Merge pull request #672 from Tecnativa/10.0-web_widget_domain_v11
[ADD][web_widget_domain_v11] Domain widget backportpull/682/head
Pedro M. Baeza
7 years ago
committed by
GitHub
18 changed files with 1687 additions and 47 deletions
-
4web_advanced_search_x2x/__manifest__.py
-
14web_advanced_search_x2x/static/src/css/web_advanced_search_x2x.less
-
70web_advanced_search_x2x/static/src/js/web_advanced_search_x2x.js
-
9web_advanced_search_x2x/static/src/xml/web_advanced_search_x2x.xml
-
73web_widget_domain_v11/README.rst
-
2web_widget_domain_v11/__init__.py
-
24web_widget_domain_v11/__manifest__.py
-
BINweb_widget_domain_v11/static/description/icon.png
-
87web_widget_domain_v11/static/src/copied-css/domain_selector.less
-
97web_widget_domain_v11/static/src/copied-css/model_field_selector.less
-
585web_widget_domain_v11/static/src/copied-js/domain_selector.js
-
47web_widget_domain_v11/static/src/copied-js/domain_selector_dialog.js
-
19web_widget_domain_v11/static/src/copied-js/domain_utils.js
-
337web_widget_domain_v11/static/src/copied-js/model_field_selector.js
-
166web_widget_domain_v11/static/src/copied-xml/templates.xml
-
155web_widget_domain_v11/static/src/js/domain_field.js
-
26web_widget_domain_v11/templates/assets.xml
-
19web_widget_domain_v11/views/ir_filters.xml
@ -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. |
@ -0,0 +1,2 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
@ -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", |
||||
|
], |
||||
|
} |
After Width: 390 | Height: 390 | Size: 24 KiB |
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
}); |
@ -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) |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
}); |
@ -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, |
||||
|
}; |
||||
|
}); |
@ -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; |
||||
|
}); |
@ -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 && !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 && !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 === '&'">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="&">All</a></li> |
||||
|
<li><a href="#" data-operator="|">Any</a></li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
<strong t-else=""> |
||||
|
<t t-if="widget.operator === '&'">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 && !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 === '=' && widget.value === false || widget.operator === '!=' && 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 && 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> |
@ -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); |
||||
|
}); |
@ -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> |
@ -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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue