diff options
Diffstat (limited to 'addons/web/static')
| -rw-r--r-- | addons/web/static/src/js/views/list/list_renderer.js | 91 | ||||
| -rw-r--r-- | addons/web/static/src/scss/list_view.scss | 7 | ||||
| -rw-r--r-- | 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) { @@ -344,6 +392,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)); } 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: ` + <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 |
