From 042b4060bdbbda73f5e02c902a8ecf8ab7ef96cb Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 26 May 2020 15:26:27 +0000 Subject: [PATCH] [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) --- .../static/src/js/views/list/list_renderer.js | 91 ++++++++-- addons/web/static/src/scss/list_view.scss | 7 +- addons/web/static/tests/views/list_tests.js | 158 ++++++++++++++++-- 3 files changed, 224 insertions(+), 32 deletions(-) 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 04fbb987c12..fc026a332f0 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -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 , 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)); } diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss index 42749503829..35c0cf95ca3 100644 --- a/addons/web/static/src/scss/list_view.scss +++ b/addons/web/static/src/scss/list_view.scss @@ -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; + } } } diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index a8de3aa523c..98b85b2270e 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -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: ` + + ' + + '' + '', }); @@ -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', { - ' + + '' + '', 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