summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js
diff options
context:
space:
mode:
authorMartin Geubelle <mge@odoo.com>2019-03-13 16:40:08 +0000
committerAaron Bohy <aab@odoo.com>2019-04-02 11:55:57 +0000
commit015958dc1730296e216a035f3b12b92df2dbd6c8 (patch)
tree1499283c74febf9d360777b8a3cfc4c733ac1089 /addons/web/static/src/js
parent2be28d4d033b5930e5a16ff02407315a116e9043 (diff)
[IMP] web,base: put buttons in list group headers
It is now possible to put buttons in the list view group headers. When the view is grouped by a many2one field, those buttons 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="groupedField"> <!-- must be a many2one --> <button type="object" name="my_method" string="Button1"/> </groupby> It is also possible to add `field`, inside the `groupby` which can be used for modifiers. These fields thus belong 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 fetched in batch when grouping on the field. Part of task 1915702
Diffstat (limited to 'addons/web/static/src/js')
-rw-r--r--addons/web/static/src/js/views/abstract_view.js2
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js17
-rw-r--r--addons/web/static/src/js/views/list/list_model.js117
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js125
-rw-r--r--addons/web/static/src/js/views/list/list_view.js21
5 files changed, 260 insertions, 22 deletions
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index 276b29b71eb..dbb1b3e557c 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -90,6 +90,8 @@ var AbstractView = Factory.extend({
* @param {boolean} [params.withControlPanel=true]
*/
init: function (viewInfo, params) {
+ this._super.apply(this, arguments);
+
var action = params.action || {};
params = _.defaults(params, this._extractParamsFromAction(action));
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index a7b71ad4cf2..c96b677e2ea 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -26,6 +26,7 @@ var ListController = BasicController.extend({
activate_next_widget: '_onActivateNextWidget',
add_record: '_onAddRecord',
button_clicked: '_onButtonClicked',
+ group_edit_button_clicked: '_onEditGroupClicked',
edit_line: '_onEditLine',
save_line: '_onSaveLine',
resequence: '_onResequence',
@@ -529,6 +530,22 @@ var ListController = BasicController.extend({
new DataExport(this, record, defaultExportFields).open();
},
/**
+ * Opens the related form view.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onEditGroupClicked: function (ev) {
+ ev.stopPropagation();
+ this.do_action({
+ type: 'ir.actions.act_window',
+ views: [[false, 'form']],
+ res_model: ev.data.record.model,
+ res_id: ev.data.record.res_id,
+ flags: {mode: 'edit'},
+ });
+ },
+ /**
* Force a resequence of the records curently on this page.
*
* @private
diff --git a/addons/web/static/src/js/views/list/list_model.js b/addons/web/static/src/js/views/list/list_model.js
new file mode 100644
index 00000000000..0be8f54e4f1
--- /dev/null
+++ b/addons/web/static/src/js/views/list/list_model.js
@@ -0,0 +1,117 @@
+odoo.define('web.ListModel', function (require) {
+ "use strict";
+
+ var BasicModel = require('web.BasicModel');
+
+ var ListModel = BasicModel.extend({
+
+ /**
+ * @override
+ * @param {Object} params.groupbys
+ */
+ init: function (parent, params) {
+ this._super.apply(this, arguments);
+
+ this.groupbys = params.groupbys;
+ },
+
+ //--------------------------------------------------------------------------
+ // 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) {
+ result.groupData = this.get(dp.groupData);
+ }
+ return result;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ *
+ * @override
+ * @private
+ */
+ _readGroup: function (list) {
+ var self = this;
+ return this._super.apply(this, arguments).then(function (result) {
+ var prom;
+ if (!list.parentID) {
+ // groupbys buttons are only displayed on the first level of
+ // groupby so no need to fetch the extra fields for inner
+ // groups
+ prom = self._readGroupExtraFields(list);
+ }
+ return Promise.resolve(prom).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.groupbys[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.groupbys[groupByFieldName].viewFields);
+ var prom;
+ if (groupIds.length && groupFields.length) {
+ prom = this._rpc({
+ model: groupedByField.relation,
+ method: 'read',
+ args: [groupIds, groupFields],
+ context: list.context,
+ });
+ }
+ return Promise.resolve(prom).then(function (result) {
+ var fvg = self.groupbys[groupByFieldName];
+ _.each(list.data, function (id) {
+ var dp = self.localData[id];
+ var groupData = result && _.findWhere(result, {
+ id: dp.res_id,
+ });
+ var groupDp = self._makeDataPoint({
+ context: dp.context,
+ data: groupData,
+ fields: fvg.fields,
+ fieldsInfo: fvg.fieldsInfo,
+ modelName: groupedByField.relation,
+ parentID: dp.id,
+ res_id: dp.res_id,
+ viewType: 'groupby',
+ });
+ dp.groupData = groupDp.id;
+ });
+ });
+ },
+ });
+ return ListModel;
+});
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index e6462d99609..670f311c195 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -31,8 +31,6 @@ var FIELD_CLASSES = {
text: 'o_list_text',
};
-var HEADING_COLUMNS_TO_SKIP_IN_GROUPS = 2;
-
var ListRenderer = BasicRenderer.extend({
events: {
'click tbody tr': '_onRowClicked',
@@ -68,6 +66,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.groupbys = params.groupbys;
},
//--------------------------------------------------------------------------
@@ -191,7 +190,7 @@ var ListRenderer = BasicRenderer.extend({
var self = this;
self.handleField = null;
this.columns = _.reject(this.arch.children, function (c) {
- if (c.tag === 'control') {
+ if (c.tag === 'control' || c.tag === 'groupby') {
return true;
}
var reject = c.attrs.modifiers.column_invisible;
@@ -212,16 +211,12 @@ var ListRenderer = BasicRenderer.extend({
*
* @private
* @param {any} aggregateValues
- * @param {Boolean} isHeader indicates wheter the groups rendered are on the header or on the footer
* @returns {jQueryElement[]} a list of <td> with the aggregate values
*/
- _renderAggregateCells: function (aggregateValues, isHeader) {
+ _renderAggregateCells: function (aggregateValues) {
var self = this;
- return _.map(this.columns, function (column, index) {
- if (isHeader && index === 0) {
- return;
- }
+ return _.map(this.columns, function (column) {
var $cell = $('<td>');
if (config.debug) {
$cell.addClass(column.attrs.name);
@@ -384,13 +379,67 @@ var ListRenderer = BasicRenderer.extend({
aggregates[column.attrs.name] = column.aggregate;
}
});
- var $cells = this._renderAggregateCells(aggregates, false);
+ var $cells = this._renderAggregateCells(aggregates);
if (this.hasSelectors) {
$cells.unshift($('<td>'));
}
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();
+ if (node.attrs.type === 'edit') {
+ self.trigger_up('group_edit_button_clicked', {
+ record: list.groupData,
+ });
+ } else {
+ 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
*
* @private
@@ -418,7 +467,7 @@ var ListRenderer = BasicRenderer.extend({
var pagerProm = pager._widgetRenderAndInsert(function () {}); // start the pager
this.defs.push(pagerProm);
- var $el = $('<div>');
+ var $el = $('<div>');
pagerProm.then(function () {
$el.replaceWith(pager.$el);
});
@@ -434,10 +483,8 @@ var ListRenderer = BasicRenderer.extend({
* @returns {jQueryElement} a <tr> element
*/
_renderGroupRow: function (group, groupLevel) {
- var aggregateValues = _.mapObject(group.aggregateValues, function (value) {
- return { value: value };
- });
- var $cells = this._renderAggregateCells(aggregateValues, true);
+ var cells = [];
+
var name = group.value === undefined ? _t('Undefined') : group.value;
var groupBy = this.state.groupedBy[groupLevel];
if (group.fields[groupBy.split(':')[0]].type !== 'boolean') {
@@ -445,10 +492,7 @@ var ListRenderer = BasicRenderer.extend({
}
var $th = $('<th>')
.addClass('o_group_name')
- .text(name + ' (' + group.count + ')');
- if (this.hasSelectors) {
- $th.attr('colspan', HEADING_COLUMNS_TO_SKIP_IN_GROUPS);
- }
+ .text(name + ' (' + group.count + ')')
var $arrow = $('<span>')
.css('padding-left', (groupLevel * 20) + 'px')
.css('padding-right', '5px')
@@ -458,18 +502,55 @@ var ListRenderer = BasicRenderer.extend({
.toggleClass('fa-caret-down', group.isOpen);
}
$th.prepend($arrow);
+ cells.push($th);
+
+ var aggregateKeys = Object.keys(group.aggregateValues);
+ var aggregateValues = _.mapObject(group.aggregateValues, function (value) {
+ return { value: value };
+ });
+ var aggregateCells = this._renderAggregateCells(aggregateValues);
+ var firstAggregateIndex = _.findIndex(this.columns, function (column) {
+ return column.tag === 'field' && _.contains(aggregateKeys, column.attrs.name);
+ });
+ var colspanBeforeAggregate;
+ if (firstAggregateIndex !== -1) {
+ // if there are aggregates, the first $th goes until the first
+ // aggregate then all cells between aggregates are rendered, then
+ // there is a last $th for the pager
+ colspanBeforeAggregate = firstAggregateIndex;
+ var lastAggregateIndex = _.findLastIndex(this.columns, function (column) {
+ return column.tag === 'field' && _.contains(aggregateKeys, column.attrs.name);
+ });
+ cells = cells.concat(aggregateCells.slice(firstAggregateIndex, lastAggregateIndex + 1));
+ cells.push($('<th>').attr('colspan', this.columns.length - 1 - lastAggregateIndex));
+ } else {
+ colspanBeforeAggregate = this.columns.length;
+ }
+ if (this.hasSelectors) {
+ colspanBeforeAggregate += 1;
+ }
+ $th.attr('colspan', colspanBeforeAggregate);
+
if (group.isOpen && !group.groupedBy.length && (group.count > group.data.length)) {
+ var $lastCell = cells[cells.length - 1];
var $pager = this._renderGroupPager(group);
- var $lastCell = $cells[$cells.length - 1] || $th;
$lastCell.addClass('o_group_pager').append($pager);
}
+ if (group.isOpen && this.groupbys[groupBy]) {
+ var $buttons = this._renderGroupButtons(group, this.groupbys[groupBy]);
+ if ($buttons.length) {
+ var $buttonSection = $('<div>', {
+ class: 'o_group_buttons',
+ }).append($buttons);
+ $th.append($buttonSection);
+ }
+ }
return $('<tr>')
.addClass('o_group_header')
.toggleClass('o_group_open', group.isOpen)
.toggleClass('o_group_has_content', group.count > 0)
.data('group', group)
- .append($th)
- .append($cells);
+ .append(cells);
},
/**
* Render the content of a given opened group.
diff --git a/addons/web/static/src/js/views/list/list_view.js b/addons/web/static/src/js/views/list/list_view.js
index e65a8c4adc4..ade6a7eb676 100644
--- a/addons/web/static/src/js/views/list/list_view.js
+++ b/addons/web/static/src/js/views/list/list_view.js
@@ -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,12 +36,20 @@ 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.groupbys = {};
+ 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;
@@ -47,6 +57,7 @@ var ListView = BasicView.extend({
this.controllerParams.selectedRecords = selectedRecords;
this.rendererParams.arch = this.arch;
+ this.rendererParams.groupbys = this.groupbys;
this.rendererParams.hasSelectors =
'hasSelectors' in params ? params.hasSelectors : true;
this.rendererParams.editable = params.readonly ? false : this.arch.attrs.editable;
@@ -54,6 +65,8 @@ var ListView = BasicView.extend({
this.rendererParams.addCreateLine = false;
this.rendererParams.addCreateLineInGroups = this.rendererParams.editable && this.controllerParams.activeActions.create;
+ this.modelParams.groupbys = this.groupbys;
+
this.loadParams.limit = this.loadParams.limit || 80;
this.loadParams.openGroupByDefault = expandGroups;
this.loadParams.type = 'list';
@@ -66,6 +79,14 @@ var ListView = BasicView.extend({
//--------------------------------------------------------------------------
/**
+ * @private
+ * @param {Object} node
+ */
+ _extractGroup: function (node) {
+ var innerView = this.fields[node.attrs.name].views.groupby;
+ this.groupbys[node.attrs.name] = this._processFieldsView(innerView, 'groupby');
+ },
+ /**
* @override
*/
_extractParamsFromAction: function (action) {