diff options
| author | Martin Geubelle <mge@odoo.com> | 2019-03-13 16:40:08 +0000 |
|---|---|---|
| committer | Aaron Bohy <aab@odoo.com> | 2019-04-02 11:55:57 +0000 |
| commit | 015958dc1730296e216a035f3b12b92df2dbd6c8 (patch) | |
| tree | 1499283c74febf9d360777b8a3cfc4c733ac1089 /addons/web/static/src | |
| parent | 2be28d4d033b5930e5a16ff02407315a116e9043 (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')
| -rw-r--r-- | addons/web/static/src/js/views/abstract_view.js | 2 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/list/list_controller.js | 17 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/list/list_model.js | 117 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/list/list_renderer.js | 125 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/list/list_view.js | 21 | ||||
| -rw-r--r-- | addons/web/static/src/scss/list_view.scss | 24 | ||||
| -rw-r--r-- | addons/web/static/src/xml/base.xml | 2 |
7 files changed, 280 insertions, 28 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) { diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss index 55f5b755df1..0882560cb10 100644 --- a/addons/web/static/src/scss/list_view.scss +++ b/addons/web/static/src/scss/list_view.scss @@ -103,12 +103,26 @@ .o_group_name { white-space: nowrap; } + .o_group_buttons { + display: inline-block; + + > button { + background-color: transparent; + border: none; + padding-top: 0; + padding-bottom: 0; + } + } .o_group_pager { - cursor: text; - user-select: none; - text-align: right; - padding-top: 0 !important; - padding-bottom: 0 !important; + position: relative; + + .o_pager { + @include o-position-absolute(0, 16px); + cursor: text; + user-select: none; + padding-top: 3px !important; + padding-bottom: 0 !important; + } .o_pager_previous, .o_pager_next { max-height: 30px; background-color: $o-list-group-header-color; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 75426a36d06..f9db8432275 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1498,7 +1498,7 @@ </button> </t> -<div t-name="Pager"> +<div t-name="Pager" class="o_pager"> <span class="o_pager_counter"> <span class="o_pager_value"></span> / <span class="o_pager_limit"></span> </span> |
