[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:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user