From 378d6813ff03a428537b98bb7232c09ef4d957d5 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Wed, 15 Mar 2017 15:40:21 +0100 Subject: [PATCH] [IMP] web: list view: nested groups Before this commit, only one level of groups was supported in list views. This commit enables the nested groups support. --- .../static/src/js/views/basic/basic_model.js | 11 +- .../static/src/js/views/list/list_renderer.js | 33 +++-- addons/web/static/test/views/list_tests.js | 120 ++++++++++++++++++ 3 files changed, 150 insertions(+), 14 deletions(-) diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 6c59d242cb0..5180cc41656 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -933,7 +933,7 @@ var BasicModel = AbstractModel.extend({ group.isOpen = true; var def; if (group.count > 0) { - def = this._fetchUngroupedList(group); + def = this._load(group); } return $.when(def).then(function () { return groupId; @@ -2048,6 +2048,7 @@ var BasicModel = AbstractModel.extend({ var fields = _.uniq(list.fieldNames.concat(list.groupedBy)); return this._rpc(list.model, 'read_group') .withFields(fields) + .withDomain(list.domain) .withContext(list.context) .groupBy(list.groupedBy) .lazy(true) @@ -2071,7 +2072,7 @@ var BasicModel = AbstractModel.extend({ var newGroup = self._makeDataPoint({ modelName: list.model, count: group[rawGroupBy + '_count'], - domain: group.__domain, + domain: list.domain.concat(group.__domain), context: list.context, fields: list.fields, fieldNames: list.fieldNames, @@ -2082,7 +2083,7 @@ var BasicModel = AbstractModel.extend({ orderedBy: list.orderedBy, limit: list.limit, openGroupByDefault: list.openGroupByDefault, - type: 'list', // nested groupedBys not handled yet + type: 'list', }); list.data.push(newGroup.id); var old_group = _.find(previousGroups, function (g) { @@ -2090,13 +2091,15 @@ var BasicModel = AbstractModel.extend({ }); if (old_group) { newGroup.isOpen = old_group.isOpen; + // copy the oldGroup data to keep 'isOpen' state of sub-groups + newGroup.data = newGroup.isOpen ? old_group.data : []; } else if (!newGroup.openGroupByDefault) { newGroup.isOpen = false; } else { newGroup.isOpen = '__fold' in group ? !group.__fold : true; } if (newGroup.isOpen && newGroup.count > 0) { - defs.push(self._fetchUngroupedList(newGroup)); + defs.push(self._load(newGroup)); } }); return $.when.apply($, defs).then(function () { 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 d82e31fc781..5618a0e36e2 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -354,9 +354,10 @@ var ListRenderer = BasicRenderer.extend({ * * @private * @param {Object} group + * @param {integer} groupLevel the nesting level (0 for root groups) * @returns {jQueryElement} a element */ - _renderGroupRow: function (group) { + _renderGroupRow: function (group, groupLevel) { var aggregateValues = _.mapObject(group.aggregateValues, function (value) { return { value: value }; }); @@ -370,7 +371,9 @@ var ListRenderer = BasicRenderer.extend({ .addClass('o_group_name') .text(name + ' (' + group.count + ')'); if (group.count > 0) { - var $arrow = $('') + var $arrow = $('') + .css('padding-left', (groupLevel * 20) + 'px') + .css('padding-right', '5px') .addClass('fa') .toggleClass('fa-caret-right', !group.isOpen) .toggleClass('fa-caret-down', group.isOpen); @@ -390,23 +393,33 @@ var ListRenderer = BasicRenderer.extend({ * row, then possibly a bunch of rows for each record. * * @private + * @param {Object} data the dataPoint containing the groups + * @param {integer} [groupLevel=0] the nesting level. 0 is for the root group * @returns {jQueryElement[]} a list of */ - _renderGroups: function () { + _renderGroups: function (data, groupLevel) { var self = this; + groupLevel = groupLevel || 0; var result = []; var $tbody = $(''); - _.each(this.state.data, function (group) { + _.each(data, function (group) { if (!$tbody) { $tbody = $(''); } - $tbody.append(self._renderGroupRow(group)); + $tbody.append(self._renderGroupRow(group, groupLevel)); if (group.data.length) { result.push($tbody); - var $records = _.map(group.data, function (record) { - return self._renderRow(record).prepend($('')); - }); - result.push($('').append($records)); + // render an opened group + if (group.groupedBy.length) { + // the opened group contains subgroups + result = result.concat(self._renderGroups(group.data, groupLevel + 1)); + } else { + // the opened group contains records + var $records = _.map(group.data, function (record) { + return self._renderRow(record).prepend($('')); + }); + result.push($('').append($records)); + } $tbody = null; } }); @@ -548,7 +561,7 @@ var ListRenderer = BasicRenderer.extend({ if (is_grouped) { $table .append(this._renderHeader(true)) - .append(this._renderGroups()) + .append(this._renderGroups(this.state.data)) .append(this._renderFooter(true)); } else { $table diff --git a/addons/web/static/test/views/list_tests.js b/addons/web/static/test/views/list_tests.js index 069de6f8b31..32cdb296ed9 100644 --- a/addons/web/static/test/views/list_tests.js +++ b/addons/web/static/test/views/list_tests.js @@ -1077,6 +1077,126 @@ QUnit.module('Views', { list.destroy(); }); + + QUnit.test('list view with nested groups', function (assert) { + assert.expect(36); + + this.data.foo.records.push({id: 5, foo: "blip", int_field: -7, m2o: 1}); + + var nbRPCs = {readGroup: 0, searchRead: 0}; + var groupID; + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '', + groupBy: ['m2o', 'foo'], + mockRPC: function (route, args) { + if (args.method === 'read_group') { + if (args.kwargs.groupby[0] === 'foo') { // nested read_group + // called twice (once when opening the group, once when sorting) + assert.deepEqual(args.kwargs.domain, [['m2o', '=', 1]], + "nested read_group should be called with correct domain"); + } + nbRPCs.readGroup++; + } else if (route === '/web/dataset/search_read') { + // called twice (once when opening the group, once when sorting) + assert.deepEqual(args.domain, [['m2o', '=', 1], ['foo', '=', 'blip']], + "nested search_read should be called with correct domain"); + nbRPCs.searchRead++; + } + return this._super.apply(this, arguments); + }, + intercepts: { + open_record: function (event) { + event.stopPropagation(); + assert.strictEqual(event.data.id, groupID, + "'open_record' event should have been triggered"); + }, + }, + }); + + assert.strictEqual(nbRPCs.readGroup, 1, "should have done one read_group"); + assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read"); + + // basic rendering tests + assert.strictEqual(list.$('tbody').length, 1, "there should be 1 tbody"); + assert.strictEqual(list.$('.o_group_header').length, 2, + "should contain 2 groups at first level"); + assert.strictEqual(list.$('.o_group_name:first').text(), 'Value 1 (4)', + "group should have correct name and count"); + assert.strictEqual(list.$('.o_group_name .fa-caret-right').length, 2, + "the carret of closed groups should be right"); + assert.strictEqual(list.$('.o_group_name:first span').css('padding-left'), + '0px', "groups of level 1 should have a 0px padding-left"); + assert.strictEqual(list.$('.o_group_header:first td:last').text(), '16', + "group aggregates are correctly displayed"); + + // open the first group + nbRPCs = {readGroup: 0, searchRead: 0}; + list.$('.o_group_header:first').click(); + assert.strictEqual(nbRPCs.readGroup, 1, "should have done one read_group"); + assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read"); + + var $openGroup = list.$('tbody:nth(1)'); + assert.strictEqual(list.$('tbody').length, 3, "there should be 3 tbodys"); + assert.strictEqual(list.$('.o_group_name:first .fa-caret-down').length, 1, + "the carret of open groups should be down"); + assert.strictEqual($openGroup.find('.o_group_header').length, 3, + "open group should contain 3 groups"); + assert.strictEqual($openGroup.find('.o_group_name:nth(2)').text(), 'blip (2)', + "group should have correct name and count"); + assert.strictEqual($openGroup.find('.o_group_name:nth(2) span').css('padding-left'), + '20px', "groups of level 2 should have a 20px padding-left"); + assert.strictEqual($openGroup.find('.o_group_header:nth(2) td:last').text(), '-11', + "inner group aggregates are correctly displayed"); + + // open subgroup + nbRPCs = {readGroup: 0, searchRead: 0}; + $openGroup.find('.o_group_header:nth(2)').click(); + assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group"); + assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read"); + + var $openSubGroup = list.$('tbody:nth(2)'); + assert.strictEqual(list.$('tbody').length, 4, "there should be 4 tbodys"); + assert.strictEqual($openSubGroup.find('.o_data_row').length, 2, + "open subgroup should contain 2 data rows"); + assert.strictEqual($openSubGroup.find('.o_data_row:first td:last').text(), '-4', + "first record in open subgroup should be res_id 4 (with int_field -4)"); + + // open a record (should trigger event 'open_record') + var $row = $openSubGroup.find('.o_data_row:first'); + groupID = $row.data('id'); + $openSubGroup.find('.o_data_row:first').click(); + + // sort by int_field (ASC) and check that open groups are still open + nbRPCs = {readGroup: 0, searchRead: 0}; + list.$('thead th:last').click(); + assert.strictEqual(nbRPCs.readGroup, 2, "should have done two read_groups"); + assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read"); + + $openSubGroup = list.$('tbody:nth(2)'); + assert.strictEqual(list.$('tbody').length, 4, "there should be 4 tbodys"); + assert.strictEqual($openSubGroup.find('.o_data_row').length, 2, + "open subgroup should contain 2 data rows"); + assert.strictEqual($openSubGroup.find('.o_data_row:first td:last').text(), '-7', + "first record in open subgroup should be res_id 5 (with int_field -7)"); + + // close first level group + nbRPCs = {readGroup: 0, searchRead: 0}; + list.$('.o_group_header:first').click(); + assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group"); + assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read"); + + assert.strictEqual(list.$('tbody').length, 1, "there should be 1 tbody"); + assert.strictEqual(list.$('.o_group_header').length, 2, + "should contain 2 groups at first level"); + assert.strictEqual(list.$('.o_group_name .fa-caret-right').length, 2, + "the carret of closed groups should be right"); + + list.destroy(); + }); }); });