[IMP] web: adapt field domain to new API

Add the implementation of the "FieldDomain" (widget='domain') in
basic fields file.
This commit is contained in:
qsm-odoo
2017-04-11 19:44:38 +02:00
committed by Aaron Bohy
parent 274a0aadc6
commit 92f24f2cfe
6 changed files with 444 additions and 57 deletions
@@ -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,
@@ -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);
});
});
@@ -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);
@@ -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()
+10 -10
View File
@@ -665,19 +665,19 @@
t-att-maxlength="widget.field.size"/>
</t>
<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">
<t t-name="FieldDomain.content">
<div t-if="hasModel" 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 t-if="isValid" class="btn btn-xs btn-default o_domain_show_selection_button" type="button">
<t t-esc="nbRecords"/> record(s)
</button>
<span class="text-warning o_domain_error_message hidden"><i class="fa fa-exclamation-triangle"/> Invalid domain</span>
<button t-if="widget.options.in_dialog &amp;&amp; !widget.get('effective_readonly')"
class="btn btn-xs btn-primary o_form_field_domain_dialog_button">Edit domain</button>
<span t-else="" class="text-warning"><i class="fa fa-exclamation-triangle"/> Invalid domain</span>
<button t-if="inDialogEdit" 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>
<div t-else="">Select a model to add a filter.</div>
</t>
<t t-name="DomainNode.ControlPanel">
<div t-if="!widget.readonly &amp;&amp; !widget.noControlPanel" class="btn-group btn-group-sm pull-right o_domain_node_control_panel">
@@ -16,17 +16,17 @@ QUnit.module('basic_fields', {
this.data = {
partner: {
fields: {
date: {string: "A date", type: "date"},
display_name: { string: "Displayed name", type: "char" },
foo: {string: "Foo", type: "char", default: "My little Foo Value"},
bar: {string: "Bar", type: "boolean", default: true},
int_field: {string: "int_field", type: "integer", sortable: true},
qux: {string: "Qux", type: "float", digits: [16,1] },
p: {string: "one2many field", type: "one2many", relation: 'partner'},
trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'},
product_id: {string: "Product", type: "many2one", relation: 'product'},
sequence: {type: "integer", string: "Sequence"},
date: {string: "A date", type: "date", searchable: true},
display_name: {string: "Displayed name", type: "char", searchable: true},
foo: {string: "Foo", type: "char", default: "My little Foo Value", searchable: true},
bar: {string: "Bar", type: "boolean", default: true, searchable: true},
int_field: {string: "int_field", type: "integer", sortable: true, searchable: true},
qux: {string: "Qux", type: "float", digits: [16,1], searchable: true},
p: {string: "one2many field", type: "one2many", relation: 'partner', searchable: true},
trululu: {string: "Trululu", type: "many2one", relation: 'partner', searchable: true},
timmy: {string: "pokemon", type: "many2many", relation: 'partner_type', searchable: true},
product_id: {string: "Product", type: "many2one", relation: 'product', searchable: true},
sequence: {type: "integer", string: "Sequence", searchable: true},
},
records: [{
id: 1,
@@ -62,7 +62,7 @@ QUnit.module('basic_fields', {
},
product: {
fields: {
name: {string: "Product Name", type: "char"}
name: {string: "Product Name", type: "char", searchable: true}
},
records: [{
id: 37,
@@ -74,8 +74,8 @@ QUnit.module('basic_fields', {
},
partner_type: {
fields: {
name: {string: "Partner Type", type: "char"},
color: {string: "Color index", type: "int"},
name: {string: "Partner Type", type: "char", searchable: true},
color: {string: "Color index", type: "integer", searchable: true},
},
records: [
{id: 12, display_name: "gold", color: 2},
@@ -539,6 +539,154 @@ QUnit.module('basic_fields', {
"date input should have the focus (make sure the test window had the focus)");
});
QUnit.module('FieldDomain');
QUnit.test('basic domain field usage is ok', function (assert) {
assert.expect(6);
this.data.partner.records[0].foo = "[]";
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form>' +
'<sheet>' +
'<group>' +
'<field name="foo" widget="domain" options="{\'model\': \'partner_type\'}"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
// As the domain is empty, there should be a button to add the first
// domain part
var $domain = form.$(".o_form_field_domain");
var $domainAddFirstNodeButton = $domain.find(".o_domain_add_first_node_button");
assert.equal($domainAddFirstNodeButton.length, 1,
"there should be a button to create first domain element");
// Clicking on the button should add the [["id", "=", "1"]] domain, so
// there should be a field selector in the DOM
$domainAddFirstNodeButton.click();
var $fieldSelector = $domain.find(".o_field_selector");
assert.equal($fieldSelector.length, 1,
"there should be a field selector");
// Focusing the field selector input should open the field selector
// popover
$fieldSelector.find("> input").focus();
var $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover");
assert.ok($fieldSelectorPopover.is(":visible"),
"field selector popover should be visible");
// The popover should contain the list of partner_type fields and so
// there should be the "Color index" field
var $lis = $fieldSelectorPopover.find("li");
var $colorIndex = $();
$lis.each(function () {
var $li = $(this);
if ($li.html().indexOf("Color index") >= 0) {
$colorIndex = $li;
}
});
assert.equal($colorIndex.length, 1,
"field selector popover should contain 'Color index' field");
// Clicking on this field should close the popover, then changing the
// associated value should reveal one matched record
$colorIndex.click();
$domain.find(".o_domain_leaf_value_input").val(2).change();
assert.equal($domain.find(".o_domain_show_selection_button").text().trim().substr(0, 2), "1 ",
"changing color value to 2 should reveal only one record");
// Saving the form view should show a readonly domain containing the
// "color" field
form.$buttons.find('.o_form_button_save').click();
$domain = form.$(".o_form_field_domain");
assert.ok($domain.html().indexOf("color") >= 0,
"field selector readonly value should now contain 'color'");
});
QUnit.test('domain field is correctly reset on every view change', function (assert) {
assert.expect(7);
this.data.partner.records[0].foo = '[["id","=",1]]';
this.data.partner.fields.bar.type = "char";
this.data.partner.records[0].bar = "product";
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form>' +
'<sheet>' +
'<group>' +
'<field name="bar"/>' +
'<field name="foo" widget="domain" options="{\'model\': \'bar\'}"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
// As the domain is equal to [["id", "=", 1]] there should be a field
// selector to change this
var $domain = form.$(".o_form_field_domain");
var $fieldSelector = $domain.find(".o_field_selector");
assert.equal($fieldSelector.length, 1,
"there should be a field selector");
// Focusing its input should open the field selector popover
$fieldSelector.find("> input").focus();
var $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover");
assert.ok($fieldSelectorPopover.is(":visible"),
"field selector popover should be visible");
// As the value of the "bar" field is "product", the field selector
// popover should contain the list of "product" fields
var $lis = $fieldSelectorPopover.find("li");
var $sampleLi = $();
$lis.each(function () {
var $li = $(this);
if ($li.html().indexOf("Product Name") >= 0) {
$sampleLi = $li;
}
});
assert.strictEqual($lis.length, 1,
"field selector popover should contain only one field");
assert.strictEqual($sampleLi.length, 1,
"field selector popover should contain 'Product Name' field");
// Now change the value of the "bar" field to "partner_type"
form.$(".o_field_widget.o_form_input").click().val("partner_type").trigger("input");
// Refocusing the field selector input should open the popover again
$fieldSelector = form.$(".o_field_selector");
$fieldSelector.find("> input").focus();
$fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover");
assert.ok($fieldSelectorPopover.is(":visible"),
"field selector popover should be visible");
// Now the list of fields should be the ones of the "partner_type" model
$lis = $fieldSelectorPopover.find("li");
$sampleLi = $();
$lis.each(function () {
var $li = $(this);
if ($li.html().indexOf("Color index") >= 0) {
$sampleLi = $li;
}
});
assert.strictEqual($lis.length, 2,
"field selector popover should contain two fields");
assert.strictEqual($sampleLi.length, 1,
"field selector popover should contain 'Color index' field");
});
});
});
});