[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.
This commit is contained in:
Aaron Bohy
2017-04-11 19:44:38 +02:00
parent e21c73305f
commit 378d6813ff
3 changed files with 150 additions and 14 deletions
@@ -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 () {
@@ -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 <tr> 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 = $('<span style="padding-right: 5px;">')
var $arrow = $('<span>')
.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 <tbody>
*/
_renderGroups: function () {
_renderGroups: function (data, groupLevel) {
var self = this;
groupLevel = groupLevel || 0;
var result = [];
var $tbody = $('<tbody>');
_.each(this.state.data, function (group) {
_.each(data, function (group) {
if (!$tbody) {
$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($('<td>'));
});
result.push($('<tbody>').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($('<td>'));
});
result.push($('<tbody>').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
+120
View File
@@ -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: '<tree><field name="id"/><field name="int_field"/></tree>',
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();
});
});
});