[IMP] web: list: display adjacent buttons in same column

Adjacent buttons in the arch are now displayed in a single column
in list views (main and x2many views). As soon as there is a node
in-between (even if it is invisible="1"), the rule doesn't apply
anymore and buttons placed before/after that node are rendered in
a separated column.

Task 2261774

closes odoo/odoo#51970

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Aaron Bohy
2020-05-29 07:40:24 +00:00
parent e34689411c
commit 042b4060bd
3 changed files with 224 additions and 32 deletions
@@ -58,6 +58,7 @@ var ListRenderer = BasicRenderer.extend({
*/
init: function (parent, state, params) {
this._super.apply(this, arguments);
this._preprocessColumns();
this.columnInvisibleFields = params.columnInvisibleFields;
this.rowDecorations = this._extractDecorationAttrs(this.arch);
this.fieldDecorations = {};
@@ -283,7 +284,62 @@ var ListRenderer = BasicRenderer.extend({
};
},
/**
* Removes the columns which should be invisible.
* Adjacent buttons (in the arch) are displayed in a single column. This
* function iterates over the arch's nodes and replaces "button" nodes by
* "button_group" nodes, with a single "button_group" node for adjacent
* "button" nodes. A "button_group" node has a "children" attribute
* containing all "button" nodes in the group.
*
* @private
*/
_groupAdjacentButtons: function () {
const children = [];
let groupId = 0;
let buttonGroupNode = null;
for (const c of this.arch.children) {
if (c.tag === 'button') {
if (!buttonGroupNode) {
buttonGroupNode = {
tag: 'button_group',
children: [c],
attrs: {
name: `button_group_${groupId++}`,
modifiers: {},
},
};
children.push(buttonGroupNode);
} else {
buttonGroupNode.children.push(c);
}
} else {
buttonGroupNode = null;
children.push(c);
}
}
this.arch.children = children;
},
/**
* Processes arch's child nodes for the needs of the list view:
* - detects oe_read_only/oe_edit_only classnames
* - groups adjacent buttons in a single column.
* This function is executed only once, at initialization.
*
* @private
*/
_preprocessColumns: function () {
this._processModeClassNames();
this._groupAdjacentButtons();
// set as readOnly (resp. editOnly) button groups containing only
// readOnly (resp. editOnly) buttons, s.t. no column is rendered
this.arch.children.filter(c => c.tag === 'button_group').forEach(c => {
c.attrs.editOnly = c.children.every(n => n.attrs.editOnly);
c.attrs.readOnly = c.children.every(n => n.attrs.readOnly);
});
},
/**
* Removes the columns which should be invisible. This function is executed
* at each (re-)rendering of the list.
*
* @param {Object} columnInvisibleFields contains the column invisible modifier values
*/
@@ -308,14 +364,6 @@ var ListRenderer = BasicRenderer.extend({
if (c.attrs.name in columnInvisibleFields) {
reject = columnInvisibleFields[c.attrs.name];
}
if (c.attrs.class) {
if (c.attrs.class.match(/\boe_edit_only\b/)) {
c.attrs.editOnly = true;
}
if (c.attrs.class.match(/\boe_read_only\b/)) {
c.attrs.readOnly = true;
}
}
if (!reject && c.attrs.widget === 'handle') {
self.handleField = c.attrs.name;
if (self.isGrouped) {
@@ -343,6 +391,22 @@ var ListRenderer = BasicRenderer.extend({
}
});
},
/**
* Classnames "oe_edit_only" and "oe_read_only" aim to only display the cell
* in the corresponding mode. This only concerns lists inside form views
* (for x2many fields). This function detects the className and stores a
* flag on the node's attrs accordingly, to ease further computations.
*
* @private
*/
_processModeClassNames: function () {
this.arch.children.forEach(c => {
if (c.attrs.class) {
c.attrs.editOnly = /\boe_edit_only\b/.test(c.attrs.class);
c.attrs.readOnly = /\boe_read_only\b/.test(c.attrs.class);
}
});
},
/**
* Render a list of <td>, with aggregates if available. It can be displayed
* in the footer, or for each open groups.
@@ -419,7 +483,7 @@ var ListRenderer = BasicRenderer.extend({
*/
_renderBodyCell: function (record, node, colIndex, options) {
var tdClassName = 'o_data_cell';
if (node.tag === 'button') {
if (node.tag === 'button_group') {
tdClassName += ' o_list_button';
} else if (node.tag === 'field') {
tdClassName += ' o_field_cell';
@@ -449,8 +513,11 @@ var ListRenderer = BasicRenderer.extend({
return $td;
}
if (node.tag === 'button') {
return $td.append(this._renderButton(record, node));
if (node.tag === 'button_group') {
for (const buttonNode of node.children) {
$td.append(this._renderButton(record, buttonNode));
}
return $td;
} else if (node.tag === 'widget') {
return $td.append(this._renderWidget(record, node));
}
+5 -2
View File
@@ -42,8 +42,11 @@
width: 17px;
height: 17px;
}
&.o_list_button .o_button_icon {
margin-right: 3px;
&.o_list_button > button {
padding: 0 5px;
.o_button_icon {
margin-right: 3px;
}
}
}
+140 -18
View File
@@ -277,6 +277,129 @@ QUnit.module('Views', {
list.destroy();
});
QUnit.test('list view with adjacent buttons', async function (assert) {
assert.expect(2);
const list = await createView({
View: ListView,
model: 'foo',
data: this.data,
arch: `
<tree>
<button name="a" type="object" icon="fa-car"/>
<field name="foo"/>
<button name="x" type="object" icon="fa-star"/>
<button name="y" type="object" icon="fa-refresh"/>
<button name="z" type="object" icon="fa-exclamation"/>
</tree>`,
});
assert.containsN(list, 'th', 4,
"adjacent buttons in the arch must be grouped in a single column");
assert.containsN(list.$('.o_data_row:first'), 'td.o_list_button', 2);
list.destroy();
});
QUnit.test('list view with adjacent buttons and invisible field', async function (assert) {
assert.expect(2);
const list = await createView({
View: ListView,
model: 'foo',
data: this.data,
arch: `
<tree>
<button name="a" type="object" icon="fa-car"/>
<field name="foo" invisible="1"/>
<button name="x" type="object" icon="fa-star"/>
<button name="y" type="object" icon="fa-refresh"/>
<button name="z" type="object" icon="fa-exclamation"/>
</tree>`,
});
assert.containsN(list, 'th', 3,
"adjacent buttons in the arch must be grouped in a single column");
assert.containsN(list.$('.o_data_row:first'), 'td.o_list_button', 2);
list.destroy();
});
QUnit.test('list view with adjacent buttons and invisible field (modifier)', async function (assert) {
assert.expect(2);
const list = await createView({
View: ListView,
model: 'foo',
data: this.data,
arch: `
<tree>
<button name="a" type="object" icon="fa-car"/>
<field name="foo" attrs="{'invisible': [['foo', '=', 'blip']]}"/>
<button name="x" type="object" icon="fa-star"/>
<button name="y" type="object" icon="fa-refresh"/>
<button name="z" type="object" icon="fa-exclamation"/>
</tree>`,
});
assert.containsN(list, 'th', 4,
"adjacent buttons in the arch must be grouped in a single column");
assert.containsN(list.$('.o_data_row:first'), 'td.o_list_button', 2);
list.destroy();
});
QUnit.test('list view with adjacent buttons and optional field', async function (assert) {
assert.expect(2);
const list = await createView({
View: ListView,
model: 'foo',
data: this.data,
arch: `
<tree>
<button name="a" type="object" icon="fa-car"/>
<field name="foo" optional="hide"/>
<button name="x" type="object" icon="fa-star"/>
<button name="y" type="object" icon="fa-refresh"/>
<button name="z" type="object" icon="fa-exclamation"/>
</tree>`,
});
assert.containsN(list, 'th', 3,
"adjacent buttons in the arch must be grouped in a single column");
assert.containsN(list.$('.o_data_row:first'), 'td.o_list_button', 2);
list.destroy();
});
QUnit.test('list view with adjacent buttons with invisible modifier', async function (assert) {
assert.expect(6);
const list = await createView({
View: ListView,
model: 'foo',
data: this.data,
arch: `
<tree>
<field name="foo"/>
<button name="x" type="object" icon="fa-star" attrs="{'invisible': [['foo', '=', 'blip']]}"/>
<button name="y" type="object" icon="fa-refresh" attrs="{'invisible': [['foo', '=', 'yop']]}"/>
<button name="z" type="object" icon="fa-exclamation" attrs="{'invisible': [['foo', '=', 'gnap']]}"/>
</tree>`,
});
assert.containsN(list, 'th', 3,
"adjacent buttons in the arch must be grouped in a single column");
assert.containsOnce(list.$('.o_data_row:first'), 'td.o_list_button');
assert.strictEqual(list.$('.o_field_cell').text(), 'yopblipgnapblip');
assert.containsN(list, 'td button i.fa-star:visible', 2);
assert.containsN(list, 'td button i.fa-refresh:visible', 3);
assert.containsN(list, 'td button i.fa-exclamation:visible', 3);
list.destroy();
});
QUnit.test('list view with icon buttons', async function (assert) {
assert.expect(5);
@@ -1884,7 +2007,6 @@ QUnit.module('Views', {
{ field: 'date', expected: 92, type: 'Date' },
{ field: 'datetime', expected: 146, type: 'Datetime' },
{ field: 'amount', expected: 104, type: 'Monetary' },
{ field: 'the_button', expected: 25, type: 'with custom width' },
];
assert.expect(9);
@@ -1901,7 +2023,7 @@ QUnit.module('Views', {
'<field name="date"/>' +
'<field name="datetime"/>' +
'<field name="amount"/>' +
'<button name="the_button" width="25px"></button>' +
'<field name="currency_id" width="25px"/>' +
'</tree>',
});
@@ -1912,6 +2034,8 @@ QUnit.module('Views', {
});
assert.strictEqual(list.$('th[data-name="foo"]')[0].style.width, '100%',
"Char field should occupy the remaining space");
assert.strictEqual(list.$('th[data-name="currency_id"]')[0].offsetWidth, 25,
'Currency field should have a fixed width of 25px (see arch)');
list.destroy();
});
@@ -1924,7 +2048,6 @@ QUnit.module('Views', {
{ field: 'date', expected: 92, type: 'Date' },
{ field: 'datetime', expected: 146, type: 'Datetime' },
{ field: 'amount', expected: 104, type: 'Monetary' },
{ field: 'the_button', expected: 25, type: 'with custom width' },
];
assert.expect(12);
@@ -1949,7 +2072,7 @@ QUnit.module('Views', {
<field name="date"/>
<field name="datetime"/>
<field name="amount"/>
<button name="the_button" width="25px"/>
<field name="currency_id" width="25px"/>
</tree>
</field>
</page>
@@ -1972,7 +2095,8 @@ QUnit.module('Views', {
});
assert.strictEqual(form.$('.o_field_one2many th[data-name="foo"]')[0].style.width, '100%',
"Char field should occupy the remaining space");
assert.strictEqual(form.$('th[data-name="currency_id"]')[0].offsetWidth, 25,
'Currency field should have a fixed width of 25px (see arch)');
assert.strictEqual(form.el.querySelector('.o_list_record_remove_header').style.width, '32px');
form.destroy();
@@ -2285,14 +2409,13 @@ QUnit.module('Views', {
});
QUnit.test('empty list: state with nameless and stringless buttons', async function (assert) {
assert.expect(3);
assert.expect(2);
this.data.foo.records = [];
const list = await createView({
arch: `
<tree>
<field name="foo"/>
<field name="text"/>
<button string="choucroute"/>
<button icon="fa-heart"/>
</tree>`,
@@ -2301,12 +2424,10 @@ QUnit.module('Views', {
View: ListView,
});
assert.strictEqual(list.el.querySelector('th[data-name="foo"]').style.width, '25%',
assert.strictEqual(list.el.querySelector('th[data-name="foo"]').style.width, '50%',
"Field column should be frozen");
assert.strictEqual(list.el.querySelector('th[data-string="choucroute"]').style.width, '25%',
"Nameless button column should be frozen");
assert.strictEqual(list.el.querySelector('th[data-icon="fa-heart"]').style.width, '25%',
"Nameless and stringless button should be frozen");
assert.strictEqual(list.el.querySelector('th:last-child').style.width, '50%',
"Buttons column should be frozen");
list.destroy();
});
@@ -2354,7 +2475,6 @@ QUnit.module('Views', {
{ field: 'date', expected: 92, type: 'Date' },
{ field: 'datetime', expected: 146, type: 'Datetime' },
{ field: 'amount', expected: 104, type: 'Monetary' },
{ field: 'the_button', expected: 25, type: 'with custom width' },
];
assert.expect(9);
@@ -2370,7 +2490,7 @@ QUnit.module('Views', {
'<field name="date"/>' +
'<field name="datetime"/>' +
'<field name="amount"/>' +
'<button name="the_button" width="25px"></button>' +
'<field name="currency_id" width="25px"/>' +
'</tree>',
groupBy: ['int_field'],
});
@@ -2382,6 +2502,8 @@ QUnit.module('Views', {
});
assert.strictEqual(list.$('th[data-name="foo"]')[0].style.width, '100%',
"Char field should occupy the remaining space");
assert.strictEqual(list.$('th[data-name="currency_id"]')[0].offsetWidth, 25,
"Currency field should have a fixed width of 25px (see arch)");
list.destroy();
});
@@ -4050,15 +4172,15 @@ QUnit.module('Views', {
assert.equal(list.$('tbody tr:nth(0) td:nth(4)').html(), "",
"td that contains an invisible field should be empty");
assert.equal(list.$('tbody tr:nth(0) td:nth(1)').html(), "",
"td that contains an invisible button should be empty");
assert.hasClass(list.$('tbody tr:nth(0) td:nth(1) button'), "o_invisible_modifier",
"button with invisible attrs should be properly hidden");
// edit first row
await testUtils.dom.click(list.$('tbody tr:nth(0) td:nth(2)'));
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(4) input.o_invisible_modifier').length, 1,
"td that contains an invisible field should not be empty in edition");
assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > button.o_invisible_modifier').length, 1,
"td that contains an invisible button should not be empty in edition");
assert.hasClass(list.$('tbody tr:nth(0) td:nth(1) button'), "o_invisible_modifier",
"button with invisible attrs should be properly hidden");
await testUtils.dom.click(list.$buttons.find('.o_list_button_discard'));
// click on the invisible field's cell to edit first row