[FIX] web: one2many contains at least 4 lines

An X2many always has at least 4 lines. If the X2many contains less
than 4 filled lines (Record or "Add a line") empty lines are added
to make up the difference.

For example, an X2many in edit mode containing only one record:
- Line containing a record
- Line containing "Add a line
- Empty line
- Empty line

closes odoo/odoo#97070

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
FrancoisGe
2022-07-29 17:12:50 +02:00
parent 65a0eb7272
commit a4df8804f4
4 changed files with 81 additions and 29 deletions
@@ -670,10 +670,21 @@ export class ListRenderer extends Component {
}
get getEmptyRowIds() {
const nbEmptyRow = Math.max(0, 4 - this.props.list.records.length);
let nbEmptyRow = Math.max(0, 4 - this.props.list.records.length);
if (nbEmptyRow > 0 && this.displayRowCreates) {
nbEmptyRow -= 1;
}
return Array.from(Array(nbEmptyRow).keys());
}
get displayRowCreates() {
const activeActions = this.props.activeActions;
return (
activeActions &&
("canLink" in activeActions ? activeActions.canLink : activeActions.canCreate)
);
}
// Group headers logic:
// if there are aggregates, the first th spans until the first
// aggregate column then all cells between aggregates are rendered
@@ -1064,7 +1075,7 @@ export class ListRenderer extends Component {
switch (hotkey) {
case "tab":
// X2many add a line
if (activeActions && (activeActions.canLink || activeActions.canCreate)) {
if (this.displayRowCreates) {
if (record.isNew && !record.isDirty) {
list.unselectRecord(true);
return false;
@@ -1133,7 +1144,7 @@ export class ListRenderer extends Component {
case "tab": {
const index = list.records.indexOf(record);
if (index === list.records.length - 1) {
if (activeActions && (activeActions.canLink || activeActions.canCreate)) {
if (this.displayRowCreates) {
if (record.isNew && !record.isDirty) {
list.unselectRecord(true);
return false;
@@ -1208,11 +1219,7 @@ export class ListRenderer extends Component {
if (futureRecord) {
futureRecord.switchMode("edit");
} else if (
this.lastIsDirty ||
!record.canBeAbandoned ||
(activeActions && (activeActions.canLink || activeActions.canCreate))
) {
} else if (this.lastIsDirty || !record.canBeAbandoned || this.displayRowCreates) {
this.props.onAdd({ group });
} else {
futureRecord = list.records.at(0);
@@ -34,26 +34,6 @@
<t t-call="web.ListRenderer.Rows">
<t t-set="list" t-value="props.list"/>
</t>
<t t-set="activeActions" t-value="props.activeActions" />
<tr t-if="activeActions and ('canLink' in activeActions ? activeActions.canLink : activeActions.canCreate)">
<td t-if="withHandleColumn"/>
<td t-att-colspan="state.columns.length + 1"
class="o_field_x2many_list_row_add"
t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev, null)"
>
<t t-foreach="creates" t-as="create" t-key="create_index">
<a
href="#"
role="button"
t-att-class="create_index !== 0 ? 'ml16' : ''"
t-att-tabindex="props.list.editedRecord ? '-1' : '0'"
t-on-click.stop.prevent="() => this.props.onAdd({context: create.context})"
>
<t t-esc="create.description"/>
</a>
</t>
</td>
</tr>
</tbody>
<tfoot t-on-click="() => props.list.unselectRecord(true)" t-att-class="{o_sample_data_disabled: props.list.model.useSampleModel}">
<tr>
@@ -93,6 +73,25 @@
<t t-foreach="list.records" t-as="record" t-key="record.id">
<t t-call="web.ListRenderer.RecordRow"/>
</t>
<tr t-if="displayRowCreates">
<td t-if="withHandleColumn"/>
<td t-att-colspan="state.columns.length + 1"
class="o_field_x2many_list_row_add"
t-on-keydown.synthetic="(ev) => this.onCellKeydown(ev, null)"
>
<t t-foreach="creates" t-as="create" t-key="create_index">
<a
href="#"
role="button"
t-att-class="create_index !== 0 ? 'ml16' : ''"
t-att-tabindex="props.list.editedRecord ? '-1' : '0'"
t-on-click.stop.prevent="() => this.props.onAdd({context: create.context})"
>
<t t-esc="create.description"/>
</a>
</t>
</td>
</tr>
<t t-if="!props.list.isGrouped">
<tr t-foreach="getEmptyRowIds" t-as="emptyRowId" t-key="emptyRowId">
<td t-att-colspan="state.columns.length + 1">&#8203;</td>
@@ -12252,4 +12252,50 @@ QUnit.module("Fields", (hooks) => {
"localStorage getItem optional_fields,partner,form,100000001,turtles,list,display_name",
]);
});
QUnit.test(
"if there are less than 4 lines in a one2many, empty lines must be displayed to cover the difference.",
async function (assert) {
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="p">
<tree editable="bottom">
<field name="display_name"/>
</tree>
</field>
</form>`,
resId: 1,
});
// Should contain 4 blank lines
assert.containsNone(target, ".o_list_renderer tbody tr .o_data_row");
assert.containsNone(target, ".o_list_renderer tbody tr .o_field_x2many_list_row_add");
assert.containsN(target, ".o_list_renderer tbody tr", 4);
await clickEdit(target);
// Should only contain the "Add a line" line and 3 blank lines
assert.containsNone(target, ".o_list_renderer tbody tr .o_data_row");
assert.containsOnce(target, ".o_list_renderer tbody tr .o_field_x2many_list_row_add");
assert.hasClass(
target.querySelector(".o_list_renderer tbody tr td"),
"o_field_x2many_list_row_add"
);
assert.containsN(target, ".o_list_renderer tbody tr", 4);
await addRow(target);
// Should only contain a new row, the "Add a line" line and 2 blank lines
assert.containsOnce(target, ".o_list_renderer tbody tr.o_data_row");
assert.hasClass(target.querySelector(".o_list_renderer tbody tr"), "o_data_row");
assert.containsOnce(target, ".o_list_renderer tbody tr .o_field_x2many_list_row_add");
assert.hasClass(
target.querySelectorAll(".o_list_renderer tbody tr")[1].querySelector("td"),
"o_field_x2many_list_row_add"
);
assert.containsN(target, ".o_list_renderer tbody tr", 4);
}
);
});
@@ -332,7 +332,7 @@ tour.stepUtils.autoExpandMoreButtons('.o_form_readonly'),
position: 'bottom',
}, {
trigger: '.o_form_button_save',
extra_trigger: ".o_field_widget[name=bom_line_ids] tr:nth-child(5):has(.o_field_x2many_list_row_add)",
extra_trigger: ".o_field_widget[name=bom_line_ids] tr:nth-child(3):has(.o_field_x2many_list_row_add)",
content: _t('Save the bom.'),
position: 'bottom',
}, {