[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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>',
|
||||
|
||||
Reference in New Issue
Block a user