diff --git a/addons/web/static/src/webclient/settings_form_view/settings_form_compiler.js b/addons/web/static/src/webclient/settings_form_view/settings_form_compiler.js index c4be8b5e93b..88f604f3a25 100644 --- a/addons/web/static/src/webclient/settings_form_view/settings_form_compiler.js +++ b/addons/web/static/src/webclient/settings_form_view/settings_form_compiler.js @@ -3,192 +3,175 @@ import { append, createElement, getTag } from "@web/core/utils/xml"; import { FormCompiler } from "@web/views/form/form_compiler"; import { toStringExpression } from "@web/views/utils"; - -function compileApp(el, params) { - if (el.getAttribute("notApp") === "1") { - return; - } - const module = { - key: el.getAttribute("name"), - string: el.getAttribute("string"), - imgurl: - el.getAttribute("logo") || - "/" + el.getAttribute("name") + "/static/description/icon.png", - }; - params.modules.push(module); - const settingsApp = createElement("SettingsApp", { - key: toStringExpression(module.key), - string: toStringExpression(module.string || ""), - imgurl: toStringExpression(module.imgurl), - selectedTab: "settings.selectedTab", - }); - - for (const child of el.children) { - append(settingsApp, this.compileNode(child, params)); - } - - return settingsApp; -} - -function compileBlock(el, params) { - const settingsContainer = createElement("SettingsBlock", { - title: toStringExpression(el.getAttribute("title") || ""), - tip: toStringExpression(el.getAttribute("help") || ""), - }); - for (const child of el.children) { - append(settingsContainer, this.compileNode(child, params)); - } - return settingsContainer; -} - -function compileSetting(el, params) { - const componentName = el.getAttribute("type") === "header" ? "SettingHeader" : "Setting"; - const setting = createElement(componentName, { - title: toStringExpression(el.getAttribute("title") || ""), - help: toStringExpression(el.getAttribute("help") || ""), - companyDependent: el.getAttribute("company_dependent") === "1" || "false", - documentation: toStringExpression(el.getAttribute("documentation") || ""), - record: `this.props.record`, - }); - let string = toStringExpression(el.getAttribute("string") || ""); - let addLabel = true; - params.labels = []; - Array.from(el.children).forEach((child, index) => { - if (getTag(child, true) === "field" && index === 0) { - const fieldSlot = createElement("t", { "t-set-slot": "fieldSlot" }); - const field = this.compileNode(child, params); - if (field) { - append(fieldSlot, field); - setting.setAttribute("fieldInfo", field.getAttribute("fieldInfo")); - - addLabel = child.hasAttribute("nolabel") - ? child.getAttribute("nolabel") !== "1" - : true; - const fieldName = child.getAttribute("name"); - string = child.hasAttribute("string") - ? toStringExpression(child.getAttribute("string")) - : string; - setting.setAttribute("fieldName", toStringExpression(fieldName)); - setting.setAttribute( - "fieldId", - toStringExpression(child.getAttribute("field_id") || fieldName) - ); - } - append(setting, fieldSlot); - } else { - append(setting, this.compileNode(child, params)); - } - }); - setting.setAttribute("string", string); - setting.setAttribute("addLabel", addLabel); - setting.setAttribute("labels", JSON.stringify(params.labels)); - return setting; -} - -function compileField(el, params) { - const res = this.compileField(el, params); - if (el.hasAttribute("widget")) { - const label = params.getFieldExpr(el.getAttribute("name"), el.getAttribute("widget")); - if (label) { - params.labels.push(label); - } - } - return res; -} - -const labelsWeak = new WeakMap(); -function compileLabel(el, params) { - const res = this.compileLabel(el, params); - // It the node is a FormLabel component node, the label is - // localized *after* the field. - // We don't know yet if the label refers to a field or not. - if (res.textContent && res.tagName !== "FormLabel") { - params.labels.push(res.textContent.trim()); - labelsWeak.set(res, { textContent: res.textContent }); - highlightElement(res); - } - return res; -} - -function compileGenericLabel(el, params) { - const res = this.compileGenericNode(el, params); - if (res.textContent) { - params.labels.push(res.textContent.trim()); - highlightElement(res); - } - return res; -} - -function highlightElement(el) { - for (const child of el.childNodes) { - if (child.nodeType === Node.TEXT_NODE) { - if (child.textContent.trim()) { - const highlight = createElement("HighlightText"); - highlight.setAttribute("originalText", `\`${child.textContent}\``); - el.replaceChild(highlight, child); - } - } else if (child.childNodes.length) { - highlightElement(child); - } - } -} - -function compileForm(el, params) { - const settingsPage = createElement("SettingsPage"); - settingsPage.setAttribute("slots", "{NoContentHelper:this.props.slots.NoContentHelper}"); - settingsPage.setAttribute("initialTab", "this.props.initialApp"); - settingsPage.setAttribute("t-slot-scope", "settings"); - - //props - params.modules = []; - - const res = this.compileForm(...arguments); - res.classList.remove("o_form_nosheet"); - - settingsPage.setAttribute("modules", JSON.stringify(params.modules)); - - for (const child of res.childNodes) { - append(settingsPage, this.compileNode(child, params)); - } - while (res.lastChild) { - res.removeChild(res.lastChild); - } - append(res, settingsPage); - - return res; -} +import { isTextNode } from "@web/views/view_compiler"; export class SettingsFormCompiler extends FormCompiler { setup() { super.setup(); - this.compilers.unshift( - { selector: "form", fn: compileForm }, - { selector: "app", fn: compileApp }, - { selector: "block", fn: compileBlock }, - { selector: "setting", fn: compileSetting }, - // search terms and highlight : - { selector: "label", fn: compileLabel, doNotCopyAttributes: true }, - { selector: "span.o_form_label", fn: compileGenericLabel }, - { selector: "div.text-muted", fn: compileGenericLabel }, - { selector: "field", fn: compileField } + this.compilers.push( + { selector: "app", fn: this.compileApp }, + { selector: "block", fn: this.compileBlock }, + { selector: "setting", fn: this.compileSetting } ); } - createLabelFromField(fieldId, fieldName, fieldString, label, params) { - const labelweak = labelsWeak.get(label); - if (labelweak) { - // Undo what we've done when we where not sure whether this label was attached to a field - // Now, we now it is. - label.textContent = labelweak.textContent; - } - const res = super.createLabelFromField(fieldId, fieldName, fieldString, label, params); - if (labelweak || label.hasAttribute("data-no-label")) { - // the work of pushing the label in the search structure is already done - return res; - } - let labelText = label.textContent || fieldString; - labelText = labelText ? labelText : params.record.fields[fieldName].string; - params.labels.push(labelText); + compileForm(el, params) { + const settingsPage = createElement("SettingsPage"); + settingsPage.setAttribute("slots", "{NoContentHelper:this.props.slots.NoContentHelper}"); + settingsPage.setAttribute("initialTab", "this.props.initialApp"); + settingsPage.setAttribute("t-slot-scope", "settings"); + + //props + params.modules = []; + + const res = super.compileForm(...arguments); + res.classList.remove("o_form_nosheet"); + + settingsPage.setAttribute("modules", JSON.stringify(params.modules)); + + // Move the compiled content of the form inside the settingsPage + while (res.firstChild) { + append(settingsPage, res.firstChild); + } + append(res, settingsPage); + + return res; + } + + compileApp(el, params) { + if (el.getAttribute("notApp") === "1") { + //An app noted with notApp="1" is not rendered. + + //This hack is used when a technical module defines settings, and we don't want to render + //the settings until the corresponding app is not installed. + + // For example, when installing the module website_sale, the module sale is also installed, + // but we don't want to render its settings (notApp="1"). + // On the contrary, when sale_management is installed, the module sale is also installed + // but in this case we want to see its settings (notApp="0"). + return; + } + const module = { + key: el.getAttribute("name"), + string: el.getAttribute("string"), + imgurl: + el.getAttribute("logo") || + "/" + el.getAttribute("name") + "/static/description/icon.png", + }; + params.modules.push(module); + const settingsApp = createElement("SettingsApp", { + key: toStringExpression(module.key), + string: toStringExpression(module.string || ""), + imgurl: toStringExpression(module.imgurl), + selectedTab: "settings.selectedTab", + }); + + for (const child of el.children) { + append(settingsApp, this.compileNode(child, params)); + } + + return settingsApp; + } + + compileBlock(el, params) { + const settingsContainer = createElement("SettingsBlock", { + title: toStringExpression(el.getAttribute("title") || ""), + tip: toStringExpression(el.getAttribute("help") || ""), + }); + for (const child of el.children) { + append(settingsContainer, this.compileNode(child, params)); + } + return settingsContainer; + } + + compileSetting(el, params) { + const componentName = el.getAttribute("type") === "header" ? "SettingHeader" : "Setting"; + const setting = createElement(componentName, { + title: toStringExpression(el.getAttribute("title") || ""), + help: toStringExpression(el.getAttribute("help") || ""), + companyDependent: el.getAttribute("company_dependent") === "1" || "false", + documentation: toStringExpression(el.getAttribute("documentation") || ""), + record: `this.props.record`, + }); + let string = toStringExpression(el.getAttribute("string") || ""); + let addLabel = true; + params.labels = []; + Array.from(el.children).forEach((child, index) => { + if (getTag(child, true) === "field" && index === 0) { + const fieldSlot = createElement("t", { "t-set-slot": "fieldSlot" }); + const field = this.compileNode(child, params); + if (field) { + append(fieldSlot, field); + setting.setAttribute("fieldInfo", field.getAttribute("fieldInfo")); + + addLabel = child.hasAttribute("nolabel") + ? child.getAttribute("nolabel") !== "1" + : true; + const fieldName = child.getAttribute("name"); + string = child.hasAttribute("string") + ? toStringExpression(child.getAttribute("string")) + : string; + setting.setAttribute("fieldName", toStringExpression(fieldName)); + setting.setAttribute( + "fieldId", + toStringExpression(child.getAttribute("field_id") || fieldName) + ); + } + append(setting, fieldSlot); + } else { + append(setting, this.compileNode(child, params)); + } + }); + setting.setAttribute("string", string); + setting.setAttribute("addLabel", addLabel); + setting.setAttribute("labels", JSON.stringify(params.labels)); + delete params.labels; + return setting; + } + + compileField(el, params) { + const res = super.compileField(el, params); + if (params.labels && el.hasAttribute("widget")) { + const label = params.getFieldExpr(el.getAttribute("name"), el.getAttribute("widget")); + if (label) { + params.labels.push(label); + } + } + return res; + } + + compileNode(node, params, evalInvisible) { + if (isTextNode(node)) { + if (params.labels && node.textContent.trim()) { + params.labels.push(node.textContent.trim()); + return createElement("HighlightText", { + originalText: toStringExpression(node.textContent), + }); + } + } + return super.compileNode(node, params, evalInvisible); + } + + createLabelFromField(fieldId, fieldName, fieldString, label, params) { + const res = super.createLabelFromField(fieldId, fieldName, fieldString, label, params); + if (res.hasAttribute("string") && params.labels) { + params.labels.push(res.getAttribute("string")); + } + return res; + } + + compileButton(el, params) { + const res = super.compileButton(el, params); + if (res.hasAttribute("string") && params.labels && res.children.length === 0) { + params.labels.push(res.getAttribute("string")); + const contentSlot = createElement("t"); + contentSlot.setAttribute("t-set-slot", "contents"); + const content = createElement("HighlightText", { + originalText: res.getAttribute("string"), + }); + append(contentSlot, content); + append(res, contentSlot); + } return res; } } diff --git a/addons/web/static/tests/webclient/settings_form_view/settings_form_view_tests.js b/addons/web/static/tests/webclient/settings_form_view/settings_form_view_tests.js index c71dfe7ed16..29742bdb80d 100644 --- a/addons/web/static/tests/webclient/settings_form_view/settings_form_view_tests.js +++ b/addons/web/static/tests/webclient/settings_form_view/settings_form_view_tests.js @@ -77,9 +77,13 @@ QUnit.module("SettingsFormView", (hooks) => { + - - + + + this is a baz + + label with content @@ -113,11 +117,11 @@ QUnit.module("SettingsFormView", (hooks) => { [...target.querySelectorAll(".settings .o_settings_container .o_form_label")].map( (x) => x.textContent ), - ["Bar", "This is Big BAR", "Foo", "Personalize setting"] + ["Bar", "Big BAZ", "Foo", "Personalize setting"] ); assert.deepEqual( [...target.querySelectorAll(".settings .text-muted")].map((x) => x.textContent), - ["this is bar", "this is big bar", "this is foo", "this is full personalize setting"] + ["this is bar", "this is a baz", "this is foo", "this is full personalize setting"] ); assert.deepEqual( [...target.querySelectorAll(".settings h2:not(.d-none)")].map((x) => x.textContent), @@ -161,7 +165,7 @@ QUnit.module("SettingsFormView", (hooks) => { [...target.querySelectorAll(".o_settings_container .o_setting_box .o_form_label")].map( (x) => x.textContent ), - ["Bar", "This is Big BAR"], + ["Bar", "Big BAZ"], "Foo is not shown" ); @@ -181,8 +185,8 @@ QUnit.module("SettingsFormView", (hooks) => { [...target.querySelectorAll(".o_settings_container .o_setting_box .o_form_label")].map( (x) => x.textContent ), - ["This is Big BAR"], - "Only 'Big Bar' is shown" + ["Big BAZ"], + "Only 'Big Baz' is shown" ); assert.deepEqual( [...target.querySelectorAll(".settings h2:not(.d-none)")].map((x) => x.textContent), @@ -194,13 +198,46 @@ QUnit.module("SettingsFormView", (hooks) => { ".app_settings_block:not(.d-none) .app_settings_header .o_setting_box" ); + await editSearch(target, "Manage Us"); + await execTimeouts(); + assert.strictEqual( + target.querySelector(".highlighter").textContent, + "Manage Us", + "Manage Us word highlighted" + ); + assert.deepEqual( + [...target.querySelectorAll(".o_settings_container .o_setting_box .o_form_label")].map( + (x) => x.textContent + ), + ["Bar"], + "Foo is not shown" + ); + assert.containsOnce( + target, + ".app_settings_block:not(.d-none) .app_settings_header .o_setting_box" + ); + await editSearch(target, "group Bar"); await execTimeouts(); assert.deepEqual( [...target.querySelectorAll(".o_settings_container .o_setting_box .o_form_label")].map( (x) => x.textContent ), - ["Bar", "This is Big BAR"], + ["Bar", "Big BAZ"], + "When searching a title, all group is shown" + ); + assert.containsOnce( + target, + ".app_settings_block:not(.d-none) .app_settings_header .o_setting_box" + ); + + await editSearch(target, "different"); + await execTimeouts(); + assert.deepEqual( + [...target.querySelectorAll(".o_settings_container .o_setting_box .o_form_label")].map( + (x) => x.textContent + ), + ["Personalize setting"], "When searching a title, all group is shown" ); assert.containsOnce( @@ -418,11 +455,11 @@ QUnit.module("SettingsFormView", (hooks) => { serverData, arch: ` - - - Make Changes - - + + + Make Changes + + `, mockRPC(route, { args, method, model }) { if (method === "create" && model === "res.config.settings") { @@ -454,9 +491,9 @@ QUnit.module("SettingsFormView", (hooks) => { - - - + + + @@ -482,13 +519,13 @@ QUnit.module("SettingsFormView", (hooks) => { serverData.views = { "res.config.settings,1,form": ` - - - - - - - + + + + + + + `, "task,2,list": ` @@ -648,14 +685,14 @@ QUnit.module("SettingsFormView", (hooks) => { serverData.views = { "res.config.settings,1,form": ` - - - - - - - - + + + + + + + + `, "task,2,list": '', "res.config.settings,false,search": "", @@ -1084,14 +1121,14 @@ QUnit.module("SettingsFormView", (hooks) => { serverData.views = { "res.config.settings,1,form": ` - - - - - - - - + + + + + + + + `, "res.config.settings,false,search": "", @@ -1278,10 +1315,12 @@ QUnit.module("SettingsFormView", (hooks) => { serverData, arch: ` - - - - + + + + + + `, }); @@ -1293,8 +1332,10 @@ QUnit.module("SettingsFormView", (hooks) => { const expectedCompiled = ` - - + + + + `; assert.areEquivalent(compiled.firstChild.innerHTML, expectedCompiled); @@ -1316,15 +1357,15 @@ QUnit.module("SettingsFormView", (hooks) => { serverData, arch: ` - - - - - this is Baz value: and this is the after text - - - - `, + + + + + this is Baz value: and this is the after text + + + + `, }); assert.strictEqual(