[FIX] web: complete x2m activeField

The goal of this commit is to complete the list of activeFields for
an x2many in list mode when a record is opened with the fields present
in its form view. This ensures that there is always an activeField
for all the data in a record. Before this commit, when the x2many's
form view was not inline, when the form view was closed, the fields
not present in the list view no longer had an activeField, which could
cause crashes.

closes odoo/odoo#130453

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
FrancoisGe
2023-08-02 16:00:01 +02:00
parent 7e6dd0e7c0
commit ce774f1322
3 changed files with 131 additions and 16 deletions
@@ -3,6 +3,7 @@
import { x2ManyCommands } from "@web/core/orm_service";
import { intersection } from "@web/core/utils/arrays";
import { pick } from "@web/core/utils/objects";
import { completeActiveFields } from "@web/model/relational_model/utils";
import { DataPoint } from "./datapoint";
import { fromUnityToServerValues, getId, patchActiveFields } from "./utils";
@@ -202,6 +203,7 @@ export class StaticList extends DataPoint {
extendRecord(params, record) {
return this.model.mutex.exec(async () => {
// extend fields and activeFields of the list with those given in params
completeActiveFields(this.config.activeFields, params.activeFields);
Object.assign(this.fields, params.fields);
const activeFields = { ...params.activeFields };
for (const fieldName in this.activeFields) {
@@ -70,6 +70,20 @@ function completeActiveField(activeField, extra) {
}
}
export function completeActiveFields(activeFields, extraActiveFields) {
for (const fieldName in extraActiveFields) {
const extraActiveField = {
...extraActiveFields[fieldName],
invisible: true,
};
if (fieldName in activeFields) {
completeActiveField(activeFields[fieldName], extraActiveField);
} else {
activeFields[fieldName] = extraActiveField;
}
}
}
export function createPropertyActiveField(property) {
const { type } = property;
@@ -182,20 +196,10 @@ export function extractFieldsFromArchInfo({ fieldNodes, widgetNodes }, fields) {
fieldNode.views.form,
fieldNode.views.form.fields
);
for (const fieldName in formArchInfo.activeFields) {
const formActiveField = {
...formArchInfo.activeFields[fieldName],
invisible: true,
};
if (fieldName in activeField.related.activeFields) {
completeActiveField(
activeField.related.activeFields[fieldName],
formActiveField
);
} else {
activeField.related.activeFields[fieldName] = formActiveField;
}
}
completeActiveFields(
activeField.related.activeFields,
formArchInfo.activeFields
);
Object.assign(activeField.related.fields, formArchInfo.fields);
}
}
@@ -546,7 +546,9 @@ QUnit.module("Fields", (hooks) => {
await editInput(target, ".o_field_widget[name=parent_id] input", "ABC");
await clickOpenedDropdownItem(target, "parent_id", "Create and edit...");
await click(
target.querySelector(".o_dialog:not(.o_inactive_modal) .modal-footer .o_form_button_save")
target.querySelector(
".o_dialog:not(.o_inactive_modal) .modal-footer .o_form_button_save"
)
);
assert.strictEqual(
target
@@ -558,7 +560,9 @@ QUnit.module("Fields", (hooks) => {
await nextTick();
// close all dialogs
await click(
target.querySelector(".o_dialog:not(.o_inactive_modal) .modal-footer .o_form_button_save")
target.querySelector(
".o_dialog:not(.o_inactive_modal) .modal-footer .o_form_button_save"
)
);
await nextTick();
assert.containsNone(target, ".o_dialog .o_form_view");
@@ -3216,6 +3220,111 @@ QUnit.module("Fields", (hooks) => {
}
);
QUnit.test(
"onchange specification complete after open sub form view not inline",
async function (assert) {
serverData.models.partner.onchanges.display_name = () => {};
serverData.views = {
"turtle,false,form": `
<form>
<field name="display_name"/>
<field name="partner_ids">
<tree>
<field name="display_name"/>
</tree>
</field>
</form>`,
};
await makeView({
type: "form",
resModel: "partner",
serverData,
arch: `
<form>
<field name="display_name"/>
<field name="turtles">
<tree>
<field name="turtle_foo"/>
</tree>
</field>
</form>`,
resId: 1,
async mockRPC(route, args) {
if (args.method === "onchange2" && args.model === "partner") {
if (args.args[1].display_name === "test") {
assert.deepEqual(args.args[3], {
display_name: {},
turtles: {
fields: {
turtle_foo: {},
},
limit: 40,
order: "",
},
});
} else if (args.args[1].display_name === "test2") {
assert.deepEqual(args.args[3], {
display_name: {},
turtles: {
fields: {
display_name: {},
partner_ids: {
fields: {
display_name: {},
},
limit: 40,
order: "",
},
turtle_foo: {},
},
limit: 40,
order: "",
},
});
return {
value: {
turtles: [
[
1,
2,
{
display_name: "yop",
partner_ids: [[1, 2, { display_name: "plop" }]],
},
],
],
},
};
}
}
},
});
await editInput(target, "div[name='display_name'] input", "test");
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.strictEqual(
target.querySelector(".modal [name='display_name'] input").value,
"donatello"
);
assert.deepEqual(
[...target.querySelectorAll(".modal .o_data_row")].map((el) => el.textContent),
["second record", "aaa"]
);
await click(target.querySelector(".modal .o_form_button_save"));
await editInput(target, "div[name='display_name'] input", "test2");
await click(target.querySelector(".o_data_row .o_data_cell"));
assert.strictEqual(
target.querySelector(".modal [name='display_name'] input").value,
"yop"
);
assert.deepEqual(
[...target.querySelectorAll(".modal .o_data_row")].map((el) => el.textContent),
["plop", "aaa"]
);
}
);
QUnit.test("sorting one2many fields", async function (assert) {
serverData.models.partner.fields.foo.sortable = true;
serverData.models.partner.records.push({ id: 23, foo: "abc", int_field: 1 });