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:' |