diff --git a/web_advanced_search_x2x/__manifest__.py b/web_advanced_search_x2x/__manifest__.py
index 72ee66f5..14bc54a4 100644
--- a/web_advanced_search_x2x/__manifest__.py
+++ b/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',
diff --git a/web_advanced_search_x2x/static/src/css/web_advanced_search_x2x.less b/web_advanced_search_x2x/static/src/css/web_advanced_search_x2x.less
index 008e4b56..b41fa4a2 100644
--- a/web_advanced_search_x2x/static/src/css/web_advanced_search_x2x.less
+++ b/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;
+ }
}
}
}
diff --git a/web_advanced_search_x2x/static/src/js/web_advanced_search_x2x.js b/web_advanced_search_x2x/static/src/js/web_advanced_search_x2x.js
index 69fa621c..36937962 100644
--- a/web_advanced_search_x2x/static/src/js/web_advanced_search_x2x.js
+++ b/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
+ * ``.
+ */
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);
diff --git a/web_advanced_search_x2x/static/src/xml/web_advanced_search_x2x.xml b/web_advanced_search_x2x/static/src/xml/web_advanced_search_x2x.xml
index 1440acf2..78a79a1d 100644
--- a/web_advanced_search_x2x/static/src/xml/web_advanced_search_x2x.xml
+++ b/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). -->
-
-
-
+
+
-
-
+
+
diff --git a/web_widget_domain_v11/README.rst b/web_widget_domain_v11/README.rst
new file mode 100644
index 00000000..73d14663
--- /dev/null
+++ b/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
+`_. 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 `_.
+
+Contributors
+------------
+
+* Odoo SA
+* Jairo Llopis
+
+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.
diff --git a/web_widget_domain_v11/__init__.py b/web_widget_domain_v11/__init__.py
new file mode 100644
index 00000000..0d415c00
--- /dev/null
+++ b/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).
diff --git a/web_widget_domain_v11/__manifest__.py b/web_widget_domain_v11/__manifest__.py
new file mode 100644
index 00000000..8e9702dc
--- /dev/null
+++ b/web_widget_domain_v11/__manifest__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Copyright 2017 Jairo Llopis
+# 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",
+ ],
+}
diff --git a/web_widget_domain_v11/static/description/icon.png b/web_widget_domain_v11/static/description/icon.png
new file mode 100644
index 00000000..815b58f4
Binary files /dev/null and b/web_widget_domain_v11/static/description/icon.png differ
diff --git a/web_widget_domain_v11/static/src/copied-css/domain_selector.less b/web_widget_domain_v11/static/src/copied-css/domain_selector.less
new file mode 100644
index 00000000..f3917526
--- /dev/null
+++ b/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;
+ }
+ }
+ }
+ }
+}
diff --git a/web_widget_domain_v11/static/src/copied-css/model_field_selector.less b/web_widget_domain_v11/static/src/copied-css/model_field_selector.less
new file mode 100644
index 00000000..539f6fb4
--- /dev/null
+++ b/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;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/web_widget_domain_v11/static/src/copied-js/domain_selector.js b/web_widget_domain_v11/static/src/copied-js/domain_selector.js
new file mode 100644
index 00000000..eccbd9d7
--- /dev/null
+++ b/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 = $("");
+ 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($("")).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($("")).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("").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;
+});
diff --git a/web_widget_domain_v11/static/src/copied-js/domain_selector_dialog.js b/web_widget_domain_v11/static/src/copied-js/domain_selector_dialog.js
new file mode 100644
index 00000000..c8f48d0d
--- /dev/null
+++ b/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)
+ );
+ },
+});
+});
diff --git a/web_widget_domain_v11/static/src/copied-js/domain_utils.js b/web_widget_domain_v11/static/src/copied-js/domain_utils.js
new file mode 100644
index 00000000..8cefb7ca
--- /dev/null
+++ b/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,
+};
+});
diff --git a/web_widget_domain_v11/static/src/copied-js/model_field_selector.js b/web_widget_domain_v11/static/src/copied-js/model_field_selector.js
new file mode 100644
index 00000000..d1d6702f
--- /dev/null
+++ b/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;
+});
diff --git a/web_widget_domain_v11/static/src/copied-xml/templates.xml b/web_widget_domain_v11/static/src/copied-xml/templates.xml
new file mode 100644
index 00000000..af5442b1
--- /dev/null
+++ b/web_widget_domain_v11/static/src/copied-xml/templates.xml
@@ -0,0 +1,166 @@
+
+
+
+
+
+