Skip to content
Permalink
Browse files

[IMP] web, base: put buttons in list group headers

It is now possible to put buttons in the list view group headers. They will
appear next to the header title when the group is opened.

The buttons are specified in the views in a <groupby> tag in the list arch, with
the following structure:
    <groupby name="groupField">
        <button type="object" name="my_method" string="Button1"/>
    </groupby>

When specifying a group on many2one fields, it is possible to add <fields>,
which can be used for modifiers. These fields are thus belonging on the many2one
comodel, like:
    <groupby name="partner_id">
        <field name="name"/> <!-- name of partner_id -->
        <button type="object" name="my_method" string="Button1" attrs="{'invisible': [('name', '=', 'Georges')]}"/>
    </groupby>

These extra fields are thus fetched in batch when grouping on the field.

Part of task 1915702
  • Loading branch information...
mge-odoo authored and aab-odoo committed Mar 13, 2019
1 parent fab6609 commit d2b8159a9f56c24dd3793e849a845b6414e8b885
@@ -0,0 +1,105 @@
odoo.define('web.ListModel', function (require) {
"use strict";

var BasicModel = require('web.BasicModel');

var ListModel = BasicModel.extend({

//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------

/**
* Overriden to add `groupData` when performing get on list datapoints.
*
* @override
* @see _readGroupExtraFields
*/
get: function () {
var result = this._super.apply(this, arguments);
var dp = result && this.localData[result.id];
if (dp && dp.groupData) {
// TODO: not sure I need to deep copy it, it's a datapoint
result.groupData = $.extend(true, {}, dp.groupData);
}
return result;
},
/**
* @override
* @param {Object} params.groups
*/
load: function (params) {
this.groups = params.groups;
return this._super.apply(this, arguments);
},

//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------

/**
*
* @override
* @private
*/
_readGroup: function (list) {
var self = this;
return this._super.apply(this, arguments).then(function (result) {
return self._readGroupExtraFields(list).then(_.constant(result));
});
},
/**
* Fetches group specific fields on the group by relation and stores it
* in the column datapoint in a special key `groupData`.
* Data for the groups are fetched in batch for all groups, to avoid
* doing multiple calls.
* Note that the option is only for m2o fields.
*
* @private
* @param {Object} list
* @returns {Promise}
*/
_readGroupExtraFields: function (list) {
var self = this;
var groupByFieldName = list.groupedBy[0].split(':')[0];
var groupedByField = list.fields[groupByFieldName];
if (groupedByField.type !== 'many2one' || !this.groups[groupByFieldName]) {
return Promise.resolve();
}
var groupIds = _.reduce(list.data, function (groupIds, id) {
var resId = self.get(id, { raw: true }).res_id;
if (resId) { // the field might be undefined when grouping
groupIds.push(resId);
}
return groupIds;
}, []);
var groupFields = Object.keys(this.groups[groupByFieldName].viewFields);
if (groupIds.length && groupFields.length) {
return this._rpc({
model: groupedByField.relation,
method: 'read',
args: [groupIds, groupFields],
context: list.context,
}).then(function (result) {
_.each(list.data, function (id) {
var dp = self.localData[id];
var groupData = _.findWhere(result, { id: dp.res_id });
var fvg = self.groups[groupByFieldName];
dp.groupData = self._makeDataPoint({
context: dp.context,
data: groupData,
fields: fvg.fields,
fieldsInfo: fvg.fieldsInfo,
modelName: fvg.model,
parentID: dp.id,
res_id: dp.res_id,
viewType: dp.viewType,
});
});
});
}
return Promise.resolve();
},
});
return ListModel;
});
@@ -68,6 +68,7 @@ var ListRenderer = BasicRenderer.extend({
this.pagers = []; // instantiated pagers (only for grouped lists)
this.editable = params.editable;
this.isGrouped = this.state.groupedBy.length > 0;
this.groups = params.groups;
},

//--------------------------------------------------------------------------
@@ -390,6 +391,54 @@ var ListRenderer = BasicRenderer.extend({
}
return $('<tfoot>').append($('<tr>').append($cells));
},
/**
* Renders the group button element.
*
* @private
* @param {Object} record
* @param {Object} group
* @returns {jQuery} a <button> element
*/
_renderGroupButton: function (list, node) {
var self = this;
var $button = viewUtils.renderButtonFromNode(node, {
extraClass: node.attrs.icon ? 'o_icon_button' : undefined,
textAsTitle: !!node.attrs.icon,
});
this._handleAttributes($button, node);
this._registerModifiers(node, list.groupData, $button);

// TODO this should be moved to a handler
$button.on("click", function (e) {
e.stopPropagation();
self.trigger_up('button_clicked', {
attrs: node.attrs,
record: list.groupData,
});
});
return $button;
},
/**
* Renders the group buttons.
*
* @private
* @param {Object} record
* @param {Object} group
* @returns {jQuery} a <button> element
*/
_renderGroupButtons: function (list, group) {
var self = this;
var $buttons = $();
if (list.value) {
// buttons make no sense for 'Undefined' group
group.arch.children.forEach(function (child) {
if (child.tag === 'button') {
$buttons = $buttons.add(self._renderGroupButton(list, child));
}
});
}
return $buttons;
},
/**
* Renders the pager for a given group
*
@@ -463,6 +512,13 @@ var ListRenderer = BasicRenderer.extend({
var $lastCell = $cells[$cells.length - 1] || $th;
$lastCell.addClass('o_group_pager').append($pager);
}
if (group.isOpen && this.groups[groupBy]) {
var $buttons = this._renderGroupButtons(group, this.groups[groupBy]);
if ($buttons.length) {
var $firstCell = $cells[1] || $th;
$firstCell.addClass('o_group_buttons').append($buttons);
}
}
return $('<tr>')
.addClass('o_group_header')
.toggleClass('o_group_open', group.isOpen)
@@ -11,6 +11,7 @@ odoo.define('web.ListView', function (require) {

var BasicView = require('web.BasicView');
var core = require('web.core');
var ListModel = require('web.ListModel');
var ListRenderer = require('web.ListRenderer');
var ListController = require('web.ListController');

@@ -21,6 +22,7 @@ var ListView = BasicView.extend({
display_name: _lt('List'),
icon: 'fa-list-ul',
config: _.extend({}, BasicView.prototype.config, {
Model: ListModel,
Renderer: ListRenderer,
Controller: ListController,
}),
@@ -34,26 +36,36 @@ var ListView = BasicView.extend({
* @param {boolean} [params.hasSelectors=true]
*/
init: function (viewInfo, params) {
var self = this;
this._super.apply(this, arguments);
var selectedRecords = []; // there is no selected records by default

var mode = this.arch.attrs.editable && !params.readonly ? "edit" : "readonly";
var expandGroups = !!JSON.parse(this.arch.attrs.expand || "0");

this.groups = {};
this.arch.children.forEach(function (child) {
if (child.tag === 'groupby') {
self._extractGroup(child);
}
});

this.controllerParams.editable = this.arch.attrs.editable;
this.controllerParams.hasSidebar = params.hasSidebar;
this.controllerParams.toolbarActions = viewInfo.toolbar;
this.controllerParams.mode = mode;
this.controllerParams.selectedRecords = selectedRecords;

this.rendererParams.arch = this.arch;
this.rendererParams.groups = this.groups;
this.rendererParams.hasSelectors =
'hasSelectors' in params ? params.hasSelectors : true;
this.rendererParams.editable = params.readonly ? false : this.arch.attrs.editable;
this.rendererParams.selectedRecords = selectedRecords;
this.rendererParams.addCreateLine = false;
this.rendererParams.addCreateLineInGroups = this.rendererParams.editable && this.controllerParams.activeActions.create;

this.loadParams.groups = this.groups;
this.loadParams.limit = this.loadParams.limit || 80;
this.loadParams.openGroupByDefault = expandGroups;
this.loadParams.type = 'list';
@@ -65,6 +77,14 @@ var ListView = BasicView.extend({
// Private
//--------------------------------------------------------------------------

/**
* @private
* @param {Object} node
*/
_extractGroup: function (node) {
var innerView = this.fields[node.attrs.name].views.groupby;
this.groups[node.attrs.name] = this._processFieldsView(innerView, 'groupby');
},
/**
* @override
*/
@@ -203,6 +203,7 @@ var MockServer = Class.extend({
var modifiersNames = ['invisible', 'readonly', 'required'];
var onchanges = this.data[model].onchanges || {};
var fieldNodes = {};
var groupbyNodes = {};

var doc;
if (typeof arch === 'string') {
@@ -220,6 +221,7 @@ var MockServer = Class.extend({
var modifiers = {};

var isField = (node.tagName === 'field');
var isGroupby = (node.tagName === 'groupby');

if (isField) {
var fieldName = node.getAttribute('name');
@@ -251,6 +253,10 @@ var MockServer = Class.extend({
modifiers[attr] = defaultValue;
}
});
} else if (isGroupby && !node._isProcessed) {
var groupbyName = node.getAttribute('name');
fieldNodes[groupbyName] = node;
groupbyNodes[groupbyName] = node;
}

// 'transfer_node_to_modifiers' simulation
@@ -290,6 +296,10 @@ var MockServer = Class.extend({
node.setAttribute('modifiers', JSON.stringify(modifiers));
}

if (isGroupby && !node._isProcessed) {
return false;
}

return !isField;
});

@@ -317,6 +327,21 @@ var MockServer = Class.extend({
node.setAttribute('on_change', "1");
}
});
_.each(groupbyNodes, function (node, name) {
var field = fields[name];
if (field.type !== 'many2one') {
throw new Error('groupby can only target many2one');
}
field.views = {};
relModel = field.relation;
relFields = $.extend(true, {}, self.data[relModel].fields);
node._isProcessed = true;
// postprocess simulation
field.views.groupby = self._fieldsViewGet(node, relModel, relFields, context);
node.childNodes.forEach(function (child) {
node.removeChild(child);
});
});

var xmlSerializer = new XMLSerializer();
var processedArch = xmlSerializer.serializeToString(doc);
Oops, something went wrong.

0 comments on commit d2b8159

Please sign in to comment.
You can’t perform that action at this time.