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);