[IMP] web: improve setting search

Before this commit, the setting search was done exclusively in some
selected texts. These texts needed to be on some specific selectors
(field, label, span.o_form_label and div.text-muted).

Now, all text on a setting are searchable (including the text in
buttons). This commit also re-structure the setting compilers file to
remove the functions outside the class, this is done to standardize the
compilers (setting, form and view).

closes odoo/odoo#107226

Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Jorge Pinna Puissant
2022-12-08 08:48:46 +01:00
parent 4f0c264ca2
commit ebbc63dbff
2 changed files with 257 additions and 233 deletions
@@ -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;
}
}