[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:
committed by
Géry Debongnie
parent
ce61a35cf8
commit
182b241e7d
@@ -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();
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user