[FIX] web: Record: complete relational values

Now that the RelationalModel is based on unity, datapoints expect
to receive their data as returned by unity read (i.e. x2many values
are list of records, not ids, and many2one values are objects with
keys id and display_name, not tuple, or simple ids). As a
consequence, the Record component needed to be slightly adapted for
current usecases to continue working (e.g. using a Record component
for a Many2ManyTags field, and providing the value of the many2many
as a list of ids, like in the studio action editor sidebar).

This commit completes the relational values received by the Record
component with the related records, like if they were read by
unity.

The Record component's API will be reworked soon, now that the
new RelationalModel has been merged, to be more compliant with the
new unity concept.

Part of~3179751

Part-of: odoo/odoo#114024
This commit is contained in:
Aaron Bohy
2023-07-24 20:17:57 +02:00
parent 0994e0ff6d
commit 65c8f41cf3
2 changed files with 237 additions and 3 deletions
+74 -3
View File
@@ -3,6 +3,7 @@
import { useService } from "@web/core/utils/hooks";
import { pick } from "@web/core/utils/objects";
import { RelationalModel } from "@web/model/relational_model/relational_model";
import { getFieldsSpec } from "@web/model/relational_model/utils";
import { Component, xml, onWillStart, onWillUpdateProps, useState } from "@odoo/owl";
const defaultActiveField = { attrs: {}, options: {}, domain: "[]", string: "" };
@@ -23,13 +24,15 @@ class StandaloneRelationalModel extends RelationalModel {
class _Record extends Component {
setup() {
this.orm = useService("orm");
const resModel = this.props.info.resModel;
const activeFields = this.getActiveFields();
const modelParams = {
config: {
resModel,
fields: this.props.fields,
isMonoRecord: true,
activeFields: this.getActiveFields(),
activeFields,
resId: this.props.info.resId,
mode: this.props.info.mode,
},
@@ -44,11 +47,79 @@ class _Record extends Component {
return [servName, useService(servName)];
})
);
modelServices.orm = useService("orm");
modelServices.orm = this.orm;
this.model = useState(new StandaloneRelationalModel(this.env, modelParams, modelServices));
const loadWithValues = (values) => {
const loadWithValues = async (values) => {
values = pick(values, ...Object.keys(modelParams.config.activeFields));
const proms = [];
for (const fieldName in values) {
if (["one2many", "many2many"].includes(this.props.fields[fieldName].type)) {
if (values[fieldName].length && typeof values[fieldName][0] === "number") {
const resModel = this.props.fields[fieldName].relation;
const resIds = values[fieldName];
const activeField = modelParams.config.activeFields[fieldName];
if (activeField.related) {
const { activeFields, fields } = activeField.related;
const fieldSpec = getFieldsSpec(activeFields, fields, {});
const kwargs = {
context: activeField.context || {},
specification: fieldSpec,
};
proms.push(
this.orm
.call(resModel, "web_read", [resIds], kwargs)
.then((records) => {
values[fieldName] = records;
})
);
}
}
}
if (this.props.fields[fieldName].type === "many2one") {
const loadDisplayName = async (resId) => {
const resModel = this.props.fields[fieldName].relation;
const activeField = modelParams.config.activeFields[fieldName];
const kwargs = {
context: activeField.context || {},
specification: { display_name: {} },
};
const records = await this.orm.call(
resModel,
"web_read",
[[resId]],
kwargs
);
return records[0].display_name;
};
if (typeof values[fieldName] === "number") {
const prom = loadDisplayName(values[fieldName]);
prom.then((displayName) => {
values[fieldName] = {
id: values[fieldName],
display_name: displayName,
};
});
proms.push(prom);
} else if (Array.isArray(values[fieldName])) {
if (values[fieldName][1] === undefined) {
const prom = loadDisplayName(values[fieldName][0]);
prom.then((displayName) => {
values[fieldName] = {
id: values[fieldName][0],
display_name: displayName,
};
});
proms.push(prom);
}
values[fieldName] = {
id: values[fieldName][0],
display_name: values[fieldName][1],
};
}
}
await Promise.all(proms);
}
return this.model.load({ values });
};
onWillStart(() => {
@@ -3,6 +3,7 @@
import { browser } from "@web/core/browser/browser";
import { Field } from "@web/views/fields/field";
import { Many2OneField } from "@web/views/fields/many2one/many2one_field";
import { Many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field";
import { Record } from "@web/views/record";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
@@ -10,6 +11,7 @@ import {
click,
editInput,
getFixture,
getNodesTextContent,
mount,
nextTick,
patchWithCleanup,
@@ -335,6 +337,167 @@ QUnit.module("Record Component", (hooks) => {
assert.strictEqual(target.querySelector(".o_field_many2one_selection input").value, "abc");
});
QUnit.test("handles many2one fields (2)", async function (assert) {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
serverData.models = {
bar: {
records: [
{ id: 1, display_name: "bar1" },
{ id: 3, display_name: "abc" },
],
},
};
class Parent extends Component {
setup() {
this.fields = {
foo: {
name: "foo",
type: "many2one",
relation: "bar",
},
};
this.values = {
foo: 1,
};
}
onRecordChanged(record, changes) {
assert.step("record changed");
assert.deepEqual(changes, { foo: 3 });
assert.deepEqual(record.data, { foo: [3, "abc"] });
}
}
Parent.components = { Record, Many2OneField };
Parent.template = xml`
<Record resModel="'partner'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data">
<Many2OneField name="'foo'" record="data.record" relation="'bar'" value="data.record.data.foo"/>
</Record>
`;
await mount(Parent, target, {
env: await makeTestEnv({
serverData,
mockRPC(route, args) {
assert.step(route);
},
}),
});
assert.verifySteps(["/web/dataset/call_kw/bar/web_read"]);
assert.strictEqual(target.querySelector(".o_field_many2one_selection input").value, "bar1");
});
QUnit.test("handles many2one fields (3)", async function (assert) {
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
serverData.models = {
bar: {
records: [
{ id: 1, display_name: "bar1" },
{ id: 3, display_name: "abc" },
],
},
};
class Parent extends Component {
setup() {
this.fields = {
foo: {
name: "foo",
type: "many2one",
relation: "bar",
},
};
this.values = {
foo: [1],
};
}
onRecordChanged(record, changes) {
assert.step("record changed");
assert.deepEqual(changes, { foo: 3 });
assert.deepEqual(record.data, { foo: [3, "abc"] });
}
}
Parent.components = { Record, Many2OneField };
Parent.template = xml`
<Record resModel="'partner'" fieldNames="['foo']" fields="fields" values="values" t-slot-scope="data">
<Many2OneField name="'foo'" record="data.record" relation="'bar'" value="data.record.data.foo"/>
</Record>
`;
await mount(Parent, target, {
env: await makeTestEnv({
serverData,
mockRPC(route, args) {
assert.step(route);
},
}),
});
assert.verifySteps(["/web/dataset/call_kw/bar/web_read"]);
assert.strictEqual(target.querySelector(".o_field_many2one_selection input").value, "bar1");
});
QUnit.test("handles x2many fields", async function (assert) {
serverData.models = {
tag: {
records: [
{ id: 1, display_name: "bug" },
{ id: 3, display_name: "ref" },
],
},
};
class Parent extends Component {
setup() {
this.activeFields = {
tags: {
related: {
activeFields: {
display_name: {},
},
fields: {
display_name: { name: "display_name", type: "string" },
},
},
},
};
this.fields = {
tags: {
name: "Tags",
type: "many2many",
relation: "tag",
},
};
this.values = {
tags: [1, 3],
};
}
}
Parent.components = { Record, Many2ManyTagsField };
Parent.template = xml`
<Record resModel="'partner'" fieldNames="['tags']" activeFields="activeFields" fields="fields" values="values" t-slot-scope="data">
<Many2ManyTagsField name="'tags'" record="data.record"/>
</Record>
`;
await mount(Parent, target, {
env: await makeTestEnv({
serverData,
mockRPC(route, args) {
assert.step(route);
},
}),
});
assert.verifySteps(["/web/dataset/call_kw/tag/web_read"]);
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_tag")), ["bug", "ref"]);
});
QUnit.test(
"supports passing dynamic values -- full control to the user of Record",
async (assert) => {