diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 4940839e612..1c5c19ebcf8 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -444,7 +444,6 @@ var FieldChar = InputField.extend(TranslatableFieldMixin, { }, }); - var LinkButton = AbstractField.extend({ events: _.extend({}, AbstractField.prototype.events, { 'click': '_onClick' @@ -483,7 +482,6 @@ var LinkButton = AbstractField.extend({ event.stopPropagation(); window.open(this.value, '_blank'); }, - }); var FieldDate = InputField.extend({ @@ -1164,6 +1162,7 @@ var FieldText = InputField.extend(TranslatableFieldMixin, { */ var HandleWidget = AbstractField.extend({ className: 'o_row_handle fa fa-arrows ui-sortable-handle', + widthFactor: 0, tagName: 'span', description: "", supportedFieldTypes: ['integer'], diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 7be77cedca0..0f247ba3a8d 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -766,6 +766,7 @@ var FieldMany2One = AbstractField.extend({ }); var ListFieldMany2One = FieldMany2One.extend({ + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js index 9e31a188922..687f046b7e6 100644 --- a/addons/web/static/src/js/views/list/list_editable_renderer.js +++ b/addons/web/static/src/js/views/list/list_editable_renderer.js @@ -446,6 +446,41 @@ ListRenderer.include({ delete this.allFieldWidgets[recordID]; } }, + /** + * Returns the relative width according to the widget or the field type. + * @see _renderHeader + * + * @param {Object} column a `field` arch node + */ + _getColumnWidthFactor: function (column) { + if (column.attrs.width) { + // the width attribute has precedence on the width factor + return 0; + } + var fieldType = this.state.fields[column.attrs.name].type; + var widget = this.state.fieldsInfo.list[column.attrs.name].Widget.prototype; + if ('widthFactor' in widget) { + return widget.widthFactor; + } + switch (fieldType) { + case 'binary': return 1; + case 'boolean': return 0.4; + case 'char': return 1; + case 'date': return 1; + case 'datetime': return 1.5; + case 'float': return 1; + case 'html': return 3; + case 'integer': return 0.8; + case 'many2many': return 2.2; + case 'many2one': return 1.5; + case 'monetary': return 1.2; + case 'one2many': return 2.2; + case 'reference': return 1.5; + case 'selection': return 1.5; + case 'text': return 3; + default: return 1; + } + }, /** * * @returns {integer} @@ -654,6 +689,30 @@ ListRenderer.include({ } }); }, + /** + * Overridden to set weights on columns for the fixed layout. + * + * @override + * @private + */ + _processColumns: function () { + this._super.apply(this, arguments); + + if (this.editable) { + var self = this; + this.columns.forEach(function (column) { + if (column.attrs.width_factor) { + column.attrs.widthFactor = parseFloat(column.attrs.width_factor, 10); + } else { + if (column.tag === 'field') { + column.attrs.widthFactor = self._getColumnWidthFactor(column); + } else { + column.attrs.widthFactor = 1; + } + } + }); + } + }, /** * @override * @returns {Promise} @@ -708,6 +767,34 @@ ListRenderer.include({ } return $body; }, + /** + * Override to optionally add a th in the header for the remove icon column. + * + * @override + * @private + */ + _renderHeader: function () { + var $thead = this._super.apply(this, arguments); + + if (this.editable) { + var totalWidth = this.columns.reduce(function (acc, column) { + return acc + column.attrs.widthFactor; + }, 0); + this.columns.forEach(function (column) { + var $cell = $thead.find('th[data-name=' + column.attrs.name + ']'); + if (column.attrs.width) { + $cell.css('width', column.attrs.width); + } else if (column.attrs.widthFactor) { + $cell.css('width', (column.attrs.widthFactor / totalWidth * 100) + '%'); + } + }); + } + + if (this.addTrashIcon) { + $thead.find('tr').append($('', {class: 'o_list_record_remove_header'})); + } + return $thead; + }, /** * Editable rows are possibly extended with a trash icon on their right, to * allow deleting the corresponding record. 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 670f311c195..7a46e3b3cf2 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -54,7 +54,6 @@ var ListRenderer = BasicRenderer.extend({ // This attribute lets us know if there is a handle widget on a field, // and on which field it is set. this.handleField = null; - this._processColumns(params.columnInvisibleFields || {}); this.rowDecorations = _.chain(this.arch.attrs) .pick(function (value, key) { return DECORATIONS.indexOf(key) >= 0; @@ -67,6 +66,7 @@ var ListRenderer = BasicRenderer.extend({ this.editable = params.editable; this.isGrouped = this.state.groupedBy.length > 0; this.groupbys = params.groupbys; + this._processColumns(params.columnInvisibleFields || {}); }, //-------------------------------------------------------------------------- @@ -188,7 +188,7 @@ var ListRenderer = BasicRenderer.extend({ */ _processColumns: function (columnInvisibleFields) { var self = this; - self.handleField = null; + this.handleField = null; this.columns = _.reject(this.arch.children, function (c) { if (c.tag === 'control' || c.tag === 'groupby') { return true; @@ -313,7 +313,7 @@ var ListRenderer = BasicRenderer.extend({ isPassword: 'password' in node.attrs, }); this._handleAttributes($td, node); - return $td.html(formattedValue); + return $td.html(formattedValue).attr('title', formattedValue); }, /** * Renders the button element associated to the given node and record. @@ -641,13 +641,14 @@ var ListRenderer = BasicRenderer.extend({ } var description; if (node.attrs.widget) { + $th.addClass(' o_' + node.attrs.widget + '_cell'); description = this.state.fieldsInfo.list[name].Widget.prototype.description; } if (description === undefined) { description = node.attrs.string || field.string; } $th.text(description) - .data('name', name) + .attr('data-name', name) .toggleClass('o-sort-down', isNodeSorted ? !order[0].asc : false) .toggleClass('o-sort-up', isNodeSorted ? order[0].asc : false) .addClass(field.sortable && 'o_column_sortable'); @@ -669,6 +670,8 @@ var ListRenderer = BasicRenderer.extend({ attrs: node.attrs, }; this._addFieldTooltip(fieldDescr, $th); + } else { + $th.attr('title', description); } return $th; }, @@ -722,7 +725,7 @@ var ListRenderer = BasicRenderer.extend({ if (disableInput) { $content.find("input[type='checkbox']").prop('disabled', disableInput); } - return $('<' + tag + ' width="1">') + return $('<' + tag + '>') .addClass('o_list_record_selector') .append($content); }, diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss index 0882560cb10..fbd12c7e8a4 100644 --- a/addons/web/static/src/scss/list_view.scss +++ b/addons/web/static/src/scss/list_view.scss @@ -21,16 +21,18 @@ thead { color: $o-main-text-color; border-bottom: 1px solid #cacaca; - > tr > th { + > tr > th:not(.o_list_record_selector) { border-left: 1px solid #dfdfdf; - @include media-breakpoint-down(sm) { - white-space: nowrap; - } + @include o-text-overflow(table-cell); &:not(.o_column_sortable):hover { cursor: default; } } } + tbody > tr > td:not(.o_list_record_selector) { + @include o-text-overflow(table-cell); + white-space: normal; + } tfoot { cursor: default; @@ -76,7 +78,7 @@ } } - .o_list_record_selector, .o_list_record_remove, .o_handle_cell { + .o_list_record_selector { width: 1px; // to prevent the column to expand } @@ -161,6 +163,18 @@ //-------------------------------------------------------------------------- // Edition //-------------------------------------------------------------------------- + + &.o_editable_list { + table-layout: fixed; + + .o_list_record_selector { + width: 40px; + } + .o_list_record_remove_header, .o_handle_cell { + width: 32px; + } + } + .o_data_row.o_selected_row > .o_data_cell:not(.o_readonly_modifier) { position: relative; diff --git a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js index 4f2a3d03bed..419d7c5a4c7 100644 --- a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js @@ -2915,14 +2915,14 @@ QUnit.module('fields', {}, function () { await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input')); await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter')); - assert.containsOnce(form, 'th', + assert.containsOnce(form, 'th:not(.o_list_record_remove_header)', "should be 1 column when the product_id is set"); await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'), '', 'keyup'); - assert.containsN(form, 'th', 2, + assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2, "should be 2 columns in the one2many when product_id is not set"); await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input')); - assert.containsOnce(form, 'th', + assert.containsOnce(form, 'th:not(.o_list_record_remove_header)', "should be 1 column after the value change"); form.destroy(); }); @@ -2969,7 +2969,7 @@ QUnit.module('fields', {}, function () { await testUtils.fields.editInput(form.$('.o_field_one2many input:first'), 'New line'); await testUtils.dom.click(form.$el); - assert.containsN(form, 'th', 2, "should be 2 columns('foo' + 'int_field')"); + assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2, "should be 2 columns('foo' + 'int_field')"); form.destroy(); }); @@ -3008,14 +3008,14 @@ QUnit.module('fields', {}, function () { await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input')); await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter')); - assert.containsOnce(form, 'th', + assert.containsOnce(form, 'th:not(.o_list_record_remove_header)', "should be 1 column when the product_id is set"); await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'), '', 'keyup'); - assert.containsN(form, 'th', 2, + assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2, "should be 2 columns in the one2many when product_id is not set"); await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input')); - assert.containsOnce(form, 'th', + assert.containsOnce(form, 'th:not(.o_list_record_remove_header)', "should be 1 column after the value change"); form.destroy(); }); diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js index fa83e91bfbe..85975d99231 100644 --- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js @@ -831,10 +831,10 @@ QUnit.module('fields', {}, function () { 'The right values should be written'); } return this._super.apply(this, arguments); - } + }, }); - assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim(), "second record", + assert.deepEqual(form.$('.o_data_cell.o_many2many_tags_cell').text().trim(), "second record", "the partner_ids should be as specified at initialization"); await testUtils.form.clickEdit(form); @@ -843,7 +843,7 @@ QUnit.module('fields', {}, function () { await testUtils.fields.editSelect($cell, "hop"); await testUtils.form.clickSave(form); - assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim().split(/\s+/), + assert.deepEqual(form.$('.o_data_cell.o_many2many_tags_cell').text().trim().split(/\s+/), ["second", "record", "aaa"], 'The partner_ids should have been updated'); @@ -8153,14 +8153,14 @@ QUnit.module('fields', {}, function () { await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input')); await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter')); - assert.containsOnce(form, 'th', + assert.containsOnce(form, 'th:not(.o_list_record_remove_header)', "should be 1 column when the product_id is set"); await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'), '', 'keyup'); - assert.containsN(form, 'th', 2, + assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2, "should be 2 columns in the one2many when product_id is not set"); await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input')); - assert.containsOnce(form, 'th', + assert.containsOnce(form, 'th:not(.o_list_record_remove_header)', "should be 1 column after the value change"); form.destroy(); }); @@ -8207,7 +8207,7 @@ QUnit.module('fields', {}, function () { await testUtils.fields.editInput(form.$('.o_field_one2many input:first'), 'New line'); await testUtils.dom.click(form.$el); - assert.containsN(form, 'th', 2, "should be 2 columns('foo' + 'int_field')"); + assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2, "should be 2 columns('foo' + 'int_field')"); form.destroy(); }); @@ -8246,14 +8246,14 @@ QUnit.module('fields', {}, function () { await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input')); await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter')); - assert.containsOnce(form, 'th', + assert.containsOnce(form, 'th:not(.o_list_record_remove_header)', "should be 1 column when the product_id is set"); await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'), '', 'keyup'); - assert.containsN(form, 'th', 2, + assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2, "should be 2 columns in the one2many when product_id is not set"); await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input')); - assert.containsOnce(form, 'th', + assert.containsOnce(form, 'th:not(.o_list_record_remove_header)', "should be 1 column after the value change"); form.destroy(); }); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 96bd4a4d633..406370b0a2b 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -2758,7 +2758,7 @@ QUnit.module('relational_fields', { View: FormView, model: 'partner', data: this.data, - arch:'
' + + arch: '' + '' + '' + '' + @@ -2783,14 +2783,14 @@ QUnit.module('relational_fields', { await testUtils.form.clickEdit(form); await testUtils.fields.many2one.clickOpenDropdown("product_id"); await testUtils.fields.many2one.clickHighlightedItem("product_id"); - assert.containsOnce(form, 'th', + assert.containsOnce(form, 'th:not(.o_list_record_remove_header)', "should be 1 column when the product_id is set"); await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'), '', 'keyup'); - assert.containsN(form, 'th', 2, + assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2, "should be 2 columns in the one2many when product_id is not set"); await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input')); - assert.containsOnce(form, 'th', + assert.containsOnce(form, 'th:not(.o_list_record_remove_header)', "should be 1 column after the value change"); form.destroy(); }); @@ -2812,7 +2812,7 @@ QUnit.module('relational_fields', { View: FormView, model: 'partner', data: this.data, - arch:'' + + arch: '' + '' + '' + '' + @@ -2837,7 +2837,8 @@ QUnit.module('relational_fields', { await testUtils.fields.editInput(form.$('.o_field_one2many input:first'), 'New line'); await testUtils.dom.click(form.$el); - assert.containsN(form, 'th', 2, "should be 2 columns('foo' + 'int_field')"); + assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2, + "should be 2 columns('foo' + 'int_field')"); form.destroy(); }); @@ -2850,7 +2851,7 @@ QUnit.module('relational_fields', { View: FormView, model: 'partner', data: this.data, - arch:'' + + arch: '' + '' + '' + '' + @@ -2876,14 +2877,14 @@ QUnit.module('relational_fields', { await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input')); await testUtils.fields.many2one.clickHighlightedItem("product_id"); - assert.containsOnce(form, 'th', + assert.containsOnce(form, 'th:not(.o_list_record_remove_header)', "should be 1 column when the product_id is set"); await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'), '', 'keyup'); - assert.containsN(form, 'th', 2, + assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2, "should be 2 columns in the one2many when product_id is not set"); await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input')); - assert.containsOnce(form, 'th', + assert.containsOnce(form, 'th:not(.o_list_record_remove_header)', "should be 1 column after the value change"); form.destroy(); }); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index da095a8ac4b..d0f5035e1f1 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -5413,7 +5413,7 @@ QUnit.module('Views', { }); QUnit.test('form rendering with groups with col/colspan', async function (assert) { - assert.expect(46); + assert.expect(45); var form = await createView({ View: FormView, @@ -5530,10 +5530,6 @@ QUnit.module('Views', { assert.hasClass($fieldGroupFifthRowTds.eq(1),'o_td_label', "the second td should be a label td"); assert.strictEqual($fieldGroupFifthRowTds.eq(2).attr('style').substr(0, 9), "width: 50", "the third td should 50% width"); - // Verify that one2many list table hasn't been impacted - assert.hasAttrValue(form.$('.o_field_one2many th:first'), 'style', undefined, - "o2m list columns should have no width harcoded"); - form.destroy(); }); @@ -6744,7 +6740,7 @@ QUnit.module('Views', { assert.strictEqual($('.modal-body .o_form_view .o_list_view .o_data_cell').text(), "yopblip", "table has some initial order"); - await testUtils.dom.click($('.modal-body .o_form_view .o_list_view th')); + await testUtils.dom.click($('.modal-body .o_form_view .o_list_view th.o_column_sortable')); assert.strictEqual($('.modal-body .o_form_view .o_list_view .o_data_cell').text(), "blipyop", "table is now sorted"); form.destroy(); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 83b5ebd25c5..2584c047508 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -207,6 +207,27 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('editable rendering with handle', async function (assert) { + assert.expect(5); + + var list = await createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '' + + '' + + '' + + '' + + '', + }); + assert.containsN(list, 'thead th', 4, "there should be 4 th"); + assert.hasClass(list.$('thead th:eq(0)'), 'o_list_record_selector'); + assert.hasClass(list.$('thead th:eq(1)'), 'o_handle_cell'); + assert.strictEqual(list.$('thead th:eq(2)').attr('style'), "width: 50%;"); + assert.strictEqual(list.$('thead th:eq(3)').attr('style'), "width: 50%;"); + list.destroy(); + }); + QUnit.test('invisible columns are not displayed', async function (assert) { assert.expect(1); diff --git a/doc/reference/views.rst b/doc/reference/views.rst index 51f06877da9..56dca21bf1d 100644 --- a/doc/reference/views.rst +++ b/doc/reference/views.rst @@ -328,6 +328,10 @@ Possible children elements of the list view are: dynamic attributes based on record values. Only effects the current field, so e.g. ``invisible`` will hide the field but leave the same field of other records visible, it will not hide the column itself + ``width_factor`` (for ``editable``) + the column relative width (as the layout is fixed) + ``width`` (for ``editable``) + the column width (as the layout is fixed) .. note:: if the list view is ``editable``, any field attribute from the :ref:`form view ` is also valid and will diff --git a/odoo/addons/base/rng/common.rng b/odoo/addons/base/rng/common.rng index 3ec673c183e..8a2b9f6fbc1 100644 --- a/odoo/addons/base/rng/common.rng +++ b/odoo/addons/base/rng/common.rng @@ -215,6 +215,7 @@ +