From cc3a3a328db913da76573404967e1190f40bcc98 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 3 Oct 2023 12:27:35 +0200 Subject: [PATCH] [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) --- addons/board/static/src/board_view.js | 8 +-- .../web/activity/activity_arch_parser.js | 11 ++-- .../src/views/web/activity/activity_record.js | 5 +- .../form/project_sharing_form_controller.js | 4 +- addons/web/static/src/core/utils/xml.js | 50 ++++++++----------- .../static/src/search/search_arch_parser.js | 18 ++++--- .../views/calendar/calendar_arch_parser.js | 6 +-- addons/web/static/src/views/fields/field.js | 4 +- .../src/views/fields/relational_utils.js | 5 +- .../static/src/views/form/form_arch_parser.js | 11 ++-- .../static/src/views/form/form_controller.js | 6 +-- .../static/src/views/form/form_renderer.js | 5 +- .../src/views/graph/graph_arch_parser.js | 11 ++-- .../src/views/kanban/kanban_arch_parser.js | 11 ++-- .../static/src/views/kanban/kanban_record.js | 5 +- .../kanban/kanban_record_quick_create.js | 3 +- .../static/src/views/list/list_arch_parser.js | 37 +++++++------- .../src/views/pivot/pivot_arch_parser.js | 11 ++-- .../static/src/views/standard_view_props.js | 2 +- addons/web/static/src/views/view.js | 19 ++++--- addons/web/static/src/views/view_compiler.js | 30 +++++------ .../settings_form_arch_parser.js | 16 ------ .../settings_form_view/settings_form_view.js | 12 +++-- .../web/static/tests/core/utils/xml_tests.js | 7 ++- addons/web/static/tests/views/view_tests.js | 20 ++++---- 25 files changed, 146 insertions(+), 171 deletions(-) delete mode 100644 addons/web/static/src/webclient/settings_form_view/settings_form_arch_parser.js diff --git a/addons/board/static/src/board_view.js b/addons/board/static/src/board_view.js index 480b53d501a..95b788f7f33 100644 --- a/addons/board/static/src/board_view.js +++ b/addons/board/static/src/board_view.js @@ -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"); diff --git a/addons/mail/static/src/views/web/activity/activity_arch_parser.js b/addons/mail/static/src/views/web/activity/activity_arch_parser.js index fdfda4023a6..ebedc296c4c 100644 --- a/addons/mail/static/src/views/web/activity/activity_arch_parser.js +++ b/addons/mail/static/src/views/web/activity/activity_arch_parser.js @@ -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, }; } } diff --git a/addons/mail/static/src/views/web/activity/activity_record.js b/addons/mail/static/src/views/web/activity/activity_record.js index 28e1d65c3ad..d9c8410b0fd 100644 --- a/addons/mail/static/src/views/web/activity/activity_record.js +++ b/addons/mail/static/src/views/web/activity/activity_record.js @@ -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() { diff --git a/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.js b/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.js index 3c87abdcd40..eee0596b147 100644 --- a/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.js +++ b/addons/project/static/src/project_sharing/views/form/project_sharing_form_controller.js @@ -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; } diff --git a/addons/web/static/src/core/utils/xml.js b/addons/web/static/src/core/utils/xml.js index 22eccc3c999..1be2e7cbf85 100644 --- a/addons/web/static/src/core/utils/xml.js +++ b/addons/web/static/src/core/utils/xml.js @@ -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); } /** diff --git a/addons/web/static/src/search/search_arch_parser.js b/addons/web/static/src/search/search_arch_parser.js index 3432bb9ecb8..6b2710a91ad 100644 --- a/addons/web/static/src/search/search_arch_parser.js +++ b/addons/web/static/src/search/search_arch_parser.js @@ -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 = {}; diff --git a/addons/web/static/src/views/calendar/calendar_arch_parser.js b/addons/web/static/src/views/calendar/calendar_arch_parser.js index 01d87ce3fa7..e22ede12084 100644 --- a/addons/web/static/src/views/calendar/calendar_arch_parser.js +++ b/addons/web/static/src/views/calendar/calendar_arch_parser.js @@ -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")) { diff --git a/addons/web/static/src/views/fields/field.js b/addons/web/static/src/views/fields/field.js index 18b4ba4a471..875f59e3565 100644 --- a/addons/web/static/src/views/fields/field.js +++ b/addons/web/static/src/views/fields/field.js @@ -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, diff --git a/addons/web/static/src/views/fields/relational_utils.js b/addons/web/static/src/views/fields/relational_utils.js index 4ad5a4dbb37..c9a5de4e561 100644 --- a/addons/web/static/src/views/fields/relational_utils.js +++ b/addons/web/static/src/views/fields/relational_utils.js @@ -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 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 532922f4fd0..b8270bfc829 100644 --- a/addons/web/static/src/views/form/form_arch_parser.js +++ b/addons/web/static/src/views/form/form_arch_parser.js @@ -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, }; } } diff --git a/addons/web/static/src/views/form/form_controller.js b/addons/web/static/src/views/form/form_controller.js index 40bdcdeba82..5c07b7175d8 100644 --- a/addons/web/static/src/views/form/form_controller.js +++ b/addons/web/static/src/views/form/form_controller.js @@ -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 } ); diff --git a/addons/web/static/src/views/form/form_renderer.js b/addons/web/static/src/views/form/form_renderer.js index 5df0a8e0e7c..94fbc075730 100644 --- a/addons/web/static/src/views/form/form_renderer.js +++ b/addons/web/static/src/views/form/form_renderer.js @@ -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"); diff --git a/addons/web/static/src/views/graph/graph_arch_parser.js b/addons/web/static/src/views/graph/graph_arch_parser.js index 9d2ad6c4154..a393bf16668 100644 --- a/addons/web/static/src/views/graph/graph_arch_parser.js +++ b/addons/web/static/src/views/graph/graph_arch_parser.js @@ -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] = {}; } 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 50a97ad94e1..e01d75a20f1 100644 --- a/addons/web/static/src/views/kanban/kanban_arch_parser.js +++ b/addons/web/static/src/views/kanban/kanban_arch_parser.js @@ -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, }; } diff --git a/addons/web/static/src/views/kanban/kanban_record.js b/addons/web/static/src/views/kanban/kanban_record.js index 191845b201b..a870d4af664 100644 --- a/addons/web/static/src/views/kanban/kanban_record.js +++ b/addons/web/static/src/views/kanban/kanban_record.js @@ -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; diff --git a/addons/web/static/src/views/kanban/kanban_record_quick_create.js b/addons/web/static/src/views/kanban/kanban_record_quick_create.js index d3bd7b48718..1f0b14b21f1 100644 --- a/addons/web/static/src/views/kanban/kanban_record_quick_create.js +++ b/addons/web/static/src/views/kanban/kanban_record_quick_create.js @@ -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 ); 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 d63d481c4b8..e69a73b5fee 100644 --- a/addons/web/static/src/views/list/list_arch_parser.js +++ b/addons/web/static/src/views/list/list_arch_parser.js @@ -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, }; } diff --git a/addons/web/static/src/views/pivot/pivot_arch_parser.js b/addons/web/static/src/views/pivot/pivot_arch_parser.js index 376c4312eb3..d32fcaf2c00 100644 --- a/addons/web/static/src/views/pivot/pivot_arch_parser.js +++ b/addons/web/static/src/views/pivot/pivot_arch_parser.js @@ -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; } diff --git a/addons/web/static/src/views/standard_view_props.js b/addons/web/static/src/views/standard_view_props.js index 06c298cca08..906cda0dd11 100644 --- a/addons/web/static/src/views/standard_view_props.js +++ b/addons/web/static/src/views/standard_view_props.js @@ -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 }, diff --git a/addons/web/static/src/views/view.js b/addons/web/static/src/views/view.js index 0c4888f9ba0..70ad7f0fb23 100644 --- a/addons/web/static/src/views/view.js +++ b/addons/web/static/src/views/view.js @@ -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(/ /g, nbsp)); + const archXmlDoc = parseXML(arch.replace(/ /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, diff --git a/addons/web/static/src/views/view_compiler.js b/addons/web/static/src/views/view_compiler.js index b480b9920ee..55b8d3b1f94 100644 --- a/addons/web/static/src/views/view_compiler.js +++ b/addons/web/static/src/views/view_compiler.js @@ -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} templates * @param {Record} [params] * @returns {Record} */ -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); } diff --git a/addons/web/static/src/webclient/settings_form_view/settings_form_arch_parser.js b/addons/web/static/src/webclient/settings_form_view/settings_form_arch_parser.js deleted file mode 100644 index dbb4b7e085c..00000000000 --- a/addons/web/static/src/webclient/settings_form_view/settings_form_arch_parser.js +++ /dev/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; - } -} diff --git a/addons/web/static/src/webclient/settings_form_view/settings_form_view.js b/addons/web/static/src/webclient/settings_form_view/settings_form_view.js index 690d2f96e2e..fc89bad8fa4 100644 --- a/addons/web/static/src/webclient/settings_form_view/settings_form_view.js +++ b/addons/web/static/src/webclient/settings_form_view/settings_form_view.js @@ -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); diff --git a/addons/web/static/tests/core/utils/xml_tests.js b/addons/web/static/tests/core/utils/xml_tests.js index 26d236e6272..07bcc3eed30 100644 --- a/addons/web/static/tests/core/utils/xml_tests.js +++ b/addons/web/static/tests/core/utils/xml_tests.js @@ -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 = ""; 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 = "
Valid
"; try { - parser.parseXML(XMLToParse); + parseXML(XMLToParse); assert.step("no error"); } catch (e) { if (e.message.includes("error occured while parsing")) { diff --git a/addons/web/static/tests/views/view_tests.js b/addons/web/static/tests/views/view_tests.js index bd117610afd..71413be256b 100644 --- a/addons/web/static/tests/views/view_tests.js +++ b/addons/web/static/tests/views/view_tests.js @@ -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`
`; @@ -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, `Specific arch content`); + assert.strictEqual(arch.outerHTML, `Specific arch content`); 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, `Specific arch content`); + assert.strictEqual(arch.outerHTML, `Specific arch content`); 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, `Specific arch content`); + assert.strictEqual(arch.outerHTML, `Specific arch content`); assert.deepEqual(fields, {}); assert.deepEqual(info.actionMenus, {}); assert.strictEqual(this.env.config.viewId, undefined);