[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
This commit is contained in:
Mathieu Duckerts-Antoine
2022-07-22 16:21:44 +02:00
committed by Géry Debongnie
parent ce61a35cf8
commit 182b241e7d
9 changed files with 213 additions and 37 deletions
@@ -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);
}
@@ -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)
@@ -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.
@@ -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();
+16
View File
@@ -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
@@ -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 };
};
@@ -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`<span t-esc="props.record.data.int_field"/>`;
registry.category("fields").add("custom_field", CustomField);
await makeView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="foo" widget="custom_field"/>
</form>
`,
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`<span t-esc="props.record.data.product_id[1]"/>`;
registry.category("fields").add("custom_field", CustomField);
await makeView({
type: "form",
resModel: "partner",
resId: 1,
arch: `
<form>
<field name="foo" widget="custom_field"/>
</form>
`,
serverData,
mockRPC: (route, args) => {
assert.step(args.method);
},
});
assert.strictEqual(target.querySelector("[name=foo] span").innerText, "xphone");
assert.verifySteps(["get_views", "read"]);
}
);
});
@@ -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`<span t-esc="props.record.data.int_field"/>`;
registry.category("fields").add("custom_field", CustomField);
await makeView({
resModel: "partner",
type: "kanban",
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="foo" widget="custom_field"/>
</div>
</t>
</templates>
</kanban>
`,
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`<span t-esc="props.record.data.product_id[1]"/>`;
registry.category("fields").add("custom_field", CustomField);
await makeView({
resModel: "partner",
type: "kanban",
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="foo" widget="custom_field"/>
</div>
</t>
</templates>
</kanban>
`,
serverData,
mockRPC: (route, args) => {
assert.step(args.method);
},
});
assert.strictEqual(target.querySelector("[name=foo] span").innerText, "hello");
assert.verifySteps(["get_views", "web_search_read"]);
}
);
});
@@ -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`<span t-esc="props.record.data.int_field"/>`;
registry.category("fields").add("custom_field", CustomField);
await makeView({
resModel: "foo",
type: "list",
arch: `
<list>
<field name="foo" widget="custom_field"/>
</list>
`,
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`<span t-esc="props.record.data.m2o[0]"/>`;
registry.category("fields").add("custom_field", CustomField);
await makeView({
resModel: "foo",
type: "list",
arch: `
<list>
<field name="foo" widget="custom_field"/>
</list>
`,
serverData,
mockRPC: (route, args) => {
assert.step(args.method);
},
});
assert.strictEqual(target.querySelector("[name=foo] span").innerText, "1");
assert.verifySteps(["get_views", "web_search_read"]);
}
);
});