[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:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user