From 182b241e7d42c56ac338a08ff92ad939d06bc327 Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Wed, 20 Jul 2022 09:52:37 +0000 Subject: [PATCH] [IMP] web: support of fieldDependencies Sometimes a field component might need to know some other fields values to render itself or for other purposes. In legacy, a mechanism allowing to load some field values not displayed was available: a field would declare a fieldDependencies object specifying some field names and their types and the related values would be loaded along the other data. Here we introduce the same mechanism for the new views. Note that in a new view list, the tag widget is not currently supported so that we have remove some lines in the list arch parser that were useless. Part-of: odoo/odoo#78221 --- addons/web/static/src/legacy/legacy_fields.js | 1 + .../static/src/views/form/form_arch_parser.js | 14 ++--- .../src/views/kanban/kanban_arch_parser.js | 18 +++--- .../static/src/views/list/list_arch_parser.js | 16 ++--- addons/web/static/src/views/utils.js | 16 +++++ addons/web/static/src/views/widgets/widget.js | 8 +-- .../tests/views/form/form_view_tests.js | 59 +++++++++++++++++ .../static/tests/views/kanban_view_tests.js | 63 +++++++++++++++++++ .../web/static/tests/views/list_view_tests.js | 55 +++++++++++++++- 9 files changed, 213 insertions(+), 37 deletions(-) diff --git a/addons/web/static/src/legacy/legacy_fields.js b/addons/web/static/src/legacy/legacy_fields.js index c0be503acef..aca270272b3 100644 --- a/addons/web/static/src/legacy/legacy_fields.js +++ b/addons/web/static/src/legacy/legacy_fields.js @@ -340,6 +340,7 @@ function registerField(name, LegacyFieldWidget) { LegacyField.template = legacyFieldTemplate; LegacyField.components = { FieldAdapter }; LegacyField.fieldsToFetch = LegacyFieldWidget.prototype.fieldsToFetch || {}; + LegacyField.fieldDependencies = LegacyFieldWidget.prototype.fieldDependencies || {}; if (!fieldRegistry.contains(name)) { fieldRegistry.add(name, LegacyField); } diff --git a/addons/web/static/src/views/form/form_arch_parser.js b/addons/web/static/src/views/form/form_arch_parser.js index 790995ef9e1..0ac621be33a 100644 --- a/addons/web/static/src/views/form/form_arch_parser.js +++ b/addons/web/static/src/views/form/form_arch_parser.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { XMLParser } from "@web/core/utils/xml"; +import { addFieldDependencies, archParseBoolean, getActiveActions } from "@web/views/utils"; import { Field } from "@web/views/fields/field"; -import { archParseBoolean, getActiveActions } from "@web/views/utils"; +import { XMLParser } from "@web/core/utils/xml"; import { Widget } from "@web/views/widgets/widget"; export class FormArchParser extends XMLParser { @@ -29,18 +29,14 @@ export class FormArchParser extends XMLParser { if (archParseBoolean(node.getAttribute("default_focus") || "")) { autofocusFieldId = fieldId; } + addFieldDependencies(activeFields, fieldInfo.FieldComponent.fieldDependencies); return false; } else if (node.tagName === "div" && node.classList.contains("oe_chatter")) { // remove this when chatter fields are declared as attributes on the root node return false; } else if (node.tagName === "widget") { - const widgetInfo = Widget.parseWidgetNode(node); - for (const [name, field] of Object.entries(widgetInfo.fieldDependencies)) { - activeFields[name] = { - name, - type: field.type, - }; - } + const { WidgetComponent } = Widget.parseWidgetNode(node); + addFieldDependencies(activeFields, WidgetComponent.fieldDependencies); } }); // TODO: generate activeFields for the model based on fieldNodes (merge duplicated fields) diff --git a/addons/web/static/src/views/kanban/kanban_arch_parser.js b/addons/web/static/src/views/kanban/kanban_arch_parser.js index 4d8fe0ec24d..f2b03778afb 100644 --- a/addons/web/static/src/views/kanban/kanban_arch_parser.js +++ b/addons/web/static/src/views/kanban/kanban_arch_parser.js @@ -1,8 +1,13 @@ /** @odoo-module **/ +import { + addFieldDependencies, + archParseBoolean, + getActiveActions, + stringToOrderBy, +} from "@web/views/utils"; import { extractAttributes, XMLParser } from "@web/core/utils/xml"; import { Field } from "@web/views/fields/field"; -import { archParseBoolean, getActiveActions, stringToOrderBy } from "@web/views/utils"; import { Widget } from "@web/views/widgets/widget"; /** @@ -52,7 +57,6 @@ export class KanbanArchParser extends XMLParser { const openAction = action && type ? { action, type } : null; const templateDocs = {}; const activeFields = {}; - // Root level of the template this.visitXML(xmlDoc, (node) => { if (node.hasAttribute("t-name")) { @@ -76,15 +80,11 @@ export class KanbanArchParser extends XMLParser { if (fieldInfo.widget === "handle") { handleField = name; } + addFieldDependencies(activeFields, fieldInfo.FieldComponent.fieldDependencies); } if (node.tagName === "widget") { - const widgetInfo = Widget.parseWidgetNode(node); - for (const [name, field] of Object.entries(widgetInfo.fieldDependencies)) { - activeFields[name] = { - name, - type: field.type, - }; - } + const { WidgetComponent } = Widget.parseWidgetNode(node); + addFieldDependencies(activeFields, WidgetComponent.fieldDependencies); } // Keep track of last update so images can be reloaded when they may have changed. diff --git a/addons/web/static/src/views/list/list_arch_parser.js b/addons/web/static/src/views/list/list_arch_parser.js index c1b4e22b978..96ea4d47783 100644 --- a/addons/web/static/src/views/list/list_arch_parser.js +++ b/addons/web/static/src/views/list/list_arch_parser.js @@ -1,15 +1,15 @@ /** @odoo-module */ -import { XMLParser } from "@web/core/utils/xml"; -import { Field } from "@web/views/fields/field"; import { + addFieldDependencies, archParseBoolean, getActiveActions, getDecoration, processButton, stringToOrderBy, } from "@web/views/utils"; -import { Widget } from "@web/views/widgets/widget"; +import { Field } from "@web/views/fields/field"; +import { XMLParser } from "@web/core/utils/xml"; export class GroupListArchParser extends XMLParser { parse(arch, models, modelName, jsClass) { @@ -84,6 +84,7 @@ export class ListArchParser extends XMLParser { if (fieldInfo.widget === "handle") { handleField = fieldInfo.name; } + addFieldDependencies(activeFields, fieldInfo.FieldComponent.fieldDependencies); if (!invisible || !archParseBoolean(invisible)) { const displayName = fieldInfo.FieldComponent.displayName; columns.push({ @@ -99,15 +100,6 @@ export class ListArchParser extends XMLParser { }); } return false; - } else if (node.tagName === "widget") { - const widgetInfo = Widget.parseWidgetNode(node); - for (const [name, field] of Object.entries(widgetInfo.fieldDependencies)) { - activeFields[name] = { - name, - type: field.type, - }; - } - return false; } else if (node.tagName === "groupby" && node.getAttribute("name")) { const fieldName = node.getAttribute("name"); const xmlSerializer = new XMLSerializer(); diff --git a/addons/web/static/src/views/utils.js b/addons/web/static/src/views/utils.js index 88b5bff104c..a1295c68205 100644 --- a/addons/web/static/src/views/utils.js +++ b/addons/web/static/src/views/utils.js @@ -7,6 +7,22 @@ export const X2M_TYPES = ["one2many", "many2many"]; const RELATIONAL_TYPES = [...X2M_TYPES, "many2one"]; const NUMERIC_TYPES = ["integer", "float", "monetary"]; +/** + * Add dependencies to activeFields + * + * @param {Object} activeFields + * @param {Object} [dependencies={}] + */ +export function addFieldDependencies(activeFields, dependencies = {}) { + for (const [name, dependency] of Object.entries(dependencies)) { + if (!(name in activeFields)) { + activeFields[name] = Object.assign({ name, rawAttrs: {} }, dependency, { + modifiers: { invisible: true }, + }); + } + } +} + /** * Parse the arch to check if is true or false * If the string is empty, 0, False or false it's considered as false diff --git a/addons/web/static/src/views/widgets/widget.js b/addons/web/static/src/views/widgets/widget.js index 361383d311c..58c1b3a0792 100644 --- a/addons/web/static/src/views/widgets/widget.js +++ b/addons/web/static/src/views/widgets/widget.js @@ -43,10 +43,6 @@ Widget.template = xml/*xml*/ ` Widget.parseWidgetNode = function (node) { const name = node.getAttribute("name"); - const component = findWidgetComponent(name); - return { - name, - component, - fieldDependencies: { ...component.fieldDependencies }, - }; + const WidgetComponent = findWidgetComponent(name); + return { WidgetComponent }; }; diff --git a/addons/web/static/tests/views/form/form_view_tests.js b/addons/web/static/tests/views/form/form_view_tests.js index 219f1694b4f..ca1e0500220 100644 --- a/addons/web/static/tests/views/form/form_view_tests.js +++ b/addons/web/static/tests/views/form/form_view_tests.js @@ -35,6 +35,8 @@ const fieldRegistry = registry.category("fields"); const serviceRegistry = registry.category("services"); const widgetRegistry = registry.category("view_widgets"); +const { Component, xml } = owl; + let target; let serverData; @@ -10962,4 +10964,61 @@ QUnit.module("Views", (hooks) => { await click(target.querySelector(".o_form_button_save")); // save the record } ); + + QUnit.test("fieldDependencies support for fields", async (assert) => { + serverData.models.partner.records = [{ id: 1, int_field: 2 }]; + + class CustomField extends Component {} + CustomField.fieldDependencies = { + int_field: { type: "integer" }, + }; + CustomField.template = xml``; + registry.category("fields").add("custom_field", CustomField); + + await makeView({ + type: "form", + resModel: "partner", + resId: 1, + arch: ` +
+ + + `, + serverData, + }); + + assert.strictEqual(target.querySelector("[name=foo] span").innerText, "2"); + }); + + QUnit.test( + "fieldDependencies support for fields: dependence on a relational field", + async (assert) => { + serverData.models.partner.records[0].product_id = 37; + + class CustomField extends Component {} + CustomField.fieldDependencies = { + product_id: { type: "many2one", relation: "product" }, + }; + CustomField.template = xml``; + registry.category("fields").add("custom_field", CustomField); + + await makeView({ + type: "form", + resModel: "partner", + resId: 1, + arch: ` +
+ + + `, + serverData, + mockRPC: (route, args) => { + assert.step(args.method); + }, + }); + + assert.strictEqual(target.querySelector("[name=foo] span").innerText, "xphone"); + assert.verifySteps(["get_views", "read"]); + } + ); }); diff --git a/addons/web/static/tests/views/kanban_view_tests.js b/addons/web/static/tests/views/kanban_view_tests.js index 69336ad13bf..67ced7bbc1c 100644 --- a/addons/web/static/tests/views/kanban_view_tests.js +++ b/addons/web/static/tests/views/kanban_view_tests.js @@ -10660,4 +10660,67 @@ QUnit.module("Views", (hooks) => { assert.verifySteps(["/mybody/isacage"]); assert.containsOnce(target, ".setmybodyfree"); }); + + QUnit.test("fieldDependencies support for fields", async (assert) => { + class CustomField extends Component {} + CustomField.fieldDependencies = { + int_field: { type: "integer" }, + }; + CustomField.template = xml``; + registry.category("fields").add("custom_field", CustomField); + + await makeView({ + resModel: "partner", + type: "kanban", + arch: ` + + + +
+ +
+
+
+
+ `, + serverData, + }); + + assert.strictEqual(target.querySelector("[name=foo] span").innerText, "10"); + }); + + QUnit.test( + "fieldDependencies support for fields: dependence on a relational field", + async (assert) => { + class CustomField extends Component {} + CustomField.fieldDependencies = { + product_id: { type: "many2one", relation: "product" }, + }; + CustomField.template = xml``; + registry.category("fields").add("custom_field", CustomField); + + await makeView({ + resModel: "partner", + type: "kanban", + arch: ` + + + +
+ +
+
+
+
+ `, + serverData, + mockRPC: (route, args) => { + assert.step(args.method); + }, + }); + + assert.strictEqual(target.querySelector("[name=foo] span").innerText, "hello"); + assert.verifySteps(["get_views", "web_search_read"]); + } + ); }); diff --git a/addons/web/static/tests/views/list_view_tests.js b/addons/web/static/tests/views/list_view_tests.js index dcaab1bd172..aaf3a715681 100644 --- a/addons/web/static/tests/views/list_view_tests.js +++ b/addons/web/static/tests/views/list_view_tests.js @@ -60,7 +60,7 @@ import { TextField } from "@web/views/fields/text/text_field"; import { DynamicRecordList } from "@web/views/relational_model"; import { registerCleanup } from "../helpers/cleanup"; -const { onWillStart } = owl; +const { Component, onWillStart, xml } = owl; const serviceRegistry = registry.category("services"); @@ -14162,4 +14162,57 @@ QUnit.module("Views", (hooks) => { assert.verifySteps(["/mybody/isacage"]); assert.containsOnce(target, ".setmybodyfree"); }); + + QUnit.test("fieldDependencies support for fields", async (assert) => { + serverData.models.foo.records = [{ id: 1, int_field: 2 }]; + + class CustomField extends Component {} + CustomField.fieldDependencies = { + int_field: { type: "integer" }, + }; + CustomField.template = xml``; + registry.category("fields").add("custom_field", CustomField); + + await makeView({ + resModel: "foo", + type: "list", + arch: ` + + + + `, + serverData, + }); + + assert.strictEqual(target.querySelector("[name=foo] span").innerText, "2"); + }); + + QUnit.test( + "fieldDependencies support for fields: dependence on a relational field", + async (assert) => { + class CustomField extends Component {} + CustomField.fieldDependencies = { + m2o: { type: "many2one", relation: "bar" }, + }; + CustomField.template = xml``; + registry.category("fields").add("custom_field", CustomField); + + await makeView({ + resModel: "foo", + type: "list", + arch: ` + + + + `, + serverData, + mockRPC: (route, args) => { + assert.step(args.method); + }, + }); + + assert.strictEqual(target.querySelector("[name=foo] span").innerText, "1"); + assert.verifySteps(["get_views", "web_search_read"]); + } + ); });