[FIX] web: restore FieldStatus behavior

FieldStatus was broken:
- With a selection, the statusbar_visible attribute forces the choices
  to display, the discarded ones do not appear at all (even folded)
- With a m2o, the fold_field option tells the field to check in the
  relation to know if the choice has to be folded or not, this was not
  properly supported
- Changing a m2o value was broken as the parseMany2one function did
  not exist
- The status were misordered
- ...

* Also refactor the way the rendering is handled.
* Also modify all statusbar tests so that the statusbar widget is
  placed in the form header tag (as it is the common use case).
This commit is contained in:
qsm-odoo
2017-04-11 19:44:38 +02:00
committed by Aaron Bohy
parent b5b649e5b2
commit 29cd7154e6
6 changed files with 130 additions and 77 deletions
+29 -1
View File
@@ -336,6 +336,34 @@ function parseMonetary(formatted_value) {
return parseFloat(value);
}
/**
* Creates an object with id and display_name.
*
* @param {Array|number|string|Object} value
* The given value can be :
* - an array with id as first element and display_name as second element
* - a number or a string representing the id (the display_name will be
* returned as undefined)
* - an object, simply returned untouched
* @returns {Object} (contains the id and display_name)
* Note: if the given value is not an array, a string or a
* number, the value is returned untouched.
*/
function parseMany2one(value) {
if (_.isArray(value)) {
return {
id: value[0],
display_name: value[1],
};
}
if (_.isNumber(value) || _.isString(value)) {
return {
id: parseInt(value, 10),
};
}
return value;
}
return {
format: {
binary: _.identity, // todo
@@ -368,7 +396,7 @@ return {
id: _.identity,
integer: parseInteger,
many2many: _.identity, // todo
many2one: _.identity,
many2one: parseMany2one,
monetary: parseMonetary,
one2many: _.identity,
reference: _.identity, // todo
@@ -1166,13 +1166,15 @@ var FieldMany2ManyCheckBoxes = AbstractField.extend({
//------------------------------------------------------------------------------
var FieldStatus = AbstractField.extend({
template: "FieldStatus",
className: 'o_statusbar_status',
events: {
'click .o_arrow_button': '_onClickStage',
'click button:not(.dropdown-toggle)': '_onClickStage',
},
specialData: "_fetchSpecialStatus",
supportedFieldTypes: ['selection', 'many2one'],
/**
* @override init from AbstractField
*/
init: function () {
this._super.apply(this, arguments);
this._setState();
@@ -1183,6 +1185,7 @@ var FieldStatus = AbstractField.extend({
//--------------------------------------------------------------------------
/**
* @override _reset from AbstractField
* @private
*/
_reset: function () {
@@ -1190,45 +1193,51 @@ var FieldStatus = AbstractField.extend({
this._setState();
},
/**
* Prepares the rendering data from the field and record data.
* @private
*/
_setState: function () {
var self = this;
if (this.field.type === 'many2one') {
this.status_information = this.record.specialData[this.name];
var status = _.findWhere(this.status_information, {id: this.value.res_id});
if (status) {
status.selected = true;
}
this.status_information = _.map(this.record.specialData[this.name], function (info) {
return _.extend({
selected: info.id === self.value.res_id,
}, info);
});
} else {
var self = this;
this.status_information = _.map(this.field.selection, function (val) {
return { id: val[0], display_name: val[1], selected: val[0] === self.value };
var selection = this.field.selection;
if (this.attrs.statusbar_visible) {
var restriction = this.attrs.statusbar_visible.split(",");
selection = _.filter(selection, function (val) {
return _.contains(restriction, val[0]) || val[0] === self.value;
});
}
this.status_information = _.map(selection, function (val) {
return { id: val[0], display_name: val[1], selected: val[0] === self.value, fold: false };
});
}
},
/**
* @override _render from AbstractField
* @private
*/
_render: function () {
var visible = this.attrs.statusbar_visible;
var selection_folded = [];
var selection_unfolded = [];
_.each(this.status_information, function (info) {
if (info.selected && (info.fold || visible && visible.indexOf(info.display_name) === -1)) {
selection_folded.push(info);
} else {
selection_unfolded.push(info);
}
});
var mobile = config.device.size_class <= config.device.SIZES.XS;
// grep: FieldStatus.content.mobile, FieldStatus.content.desktop
var content = qweb.render("FieldStatus.content." + (mobile ? 'mobile' : 'desktop'), {
selection_folded: selection_folded.reverse(),
selection_unfolded: selection_unfolded.reverse(),
clickable: !!this.attrs.clickable,
});
this.$el.html(content);
this.$('.disabled').prop('disabled', true);
if (config.device.size_class > config.device.SIZES.XS) {
var selections = _.partition(this.status_information, function (info) {
return (info.selected || !info.fold);
});
this.$el.html(qweb.render("FieldStatus.content.desktop", {
selection_unfolded: selections[0],
selection_folded: selections[1],
clickable: !!this.attrs.clickable,
}));
} else {
this.$el.html(qweb.render("FieldStatus.content.mobile", {
selection: this.status_information,
value: _.findWhere(this.status_information, {selected: true}).display_name,
clickable: !!this.attrs.clickable,
}));
}
},
//--------------------------------------------------------------------------
@@ -1236,22 +1245,14 @@ var FieldStatus = AbstractField.extend({
//--------------------------------------------------------------------------
/**
* Note that this function is debounced...
* Called when on status stage is clicked -> sets the field value.
* Note: this function is debounced...
*
* @private
* @param {MouseEvent} event
* @param {MouseEvent} e
*/
_onClickStage: _.debounce(function (event) {
var $stage = $(event.currentTarget);
var val;
if (this.field.type === "many2one") {
val = parseInt($stage.data("value"), 10);
} else {
val = $stage.data("value");
}
if (val) {
this._setValue(val);
}
_onClickStage: _.debounce(function (e) {
this._setValue($(e.currentTarget).data("value"));
}, 300, true),
});
@@ -964,7 +964,7 @@ var BasicModel = AbstractModel.extend({
}
var rel_data = _.pick(data, 'id', 'display_name');
var def;
if (!('display_name' in rel_data)) {
if (rel_data.display_name === undefined) {
var field = record.fields[fieldName];
def = this._rpc(field.relation, 'name_get')
.args([data.id])
@@ -1498,7 +1498,12 @@ var BasicModel = AbstractModel.extend({
_fetchSpecialStatus: function (record, fieldName) {
var foldField = record.fieldAttrs[fieldName].options.fold_field;
var fieldsToRead = foldField ? [foldField] : [];
return this._fetchSpecialMany2ones(record, fieldName, fieldsToRead);
return this._fetchSpecialMany2ones(record, fieldName, fieldsToRead).then(function (m2os) {
_.each(m2os, function (m2o) {
m2o.fold = foldField ? m2o[foldField] : false;
});
return m2os;
});
},
/**
* Fetch all data in a ungrouped list
@@ -477,14 +477,13 @@ return BasicRenderer.extend({
},
_renderTagHeader: function (node) {
var self = this;
var $statusbar = $('<div>').addClass('o_form_statusbar');
var $buttons = $('<div>').addClass('o_statusbar_buttons');
var $statusbar = $('<div>', {class: 'o_form_statusbar'});
var $buttons = $('<div>', {class: 'o_statusbar_buttons'});
$statusbar.append($buttons);
_.each(node.children, function (child) {
if (child.tag === 'button') {
$buttons.append(self._renderHeaderButton(child));
}
if (child.tag === 'field') {
} else if (child.tag === 'field') {
var widget = self._renderFieldWidget(child);
$statusbar.append(widget.$el);
}
+8 -17
View File
@@ -926,13 +926,7 @@
<span t-if="widget.string"><t t-esc="widget.string"/></span>
</div>
</t>
<t t-name="FieldStatus">
<ul class="o_statusbar_status" t-att-style="widget.attrs.style"/>
</t>
<t t-name="FieldStatus.content.desktop">
<t t-foreach="selection_unfolded" t-as="i">
<t t-call="FieldStatus.content.button"/>
</t>
<t t-if="selection_folded.length">
<ul class="dropdown-menu o-status-more" role="menu">
<li t-foreach="selection_folded" t-as="i">
@@ -941,27 +935,24 @@
</ul>
<button type="button" class="btn btn-sm o_arrow_button btn-default dropdown-toggle" data-toggle="dropdown">More <span class="caret"/></button>
</t>
<t t-foreach="selection_unfolded.reverse()" t-as="i">
<t t-call="FieldStatus.content.button"/>
</t>
</t>
<t t-name="FieldStatus.content.mobile">
<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-toggle="dropdown">
<t t-foreach="selection_unfolded" t-as="i">
<t t-if="i.id === value">
<t t-esc="i.display_name"/>
</t>
</t>
<t t-esc="value"/>
</button>
<ul class="dropdown-menu o-status-more" role="menu">
<li t-foreach="selection_unfolded" t-as="i">
<t t-call="FieldStatus.content.button"/>
</li>
<li t-foreach="selection_folded" t-as="i">
<li t-foreach="selection" t-as="i">
<t t-call="FieldStatus.content.button"/>
</li>
</ul>
</t>
<t t-name="FieldStatus.content.button">
<button type="button" t-att-data-value="i.id"
t-attf-class="btn btn-sm o_arrow_button #{i.selected ? 'btn-primary disabled' : 'btn-default'} #{!clickable ? 'disabled' : ''}">
<t t-set="disabled" t-value="!clickable || i.selected"/>
<button type="button" t-att-data-value="i.id" t-att-disabled="disabled ? 'disabled' : undefined"
t-attf-class="btn btn-sm o_arrow_button btn-#{i.selected ? 'primary' : 'default'}#{disabled ? ' disabled' : ''}">
<t t-esc="i.display_name"/>
</button>
</t>
@@ -2323,9 +2323,8 @@ QUnit.module('relational_fields', {
assert.expect(5);
this.data.partner.fields.trululu.domain = "[('bar', '=', True)]";
this.data.partner.records.forEach(function (record) {
record.bar = true;
});
this.data.partner.records[1].bar = false;
var count = 0;
var nb_fields_fetched;
var form = createView({
@@ -2333,7 +2332,7 @@ QUnit.module('relational_fields', {
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="trululu" widget="statusbar"/>' +
'<header><field name="trululu" widget="statusbar"/></header>' +
// the following field seem useless, but its presence was the
// cause of a crash when evaluating the field domain.
'<field name="timmy"/>' +
@@ -2350,8 +2349,8 @@ QUnit.module('relational_fields', {
assert.strictEqual(count, 1, 'once search_read should have been done to fetch the relational values');
assert.strictEqual(nb_fields_fetched, 1, 'search_read should only fetch field id');
assert.strictEqual(form.$('.o_statusbar_status button').length, 3, "should have 3 status");
assert.strictEqual(form.$('.o_statusbar_status button.disabled').length, 3,
assert.strictEqual(form.$('.o_statusbar_status button:not(.dropdown-toggle)').length, 2, "should have 2 status");
assert.strictEqual(form.$('.o_statusbar_status button:disabled').length, 2,
"all status should be disabled");
assert.ok(form.$('.o_statusbar_status button[data-value="4"]').hasClass('btn-primary'),
"selected status should be btn-primary");
@@ -2360,22 +2359,27 @@ QUnit.module('relational_fields', {
});
QUnit.test('clickable statusbar widget on many2one field', function (assert) {
assert.expect(2);
assert.expect(3);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="trululu" widget="statusbar" clickable="True"/>' +
'<header><field name="trululu" widget="statusbar" clickable="True"/></header>' +
'</form>',
res_id: 1,
});
assert.ok(form.$('.o_statusbar_status button[data-value="4"]').hasClass('btn-primary disabled'),
"selected status should be btn-primary and disabled");
assert.strictEqual(form.$('.o_statusbar_status button.btn-default:not(.disabled)').length, 2,
var $clickable = form.$('.o_statusbar_status button.btn-default:not(.dropdown-toggle):not(:disabled)');
assert.strictEqual($clickable.length, 2,
"other status should be btn-default and not disabled");
$clickable.last().click(); // (last is visually the first here (css))
assert.ok(form.$('.o_statusbar_status button[data-value="1"]').hasClass("btn-primary disabled"),
"value should have been updated");
form.destroy();
});
@@ -2388,7 +2392,7 @@ QUnit.module('relational_fields', {
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="product_id" widget="statusbar"/>' +
'<header><field name="product_id" widget="statusbar"/></header>' +
'</form>',
res_id: 1,
});
@@ -2400,6 +2404,31 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('statusbar fold_field option and statusbar_visible attribute', function (assert) {
assert.expect(2);
this.data.partner.records[0].bar = false;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form string="Partners">' +
'<header><field name="trululu" widget="statusbar" options="{\'fold_field\': \'bar\'}"/>' +
'<field name="color" widget="statusbar" statusbar_visible="red"/></header>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_statusbar_status:first .dropdown-menu button.disabled').length, 1, "should have 1 folded status");
assert.strictEqual(form.$('.o_statusbar_status:last button.disabled').length, 1, "should have 1 status (other discarded)");
form.destroy();
});
QUnit.test('statusbar with dynamic domain', function (assert) {
assert.expect(5);
@@ -2413,7 +2442,7 @@ QUnit.module('relational_fields', {
data: this.data,
arch:
'<form string="Partners">' +
'<field name="trululu" widget="statusbar"/>' +
'<header><field name="trululu" widget="statusbar"/></header>' +
'<field name="qux"/>' +
'<field name="foo"/>' +
'</form>',