[REF] web,*: simplify arch manipulation and parsing logic

*board,mail,project

This commit aims to simplify the XMLParser logic and the way archs
are manipulated in views, motivated by [1] where we had to
modify the arch in View to insert access right information.

First, the xml utils have been reworked. The XMLParser class has
been removed. The xml utils module already exported a parseXML and
a serializeXML functions, this commit adds visitXML, s.t. the whole
XMLParser feature is fully replaced by the 3 functions.

Second, concrete views now receive the arch in props as an
XMLDocument, as the arch is parsed once for all in View. With this,
we were able to remove serializing/parsing back and forth at several
places, where we needed to extract information for sub-parts of an
arch individually (e.g. View, x2many subviews, list view groupby).

Third, even though this change has been driven by the one above and
wasn't initally wanted, the view compiler cache and API have been
simplified. The cache is now flat, there's an entry in the cache for
each template that has been compiled. Moreover, the useViewCompiler
hook no longer takes the cache key in params, as it can directly
compute it itself (the key being the outerHTML of the template).

[1] odoo/odoo#135145

closes odoo/odoo#136376

Related: odoo/enterprise#47808
Signed-off-by: Pierre Rousseau (pro) <pro@odoo.com>
This commit is contained in:
Aaron Bohy
2023-10-04 08:39:12 +00:00
parent 344896ab04
commit cc3a3a328d
25 changed files with 146 additions and 171 deletions
+4 -4
View File
@@ -3,9 +3,9 @@
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { BoardController } from "./board_controller";
import { XMLParser } from "@web/core/utils/xml";
import { visitXML } from "@web/core/utils/xml";
import { Domain } from "@web/core/domain";
export class BoardArchParser extends XMLParser {
export class BoardArchParser {
parse(arch, customViewId) {
let nextId = 1;
const archInfo = {
@@ -18,7 +18,7 @@ export class BoardArchParser extends XMLParser {
};
let currentIndex = -1;
this.visitXML(arch, (node) => {
visitXML(arch, (node) => {
switch (node.tagName) {
case "form":
archInfo.title = node.getAttribute("string");
@@ -44,7 +44,7 @@ export class BoardArchParser extends XMLParser {
isFolded,
};
if (node.hasAttribute("domain")) {
let domain = node.getAttribute("domain");
const domain = node.getAttribute("domain");
action.domain = new Domain(domain).toList();
// so it can be serialized when reexporting board xml
action.domain.toString = () => node.getAttribute("domain");
@@ -1,11 +1,10 @@
/* @odoo-module */
import { XMLParser } from "@web/core/utils/xml";
import { visitXML } from "@web/core/utils/xml";
import { Field } from "@web/views/fields/field";
export class ActivityArchParser extends XMLParser {
parse(arch, models, modelName) {
const xmlDoc = this.parseXML(arch);
export class ActivityArchParser {
parse(xmlDoc, models, modelName) {
const jsClass = xmlDoc.getAttribute("js_class");
const title = xmlDoc.getAttribute("string");
@@ -13,7 +12,7 @@ export class ActivityArchParser extends XMLParser {
const templateDocs = {};
const fieldNextIds = {};
this.visitXML(xmlDoc, (node) => {
visitXML(xmlDoc, (node) => {
if (node.hasAttribute("t-name")) {
templateDocs[node.getAttribute("t-name")] = node;
return;
@@ -48,11 +47,9 @@ export class ActivityArchParser extends XMLParser {
}
});
return {
arch,
fieldNodes,
templateDocs,
title,
__rawArch: arch,
};
}
}
@@ -33,8 +33,9 @@ export class ActivityRecord extends Component {
editable: false,
isHtmlEmpty,
};
const { arch, templateDocs } = this.props.archInfo;
this.recordTemplate = useViewCompiler(ActivityCompiler, arch, templateDocs)["activity-box"];
const { templateDocs } = this.props.archInfo;
const templates = useViewCompiler(ActivityCompiler, templateDocs);
this.recordTemplate = templates["activity-box"];
}
getRenderingContext() {
@@ -11,13 +11,13 @@ export class ProjectSharingFormController extends FormController {
setup() {
super.setup();
this.uiService = useService('ui');
const { arch, xmlDoc } = this.archInfo;
const { xmlDoc } = this.archInfo;
const template = createElement('t');
const xmlDocChatter = xmlDoc.querySelector("div.oe_chatter");
if (xmlDocChatter && xmlDocChatter.parentNode.nodeName === "form") {
template.appendChild(xmlDocChatter.cloneNode(true));
}
const mailTemplates = useViewCompiler(ProjectSharingChatterCompiler, arch, { Mail: template }, {});
const mailTemplates = useViewCompiler(ProjectSharingChatterCompiler, { Mail: template });
this.mailTemplate = mailTemplates.Mail;
}
+20 -30
View File
@@ -34,38 +34,28 @@ export function serializeXML(xml) {
return serializer.serializeToString(xml);
}
export class XMLParser {
/**
* @param {Element | string} xml
* @param {(el: Element, visitChildren: () => any) => any} callback
*/
visitXML(xml, callback) {
const visit = (el) => {
if (el) {
let didVisitChildren = false;
const visitChildren = () => {
for (const child of el.children) {
visit(child);
}
didVisitChildren = true;
};
const shouldVisitChildren = callback(el, visitChildren);
if (shouldVisitChildren !== false && !didVisitChildren) {
visitChildren();
/**
* @param {Element | string} xml
* @param {(el: Element, visitChildren: () => any) => any} callback
*/
export function visitXML(xml, callback) {
const visit = (el) => {
if (el) {
let didVisitChildren = false;
const visitChildren = () => {
for (const child of el.children) {
visit(child);
}
didVisitChildren = true;
};
const shouldVisitChildren = callback(el, visitChildren);
if (shouldVisitChildren !== false && !didVisitChildren) {
visitChildren();
}
};
const xmlDoc = typeof xml === "string" ? this.parseXML(xml) : xml;
visit(xmlDoc);
}
/**
* @param {string} str
* @returns {Element}
*/
parseXML(str) {
return parseXML(str);
}
}
};
const xmlDoc = typeof xml === "string" ? parseXML(xml) : xml;
visit(xmlDoc);
}
/**
@@ -3,7 +3,7 @@
import { makeContext } from "@web/core/context";
import { _t } from "@web/core/l10n/translation";
import { evaluateExpr, evaluateBooleanExpr } from "@web/core/py_js/py";
import { XMLParser } from "@web/core/utils/xml";
import { visitXML } from "@web/core/utils/xml";
import { DEFAULT_INTERVAL, DEFAULT_PERIOD } from "@web/search/utils/dates";
const ALL = _t("All");
@@ -35,10 +35,8 @@ function reduceType(type) {
return type;
}
export class SearchArchParser extends XMLParser {
export class SearchArchParser {
constructor(searchViewDescription, fields, searchDefaults = {}, searchPanelDefaults = {}) {
super();
const { irFilters, arch } = searchViewDescription;
this.fields = fields || {};
@@ -63,7 +61,7 @@ export class SearchArchParser extends XMLParser {
}
parse() {
this.visitXML(this.arch, (node, visitChildren) => {
visitXML(this.arch, (node, visitChildren) => {
switch (node.tagName) {
case "search":
this.visitSearch(node, visitChildren);
@@ -159,9 +157,10 @@ export class SearchArchParser extends XMLParser {
} else if (fieldType === "many2one") {
this.labels.push((orm) => {
return orm
.call(relation, "read", [value, ['display_name']], { context })
.call(relation, "read", [value, ["display_name"]], { context })
.then((results) => {
preField.defaultAutocompleteValue.label = results[0]['display_name'];
preField.defaultAutocompleteValue.label =
results[0]["display_name"];
});
});
}
@@ -288,7 +287,10 @@ export class SearchArchParser extends XMLParser {
if (node.nodeType !== 1 || node.tagName !== "field") {
continue;
}
if (node.getAttribute("invisible") === "True" || node.getAttribute("invisible") === "1") {
if (
node.getAttribute("invisible") === "True" ||
node.getAttribute("invisible") === "1"
) {
continue;
}
const attrs = {};
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { evaluateExpr } from "@web/core/py_js/py";
import { XMLParser } from "@web/core/utils/xml";
import { visitXML } from "@web/core/utils/xml";
import { Field } from "@web/views/fields/field";
import { archParseBoolean } from "@web/views/utils";
@@ -18,7 +18,7 @@ const SCALES = ["day", "week", "month", "year"];
export class CalendarParseArchError extends Error {}
export class CalendarArchParser extends XMLParser {
export class CalendarArchParser {
parse(arch, models, modelName) {
const fields = models[modelName];
const fieldNames = new Set(fields.display_name ? ["display_name"] : []);
@@ -38,7 +38,7 @@ export class CalendarArchParser extends XMLParser {
const popoverFieldNodes = {};
const filtersInfo = {};
this.visitXML(arch, (node) => {
visitXML(arch, (node) => {
switch (node.tagName) {
case "calendar": {
if (!node.hasAttribute("date_start")) {
+1 -3
View File
@@ -326,9 +326,7 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) {
for (const child of node.children) {
const viewType = child.tagName === "tree" ? "list" : child.tagName;
const { ArchParser } = viewRegistry.get(viewType);
const xmlSerializer = new XMLSerializer();
const subArch = xmlSerializer.serializeToString(child);
const archInfo = new ArchParser().parse(subArch, models, fields[name].relation);
const archInfo = new ArchParser().parse(child, models, fields[name].relation);
views[viewType] = {
...archInfo,
limit: archInfo.limit || 40,
@@ -14,7 +14,7 @@ import {
useOwnedDialogs,
useService,
} from "@web/core/utils/hooks";
import { createElement } from "@web/core/utils/xml";
import { createElement, parseXML } from "@web/core/utils/xml";
import { FormArchParser } from "@web/views/form/form_arch_parser";
import { loadSubViews } from "@web/views/form/form_controller";
import { FormRenderer } from "@web/views/form/form_renderer";
@@ -652,7 +652,8 @@ async function getFormViewInfo({ list, activeField, viewService, userService, en
resModel: comodel,
views: [[false, "form"]],
});
formArchInfo = new FormArchParser().parse(views.form.arch, relatedModels, comodel);
const xmlDoc = parseXML(views.form.arch);
formArchInfo = new FormArchParser().parse(xmlDoc, relatedModels, comodel);
// Fields that need to be defined are the ones in the form view, this is natural,
// plus the ones that the list record has, that is, present in either the list arch
// or the kanban arch of the one2many field
@@ -1,13 +1,12 @@
/** @odoo-module **/
import { XMLParser } from "@web/core/utils/xml";
import { visitXML } from "@web/core/utils/xml";
import { Field } from "@web/views/fields/field";
import { Widget } from "@web/views/widgets/widget";
import { archParseBoolean, getActiveActions } from "@web/views/utils";
export class FormArchParser extends XMLParser {
parse(arch, models, modelName) {
const xmlDoc = this.parseXML(arch);
export class FormArchParser {
parse(xmlDoc, models, modelName) {
const jsClass = xmlDoc.getAttribute("js_class");
const disableAutofocus = archParseBoolean(xmlDoc.getAttribute("disable_autofocus") || "");
const activeActions = getActiveActions(xmlDoc);
@@ -16,7 +15,7 @@ export class FormArchParser extends XMLParser {
let widgetNextId = 0;
const fieldNextIds = {};
let autofocusFieldId = null;
this.visitXML(xmlDoc, (node) => {
visitXML(xmlDoc, (node) => {
if (node.tagName === "field") {
const fieldInfo = Field.parseFieldNode(node, models, modelName, "form", jsClass);
if (!(fieldInfo.name in fieldNextIds)) {
@@ -40,14 +39,12 @@ export class FormArchParser extends XMLParser {
}
});
return {
arch,
activeActions,
autofocusFieldId,
disableAutofocus,
fieldNodes,
widgetNodes,
xmlDoc,
__rawArch: arch,
};
}
}
@@ -9,7 +9,7 @@ import { registry } from "@web/core/registry";
import { SIZES } from "@web/core/ui/ui_service";
import { useBus, useService } from "@web/core/utils/hooks";
import { omit } from "@web/core/utils/objects";
import { createElement } from "@web/core/utils/xml";
import { createElement, parseXML } from "@web/core/utils/xml";
import { evaluateBooleanExpr } from "@web/core/py_js/py";
import { Layout } from "@web/search/layout";
import { usePager } from "@web/search/pager_hook";
@@ -97,7 +97,8 @@ export async function loadSubViews(
context: makeContext([fieldContext, userService.context, refinedContext]),
});
const { ArchParser } = viewRegistry.get(viewType);
const archInfo = new ArchParser().parse(views[viewType].arch, relatedModels, comodel);
const xmlDoc = parseXML(views[viewType].arch);
const archInfo = new ArchParser().parse(xmlDoc, relatedModels, comodel);
fieldInfo.views[viewType] = {
...archInfo,
limit: archInfo.limit || 40,
@@ -178,7 +179,6 @@ export class FormController extends Component {
if (xmlDocButtonBox) {
const buttonBoxTemplates = useViewCompiler(
this.props.Compiler || FormCompiler,
xmlDocButtonBox.outerHTML,
{ ButtonBox: xmlDocButtonBox },
{ isSubView: true }
);
@@ -32,10 +32,9 @@ export class FormRenderer extends Component {
setup() {
this.evaluateBooleanExpr = evaluateBooleanExpr;
const { archInfo, Compiler, record } = this.props;
const { arch, xmlDoc } = archInfo;
const templates = { FormRenderer: xmlDoc };
const templates = { FormRenderer: archInfo.xmlDoc };
this.state = useState({}); // Used by Form Compiler
this.templates = useViewCompiler(Compiler || FormCompiler, arch, templates);
this.templates = useViewCompiler(Compiler || FormCompiler, templates);
useSubEnv({ model: record.model });
useBounceButton(useRef("compiled_view_root"), (target) => {
return !record.isInEdition && !!target.closest(".oe_title, .o_inner_group");
@@ -1,16 +1,16 @@
/** @odoo-module **/
import { XMLParser } from "@web/core/utils/xml";
import { visitXML } from "@web/core/utils/xml";
import { GROUPABLE_TYPES } from "@web/search/utils/misc";
import { archParseBoolean } from "@web/views/utils";
const MODES = ["bar", "line", "pie"];
const ORDERS = ["ASC", "DESC", "asc", "desc", null];
export class GraphArchParser extends XMLParser {
export class GraphArchParser {
parse(arch, fields = {}) {
const archInfo = { fields, fieldAttrs: {}, groupBy: [] };
this.visitXML(arch, (node) => {
visitXML(arch, (node) => {
switch (node.tagName) {
case "graph": {
if (node.hasAttribute("disable_linking")) {
@@ -55,7 +55,10 @@ export class GraphArchParser extends XMLParser {
}
archInfo.fieldAttrs[fieldName].string = string;
}
if (node.getAttribute("invisible") === "True" || node.getAttribute("invisible") === "1") {
if (
node.getAttribute("invisible") === "True" ||
node.getAttribute("invisible") === "1"
) {
if (!archInfo.fieldAttrs[fieldName]) {
archInfo.fieldAttrs[fieldName] = {};
}
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { extractAttributes, XMLParser } from "@web/core/utils/xml";
import { extractAttributes, visitXML } from "@web/core/utils/xml";
import { stringToOrderBy } from "@web/search/utils/order_by";
import { Field } from "@web/views/fields/field";
import { Widget } from "@web/views/widgets/widget";
@@ -24,10 +24,9 @@ export const KANBAN_BOX_ATTRIBUTE = "kanban-box";
export const KANBAN_MENU_ATTRIBUTE = "kanban-menu";
export const KANBAN_TOOLTIP_ATTRIBUTE = "kanban-tooltip";
export class KanbanArchParser extends XMLParser {
parse(arch, models, modelName) {
export class KanbanArchParser {
parse(xmlDoc, models, modelName) {
const fields = models[modelName];
const xmlDoc = this.parseXML(arch);
const className = xmlDoc.getAttribute("class") || null;
let defaultOrder = stringToOrderBy(xmlDoc.getAttribute("default_order") || null);
const defaultGroupBy = xmlDoc.getAttribute("default_group_by");
@@ -60,7 +59,7 @@ export class KanbanArchParser extends XMLParser {
const creates = [];
let button_id = 0;
// Root level of the template
this.visitXML(xmlDoc, (node) => {
visitXML(xmlDoc, (node) => {
if (node.hasAttribute("t-name")) {
templateDocs[node.getAttribute("t-name")] = node;
return;
@@ -164,7 +163,6 @@ export class KanbanArchParser extends XMLParser {
}
return {
arch,
activeActions,
className,
creates,
@@ -188,7 +186,6 @@ export class KanbanArchParser extends XMLParser {
tooltipInfo,
examples: xmlDoc.getAttribute("examples"),
xmlDoc,
__rawArch: arch,
};
}
@@ -186,11 +186,10 @@ export class KanbanRecord extends Component {
this.notification = useService("notification");
this.user = useService("user");
const { archInfo, Compiler, templates } = this.props;
const { arch } = archInfo;
const { Compiler, templates } = this.props;
const ViewCompiler = Compiler || this.constructor.Compiler;
this.templates = useViewCompiler(ViewCompiler, arch, templates);
this.templates = useViewCompiler(ViewCompiler, templates);
if (this.constructor.KANBAN_MENU_ATTRIBUTE in templates) {
this.showMenu = true;
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { parseXML } from "@web/core/utils/xml";
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { useOwnedDialogs, useService } from "@web/core/utils/hooks";
@@ -251,7 +252,7 @@ export class KanbanRecordQuickCreate extends Component {
[props.group.resModel]: quickCreateFields,
};
const archInfo = new formView.ArchParser().parse(
quickCreateForm.arch,
parseXML(quickCreateForm.arch),
models,
props.group.resModel
);
@@ -1,20 +1,20 @@
/** @odoo-module */
import { Field } from "@web/views/fields/field";
import { XMLParser } from "@web/core/utils/xml";
import { visitXML } from "@web/core/utils/xml";
import { stringToOrderBy } from "@web/search/utils/order_by";
import { archParseBoolean, getActiveActions, getDecoration, processButton } from "@web/views/utils";
import { encodeObjectForTemplate } from "@web/views/view_compiler";
import { combineModifiers } from "@web/model/relational_model/utils";
import { Widget } from "@web/views/widgets/widget";
export class GroupListArchParser extends XMLParser {
export class GroupListArchParser {
parse(arch, models, modelName, jsClass) {
const fieldNodes = {};
const fieldNextIds = {};
const buttons = [];
let buttonId = 0;
this.visitXML(arch, (node) => {
visitXML(arch, (node) => {
if (node.tagName === "button") {
buttons.push({
...processButton(node),
@@ -36,7 +36,7 @@ export class GroupListArchParser extends XMLParser {
}
}
export class ListArchParser extends XMLParser {
export class ListArchParser {
parseFieldNode(node, models, modelName) {
return Field.parseFieldNode(node, models, modelName, "list");
}
@@ -49,8 +49,7 @@ export class ListArchParser extends XMLParser {
return processButton(node);
}
parse(arch, models, modelName) {
const xmlDoc = this.parseXML(arch);
parse(xmlDoc, models, modelName) {
const fieldNodes = {};
const widgetNodes = {};
let widgetNextId = 0;
@@ -69,7 +68,7 @@ export class ListArchParser extends XMLParser {
const treeAttr = {};
let nextId = 0;
const fieldNextIds = {};
this.visitXML(arch, (node) => {
visitXML(xmlDoc, (node) => {
if (node.tagName !== "button") {
buttonGroup = undefined;
}
@@ -82,14 +81,18 @@ export class ListArchParser extends XMLParser {
};
if (buttonGroup) {
buttonGroup.buttons.push(button);
buttonGroup.column_invisible = combineModifiers(buttonGroup.column_invisible, node.getAttribute('column_invisible'), "AND");
buttonGroup.column_invisible = combineModifiers(
buttonGroup.column_invisible,
node.getAttribute("column_invisible"),
"AND"
);
} else {
buttonGroup = {
id: `column_${nextId++}`,
type: "button_group",
buttons: [button],
hasLabel: false,
column_invisible: node.getAttribute('column_invisible'),
column_invisible: node.getAttribute("column_invisible"),
};
columns.push(buttonGroup);
}
@@ -138,10 +141,8 @@ export class ListArchParser extends XMLParser {
});
} else if (node.tagName === "groupby" && node.getAttribute("name")) {
const fieldName = node.getAttribute("name");
const xmlSerializer = new XMLSerializer();
const groupByArch = xmlSerializer.serializeToString(node);
const coModelName = fields[fieldName].relation;
const groupByArchInfo = groupListArchParser.parse(groupByArch, models, coModelName);
const groupByArchInfo = groupListArchParser.parse(node, models, coModelName);
groupBy.buttons[fieldName] = groupByArchInfo.buttons;
groupBy.fields[fieldName] = {
fieldNodes: groupByArchInfo.fieldNodes,
@@ -152,12 +153,11 @@ export class ListArchParser extends XMLParser {
// AAB: not sure we need to handle invisible="True" button as the usecase seems way
// less relevant than for fields (so for buttons, relying on the modifiers logic
// that applies later on could be enough, even if the value is always true)
headerButtons = [...node.children]
.map((node) => ({
...this.processButton(node),
type: "button",
id: buttonId++,
}));
headerButtons = [...node.children].map((node) => ({
...this.processButton(node),
type: "button",
id: buttonId++,
}));
return false;
} else if (node.tagName === "control") {
for (const childNode of node.children) {
@@ -229,7 +229,6 @@ export class ListArchParser extends XMLParser {
columns,
groupBy,
xmlDoc,
__rawArch: arch,
...treeAttr,
};
}
@@ -1,9 +1,9 @@
/** @odoo-module **/
import { XMLParser } from "@web/core/utils/xml";
import { visitXML } from "@web/core/utils/xml";
import { archParseBoolean } from "@web/views/utils";
export class PivotArchParser extends XMLParser {
export class PivotArchParser {
parse(arch) {
const archInfo = {
activeMeasures: [], // store the defined active measures
@@ -14,7 +14,7 @@ export class PivotArchParser extends XMLParser {
widgets: {}, // wigdets defined in the arch
};
this.visitXML(arch, (node) => {
visitXML(arch, (node) => {
switch (node.tagName) {
case "pivot": {
if (node.hasAttribute("disable_linking")) {
@@ -42,7 +42,10 @@ export class PivotArchParser extends XMLParser {
if (node.hasAttribute("string")) {
archInfo.fieldAttrs[fieldName].string = node.getAttribute("string");
}
if (node.getAttribute("invisible") === "True" || node.getAttribute("invisible") === "1") {
if (
node.getAttribute("invisible") === "True" ||
node.getAttribute("invisible") === "1"
) {
archInfo.fieldAttrs[fieldName].isInvisible = true;
break;
}
@@ -17,7 +17,7 @@ export const standardViewProps = {
type: Object,
},
resModel: String,
arch: { type: String },
arch: { type: Element },
bannerRoute: { type: String, optional: true },
className: { type: String, optional: true },
comparison: { type: [Object, { value: null }], optional: true },
+9 -10
View File
@@ -6,7 +6,7 @@ import { KeepLast } from "@web/core/utils/concurrency";
import { useService } from "@web/core/utils/hooks";
import { deepCopy, pick } from "@web/core/utils/objects";
import { nbsp } from "@web/core/utils/strings";
import { parseXML, serializeXML } from "@web/core/utils/xml";
import { parseXML } from "@web/core/utils/xml";
import { extractLayoutComponents } from "@web/search/layout";
import { WithSearch } from "@web/search/with_search/with_search";
import { OnboardingBanner } from "@web/views/onboarding_banner";
@@ -277,18 +277,17 @@ export class View extends Component {
actionMenus = viewDescription.actionMenus;
}
const rootNode = parseXML(arch.replace(/&amp;nbsp;/g, nbsp));
const archXmlDoc = parseXML(arch.replace(/&amp;nbsp;/g, nbsp));
for (const action of ACTIONS) {
if (action in this.props.context && !this.props.context[action]) {
rootNode.setAttribute(action, "0");
archXmlDoc.setAttribute(action, "0");
}
}
arch = serializeXML(rootNode);
let subType = rootNode.getAttribute("js_class");
const bannerRoute = rootNode.getAttribute("banner_route");
const sample = rootNode.getAttribute("sample");
const className = computeViewClassName(type, rootNode, [
let subType = archXmlDoc.getAttribute("js_class");
const bannerRoute = archXmlDoc.getAttribute("banner_route");
const sample = archXmlDoc.getAttribute("sample");
const className = computeViewClassName(type, archXmlDoc, [
"o_view_controller",
...(props.className || "").split(" "),
]);
@@ -303,7 +302,7 @@ export class View extends Component {
}
Object.assign(this.env.config, {
viewArch: rootNode,
viewArch: archXmlDoc,
viewId: viewDescription.id,
viewType: type,
viewSubType: subType,
@@ -323,7 +322,7 @@ export class View extends Component {
// prepare the view props
const viewProps = {
info,
arch,
arch: archXmlDoc,
fields,
relatedModels,
resModel,
+15 -15
View File
@@ -249,7 +249,8 @@ export class ViewCompiler {
* @returns {string}
*/
compile(key, params = {}) {
const child = this.compileNode(this.templates[key], params);
const root = this.templates[key].cloneNode(true);
const child = this.compileNode(root, params);
const newRoot = createElement("t", [child]);
return newRoot;
}
@@ -438,27 +439,26 @@ export class ViewCompiler {
}
ViewCompiler.OWL_DIRECTIVE_WHITELIST = [];
let templateIds = Object.create(null);
let templateCache = Object.create(null);
/**
* @param {typeof ViewCompiler} ViewCompiler
* @param {string} rawArch
* @param {string} key
* @param {Record<string, Element>} templates
* @param {Record<string, any>} [params]
* @returns {Record<string, string>}
*/
export function useViewCompiler(ViewCompiler, rawArch, templates, params) {
if (!templateIds[rawArch]) {
templateIds[rawArch] = {};
}
const compiledTemplates = templateIds[rawArch];
const compiler = new ViewCompiler(templates);
for (const key in templates) {
if (!compiledTemplates[key]) {
const compiledDoc = compiler.compile(key, params);
compiledTemplates[key] = xml`${compiledDoc.outerHTML}`;
export function useViewCompiler(ViewCompiler, templates, params) {
const compiledTemplates = {};
let compiler;
for (const tname in templates) {
const key = templates[tname].outerHTML;
if (!templateCache[key]) {
compiler = compiler || new ViewCompiler(templates);
templateCache[key] = xml`${compiler.compile(tname, params).outerHTML}`;
}
compiledTemplates[tname] = templateCache[key];
}
return { ...compiledTemplates };
return compiledTemplates;
}
/*
@@ -470,5 +470,5 @@ export function useViewCompiler(ViewCompiler, rawArch, templates, params) {
* This is how a memory leak occurs. :-)
*/
export function resetViewCompilerCache() {
templateIds = Object.create(null);
templateCache = Object.create(null);
}
@@ -1,16 +0,0 @@
/** @odoo-module */
import { evaluateExpr } from "@web/core/py_js/py";
import { formView } from "@web/views/form/form_view";
export class SettingsArchParser extends formView.ArchParser {
parseXML() {
const result = super.parseXML(...arguments);
Array.from(result.querySelectorAll("setting[type='header'] field")).forEach((el) => {
const options = evaluateExpr(el.getAttribute("options") || "{}");
options.isHeaderField = true;
el.setAttribute("options", JSON.stringify(options));
});
return result;
}
}
@@ -1,13 +1,13 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { evaluateExpr } from "@web/core/py_js/py";
import { intersection } from "@web/core/utils/arrays";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { formView } from "@web/views/form/form_view";
import { SettingsFormController } from "./settings_form_controller";
import { SettingsFormRenderer } from "./settings_form_renderer";
import { SettingsFormCompiler } from "./settings_form_compiler";
import { SettingsArchParser } from "./settings_form_arch_parser";
class SettingRecord extends formView.Model.Record {
_update(changes) {
@@ -32,7 +32,6 @@ class SettingModel extends formView.Model {
nextConfig.resId = false;
return nextConfig;
}
}
SettingModel.Record = SettingRecord;
@@ -40,12 +39,19 @@ export const settingsFormView = {
...formView,
display: {},
buttonTemplate: "web.SettingsFormView.Buttons",
ArchParser: SettingsArchParser,
Model: SettingModel,
ControlPanel: ControlPanel,
Controller: SettingsFormController,
Compiler: SettingsFormCompiler,
Renderer: SettingsFormRenderer,
props: (genericProps, view) => {
[...genericProps.arch.querySelectorAll("setting[type='header'] field")].forEach((el) => {
const options = evaluateExpr(el.getAttribute("options") || "{}");
options.isHeaderField = true;
el.setAttribute("options", JSON.stringify(options));
});
return formView.props(genericProps, view);
},
};
registry.category("views").add("base_settings", settingsFormView);
@@ -1,5 +1,5 @@
/** @odoo-module **/
import { XMLParser } from "@web/core/utils/xml";
import { parseXML } from "@web/core/utils/xml";
QUnit.module("utils", () => {
QUnit.module("xml");
@@ -7,10 +7,9 @@ QUnit.module("utils", () => {
QUnit.test("parse error throws an exception", async (assert) => {
assert.expect(3);
const parser = new XMLParser();
let XMLToParse = "<invalid'>";
try {
parser.parseXML(XMLToParse);
parseXML(XMLToParse);
assert.step("no error");
} catch (e) {
if (e.message.includes("error occured while parsing")) {
@@ -20,7 +19,7 @@ QUnit.module("utils", () => {
XMLToParse = "<div><div>Valid</div><div><Invalid</div></div>";
try {
parser.parseXML(XMLToParse);
parseXML(XMLToParse);
assert.step("no error");
} catch (e) {
if (e.message.includes("error occured while parsing")) {
+10 -10
View File
@@ -76,7 +76,7 @@ QUnit.module("Views", (hooks) => {
class ToyController extends Component {
setup() {
this.class = "toy";
this.template = xml`${this.props.arch}`;
this.template = xml`${this.props.arch.outerHTML}`;
}
}
ToyController.template = xml`<div t-attf-class="{{class}} {{props.className}}"><t t-call="{{ template }}"/></div>`;
@@ -125,7 +125,7 @@ QUnit.module("Views", (hooks) => {
setup() {
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, serverData.views["animal,false,toy"]);
assert.strictEqual(arch.outerHTML, serverData.views["animal,false,toy"]);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
assert.strictEqual(this.env.config.viewId, false);
@@ -163,7 +163,7 @@ QUnit.module("Views", (hooks) => {
setup() {
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, serverData.views["animal,1,toy"]);
assert.strictEqual(arch.outerHTML, serverData.views["animal,1,toy"]);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
assert.strictEqual(this.env.config.viewId, 1);
@@ -200,7 +200,7 @@ QUnit.module("Views", (hooks) => {
setup() {
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, serverData.views["animal,1,toy"]);
assert.strictEqual(arch.outerHTML, serverData.views["animal,1,toy"]);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
assert.strictEqual(this.env.config.viewId, 1);
@@ -241,7 +241,7 @@ QUnit.module("Views", (hooks) => {
setup() {
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, serverData.views["animal,false,toy"]);
assert.strictEqual(arch.outerHTML, serverData.views["animal,false,toy"]);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
assert.strictEqual(this.env.config.viewId, false);
@@ -284,7 +284,7 @@ QUnit.module("Views", (hooks) => {
setup() {
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, serverData.views["animal,1,toy"]);
assert.strictEqual(arch.outerHTML, serverData.views["animal,1,toy"]);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
assert.strictEqual(this.env.config.viewId, 1);
@@ -330,7 +330,7 @@ QUnit.module("Views", (hooks) => {
setup() {
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, `<toy>Specific arch content</toy>`);
assert.strictEqual(arch.outerHTML, `<toy>Specific arch content</toy>`);
assert.deepEqual(fields, {});
assert.strictEqual(info.actionMenus, undefined);
assert.strictEqual(this.env.config.viewId, undefined);
@@ -363,7 +363,7 @@ QUnit.module("Views", (hooks) => {
setup() {
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, serverData.views["animal,false,toy"]);
assert.strictEqual(arch.outerHTML, serverData.views["animal,false,toy"]);
assert.deepEqual(fields, {});
assert.deepEqual(info.actionMenus, {});
assert.strictEqual(this.env.config.viewId, false);
@@ -403,7 +403,7 @@ QUnit.module("Views", (hooks) => {
setup() {
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, `<toy>Specific arch content</toy>`);
assert.strictEqual(arch.outerHTML, `<toy>Specific arch content</toy>`);
assert.deepEqual(fields, {});
assert.deepEqual(info.actionMenus, {});
assert.strictEqual(this.env.config.viewId, false);
@@ -446,7 +446,7 @@ QUnit.module("Views", (hooks) => {
setup() {
super.setup();
const { arch, fields, info } = this.props;
assert.strictEqual(arch, `<toy>Specific arch content</toy>`);
assert.strictEqual(arch.outerHTML, `<toy>Specific arch content</toy>`);
assert.deepEqual(fields, {});
assert.deepEqual(info.actionMenus, {});
assert.strictEqual(this.env.config.viewId, undefined);