From 65c8f41cf31787d30675ffd44fc955dae0102ee6 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Thu, 20 Jul 2023 15:55:50 +0000 Subject: [PATCH] [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 --- addons/web/static/src/views/record.js | 77 ++++++++- addons/web/static/tests/views/record_tests.js | 163 ++++++++++++++++++ 2 files changed, 237 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/views/record.js b/addons/web/static/src/views/record.js index 454bbd8a8cc..99c74b46436 100644 --- a/addons/web/static/src/views/record.js +++ b/addons/web/static/src/views/record.js @@ -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(() => { diff --git a/addons/web/static/tests/views/record_tests.js b/addons/web/static/tests/views/record_tests.js index 9973739d14d..fa5450ef2ce 100644 --- a/addons/web/static/tests/views/record_tests.js +++ b/addons/web/static/tests/views/record_tests.js @@ -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` + + + + `; + + 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` + + + + `; + + 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` + + + + `; + + 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) => {