diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 34c2b08e362..2dedec8978d 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -14,10 +14,14 @@ var core = require('web.core'); var crash_manager = require('web.crash_manager'); var datepicker = require('web.datepicker'); var dom = require('web.dom'); +var Domain = require('web.Domain'); +var DomainSelector = require('web.DomainSelector'); +var DomainSelectorDialog = require('web.DomainSelectorDialog'); var field_utils = require('web.field_utils'); var framework = require('web.framework'); var session = require('web.session'); var utils = require('web.utils'); +var view_dialogs = require('web.view_dialogs'); var qweb = core.qweb; var _t = core._t; @@ -1188,6 +1192,225 @@ var JournalDashboardGraph = AbstractField.extend({ }); +/** + * 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 = AbstractField.extend({ + /** + * The model this field widget works with could be dependant on another + * field value. This field widget must then be reset when a field is changed + * in the view. + */ + resetOnAnyFieldChange: true, + + events: _.extend({}, AbstractField.prototype.events, { + "click .o_domain_show_selection_button": "_onShowSelectionButtonClick", + "click .o_form_field_domain_dialog_button": "_onDialogEditButtonClick", + }), + custom_events: { + "domain_changed": "_onDomainSelectorValueChange", + "domain_selected": "_onDomainSelectorDialogValueChange", + }, + /** + * @constructor + * @override init from AbstractField + */ + init: function () { + this._super.apply(this, arguments); + + this.inDialog = !!this.nodeOptions.in_dialog; + this.modelOption = this.nodeOptions.model; + this.fsFilters = this.nodeOptions.fs_filters || {}; + + this.className = "o_form_field_domain"; + if (this.mode === "edit") { + this.className += " o_edit_mode"; + } + if (!this.inDialog) { + this.className += " o_inline_mode"; + } + + this._initModel(); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override isValid from AbstractField.isValid + * Parsing the char value is not enough for this field. It is considered + * valid if the internal domain selector was built correctly and that the + * query to the model to test the domain did not fail. + * + * @returns {boolean} + */ + isValid: function () { + return ( + this._super.apply(this, arguments) + && this._isValidForModel + ); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @override _render from AbstractField + * @returns {Deferred} + */ + _render: function () { + // If there is no model, only change the non-domain-selector content + if (!this.domainModel) { + this._replaceContent(); + return $.when(); + } + + // Convert char value to array value + var value = this.value || "[]"; + var domain = Domain.prototype.stringToArray(value); + + // Create the domain selector or change the value of the current one... + var def; + if (!this.domainSelector) { + this.domainSelector = new DomainSelector(this, this.domainModel, domain, { + readonly: this.mode === "readonly" || this.inDialog, + filters: this.fsFilters, + debugMode: session.debug, + }); + def = this.domainSelector.prependTo(this.$el); + } else { + def = this.domainSelector.setDomain(domain); + } + // ... then query the model to get the number of matched record if valid + var self = this; + return def.then(function () { + var context = {}; // FIXME ? + return self.performModelRPC(self.domainModel, "search_count", [ + domain, + context + ]).then( + function (data) { + self._isValidForModel = true; + return data; + }, + function (error, e) { + e.preventDefault(); + self._isValidForModel = false; + } + ).always(self._replaceContent.bind(self)); + }); + }, + /** + * Render the field DOM except for the domain selector part. The full field + * DOM is composed of a DIV which contains the domain selector widget, + * followed by other content. This other content is handled by this method. + * + * @private + * @param {integer} [nbRecords=0] - The number of records the domain matches + */ + _replaceContent(nbRecords) { + if (this._$content) { + this._$content.remove(); + } + this._$content = $(qweb.render("FieldDomain.content", { + hasModel: !!this.domainModel, + isValid: !!this._isValidForModel, + nbRecords: nbRecords || 0, + inDialogEdit: this.inDialog && this.mode === "edit", + })); + this._$content.appendTo(this.$el); + }, + /** + * @override _reset from AbstractField + * Check if the model the field works with has (to be) changed. + */ + _reset: function () { + this._super.apply(this, arguments); + var oldDomainModel = this.domainModel; + this._initModel(); + if (this.domainSelector && this.domainModel !== oldDomainModel) { + // If the model has changed, destroy the current domain selector + this.domainSelector.destroy(); + this.domainSelector = null; + } + }, + /** + * Checks the model the field must work with. It is either directly given + * in the field option or it is the value of another field in the view. + * + * @private + */ + _initModel: function () { + this.domainModel = this.modelOption; + if (this.recordData.hasOwnProperty(this.domainModel)) { + this.domainModel = this.recordData[this.domainModel]; + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the "Show selection" button is clicked + * -> Open a modal to see the matched records + * + * @param {Event} e + */ + _onShowSelectionButtonClick: function (e) { + e.preventDefault(); + new view_dialogs.SelectCreateDialog(this, { + title: _t("Selected records"), + res_model: this.domainModel, + domain: this.value || "[]", + no_create: true, + readonly: true, + disable_multiple_selection: true, + }).open(); + }, + /** + * Called when the "Edit domain" button is clicked (when using the in_dialog + * option) -> Open a DomainSelectorDialog to edit the value + * + * @param {Event} e + */ + _onDialogEditButtonClick: function (e) { + e.preventDefault(); + new DomainSelectorDialog(this, this.domainModel, this.value || "[]", { + readonly: this.mode === "readonly", + filters: this.fsFilters, + debugMode: session.debug, + }).open(); + }, + /** + * Called when the domain selector value is changed (do nothing if it is the + * one which is in a dialog (@see _onDomainSelectorDialogValueChange)) + * -> Adapt the internal value state + * + * @param {OdooEvent} e + */ + _onDomainSelectorValueChange: function (e) { + if (this.inDialog) return; + this._setValue(Domain.prototype.arrayToString(this.domainSelector.getDomain())); + }, + /** + * Called when the in-dialog domain selector value is confirmed + * -> Adapt the internal value state + * + * @param {OdooEvent} e + */ + _onDomainSelectorDialogValueChange: function (e) { + this._setValue(Domain.prototype.arrayToString(e.data.domain)); + }, +}); + /** * This widget is intended to be used on Text fields. It will provide Ace Editor * for editing XML and Python. @@ -1275,6 +1498,7 @@ return { FieldChar: FieldChar, FieldDate: FieldDate, FieldDateTime: FieldDateTime, + FieldDomain: FieldDomain, FieldFloat: FieldFloat, FieldFloatTime: FieldFloatTime, FieldHtml: FieldHtml, diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js index 0bfddd03098..405af71438b 100644 --- a/addons/web/static/src/js/fields/field_registry.js +++ b/addons/web/static/src/js/fields/field_registry.js @@ -24,6 +24,7 @@ registry .add('boolean', basic_fields.FieldBoolean) .add('date', basic_fields.FieldDate) .add('datetime', basic_fields.FieldDateTime) + .add('domain', basic_fields.FieldDomain) .add('text', basic_fields.FieldText) .add('float', basic_fields.FieldFloat) .add('char', basic_fields.FieldChar) @@ -63,4 +64,4 @@ registry .add('statusbar', relational_fields.FieldStatus) .add('one2many_list', relational_fields.FieldOne2Many); -}); \ No newline at end of file +}); diff --git a/addons/web/static/src/js/widgets/domain_selector.js b/addons/web/static/src/js/widgets/domain_selector.js index f7c67aa37cb..ba1ac9e77c9 100644 --- a/addons/web/static/src/js/widgets/domain_selector.js +++ b/addons/web/static/src/js/widgets/domain_selector.js @@ -301,6 +301,12 @@ var DomainSelector = DomainTree.extend({ } return this._super(domain); }, + setDomain: function (domain) { + if (Domain.prototype.arrayToString(domain) === Domain.prototype.arrayToString(this.getDomain())) { + return $.when(); + } + return this._redraw(domain); + }, _postRender: function () { this._super.apply(this, arguments); diff --git a/addons/web/static/src/js/widgets/model_field_selector.js b/addons/web/static/src/js/widgets/model_field_selector.js index 907368920ac..9c843937c90 100644 --- a/addons/web/static/src/js/widgets/model_field_selector.js +++ b/addons/web/static/src/js/widgets/model_field_selector.js @@ -2,11 +2,20 @@ 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; +/** + * Field Selector Cache - TODO Should be improved to use external cache ? + * - Stores fields per model used in field selector + * @see ModelFieldSelector._getModelFieldsFromCache + */ +var modelFieldsCache = { + cache: {}, + cacheDefs: {}, +}; + /// The ModelFieldSelector widget can be used to select a particular field chain from a given model. var ModelFieldSelector = Widget.extend({ template: "FieldSelector", @@ -32,7 +41,7 @@ var ModelFieldSelector = Widget.extend({ }, // Handle a direct change in the debug input - "change input": function() { + "change input": function () { var userChain = this.$input.val(); if (!this.options.followRelations) { var fields = userChain.split("."); @@ -208,7 +217,7 @@ var ModelFieldSelector = Widget.extend({ /// @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._pushPageData(this.model).then((function () { return (this.chain ? processChain.call(this, this.chain.split(".").reverse()) : $.when()); }).bind(this)); @@ -234,7 +243,7 @@ var ModelFieldSelector = Widget.extend({ if (this.model === model && this.options.fields) { def = $.when(sortFields(this.options.fields)); } else { - def = fieldsCache.getFields(model, this.options.filters); + def = this._getModelFieldsFromCache(model, this.options.filters); } return def.then((function (fields) { this.pages.push(fields); @@ -294,36 +303,35 @@ var ModelFieldSelector = Widget.extend({ name: name, }); }, + /** + * The _getFieldsFromCache method searches the cache for the given model + * fields, according to the given filter. If the cache does not know about + * the model, the cache is updated. + * + * @param {string} model + * @param {Object} filters @see ModelFieldSelector.init.options.filters + * + * @returns {Object[]} a list of the model fields info, sorted by field + * non-technical name + */ + _getModelFieldsFromCache: function (model, filters) { + var def = modelFieldsCache.cacheDefs[model]; + if (!def) { + def = modelFieldsCache.cacheDefs[model] = this.performModelRPC(model, "fields_get", [ + false, + ["store", "searchable", "type", "string", "relation", "selection", "related"], + ]).then((function (fields) { + modelFieldsCache.cache[model] = sortFields(fields); + }).bind(this)); + } + return def.then((function () { + return _.filter(modelFieldsCache.cache[model], function (f) { + return !filters.searchable || f.searchable; + }); + }).bind(this)); + }, }); -/// 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() diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index f5fb05044e6..6d51198fd8b 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -665,19 +665,19 @@ t-att-maxlength="widget.field.size"/> -