+
`;
diff --git a/addons/web/static/src/webclient/settings_form_view/highlight_text/form_label_highlight_text.js b/addons/web/static/src/webclient/settings_form_view/highlight_text/form_label_highlight_text.js
new file mode 100644
index 00000000000..0c088dce01f
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/highlight_text/form_label_highlight_text.js
@@ -0,0 +1,18 @@
+/** @odoo-module **/
+import { FormLabel } from "@web/views/form/form_label";
+import { HighlightText } from "./highlight_text";
+
+export class FormLabelHighlightText extends FormLabel {
+ setup() {
+ super.setup();
+ }
+ get className() {
+ if (this.props.className) {
+ return this.props.className;
+ }
+ return super.className;
+ }
+}
+
+FormLabelHighlightText.template = "web.FormLabelHighlightText";
+FormLabelHighlightText.components = { HighlightText };
diff --git a/addons/web/static/src/webclient/settings_form_view/highlight_text/form_label_highlight_text.xml b/addons/web/static/src/webclient/settings_form_view/highlight_text/form_label_highlight_text.xml
new file mode 100644
index 00000000000..449e5c8ae29
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/highlight_text/form_label_highlight_text.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/addons/web/static/src/webclient/settings_form_view/highlight_text/highlight_text.js b/addons/web/static/src/webclient/settings_form_view/highlight_text/highlight_text.js
new file mode 100644
index 00000000000..f9d2848e70c
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/highlight_text/highlight_text.js
@@ -0,0 +1,25 @@
+/** @odoo-module **/
+import { escapeRegExp } from "@web/core/utils/strings";
+
+const { Component, useState, onWillRender } = owl;
+
+export class HighlightText extends Component {
+ setup() {
+ this.searchState = useState(this.env.searchState);
+
+ onWillRender(() => {
+ const splitText = this.props.originalText.split(
+ new RegExp(`(${escapeRegExp(this.searchState.value)})`, "ig")
+ );
+ this.splitText =
+ this.searchState.value.length && splitText.length > 1
+ ? splitText
+ : [this.props.originalText];
+ });
+ }
+}
+HighlightText.template = "web.HighlightText";
+HighlightText.props = {
+ originalText: String,
+};
+HighlightText.highlightClass = "highlighter";
diff --git a/addons/web/static/src/webclient/settings_form_view/highlight_text/highlight_text.xml b/addons/web/static/src/webclient/settings_form_view/highlight_text/highlight_text.xml
new file mode 100644
index 00000000000..cd2e16ad107
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/highlight_text/highlight_text.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/addons/web/static/src/webclient/settings_form_view/highlight_text/settings_radio_field.js b/addons/web/static/src/webclient/settings_form_view/highlight_text/settings_radio_field.js
new file mode 100644
index 00000000000..5b36f8cbccd
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/highlight_text/settings_radio_field.js
@@ -0,0 +1,16 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { RadioField } from "@web/views/fields/radio/radio_field";
+import { FormLabelHighlightText } from "./form_label_highlight_text";
+
+export class SettingsRadioField extends RadioField {}
+
+SettingsRadioField.extractStringExpr = (fieldName, record) => {
+ const radioItems = SettingsRadioField.getItems(fieldName, record);
+ return radioItems.map((r) => r[1]);
+};
+SettingsRadioField.template = "web.SettingsRadioField";
+SettingsRadioField.components = { ...RadioField.components, FormLabelHighlightText };
+
+registry.category("fields").add("base_settings.radio", SettingsRadioField);
diff --git a/addons/web/static/src/webclient/settings_form_view/highlight_text/settings_radio_field.xml b/addons/web/static/src/webclient/settings_form_view/highlight_text/settings_radio_field.xml
new file mode 100644
index 00000000000..5f187276896
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/highlight_text/settings_radio_field.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/web/static/src/webclient/settings_form_view/settings/settings_app.js b/addons/web/static/src/webclient/settings_form_view/settings/settings_app.js
new file mode 100644
index 00000000000..b5690da131d
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/settings/settings_app.js
@@ -0,0 +1,12 @@
+/** @odoo-module **/
+
+const { Component, useState } = owl;
+
+export class SettingsApp extends Component {
+ setup() {
+ this.state = useState({
+ search: this.env.searchState,
+ });
+ }
+}
+SettingsApp.template = "web.SettingsApp";
diff --git a/addons/web/static/src/webclient/settings_form_view/settings/settings_app.xml b/addons/web/static/src/webclient/settings_form_view/settings/settings_app.xml
new file mode 100644
index 00000000000..24e43cb8ce0
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/settings/settings_app.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
diff --git a/addons/web/static/src/webclient/settings_form_view/settings/settings_page.js b/addons/web/static/src/webclient/settings_form_view/settings/settings_page.js
new file mode 100644
index 00000000000..54d1da58f02
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/settings/settings_page.js
@@ -0,0 +1,22 @@
+/** @odoo-module **/
+
+const { Component, useState } = owl;
+
+export class SettingsPage extends Component {
+ setup() {
+ this.state = useState({
+ selectedTab: "",
+ search: this.env.searchState,
+ });
+
+ if (this.props.modules) {
+ this.state.selectedTab = this.props.initialTab || this.props.modules[0].key;
+ }
+ }
+
+ onSettingTabClick(key) {
+ this.state.selectedTab = key;
+ this.env.searchState.value = "";
+ }
+}
+SettingsPage.template = "web.SettingsPage";
diff --git a/addons/web/static/src/webclient/settings_form_view/settings/settings_page.xml b/addons/web/static/src/webclient/settings_form_view/settings/settings_page.xml
new file mode 100644
index 00000000000..67f61fe75a7
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/settings/settings_page.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.js b/addons/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.js
new file mode 100644
index 00000000000..b92a917e376
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.js
@@ -0,0 +1,21 @@
+/** @odoo-module **/
+
+import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
+import { _lt } from "@web/core/l10n/translation";
+
+export class SettingsConfirmationDialog extends ConfirmationDialog {
+ _stayHere() {
+ if (this.props.stayHere) {
+ this.props.stayHere();
+ }
+ this.props.close();
+ }
+}
+SettingsConfirmationDialog.defaultProps = {
+ title: _lt("Unsaved changes"),
+};
+SettingsConfirmationDialog.template = "web.SettingsConfirmationDialog";
+SettingsConfirmationDialog.props = {
+ ...ConfirmationDialog.props,
+ stayHere: { type: Function, optional: true },
+};
diff --git a/addons/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.xml b/addons/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.xml
new file mode 100644
index 00000000000..9427c9bc0c1
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ Save
+
+
+ Stay Here
+
+
+ Discard
+
+
+
+
+
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
new file mode 100644
index 00000000000..ea977d2fbeb
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/settings_form_compiler.js
@@ -0,0 +1,248 @@
+/** @odoo-module **/
+
+import { append, createElement } from "@web/core/utils/xml";
+import { FormCompiler } from "@web/views/form/form_compiler";
+import { getModifier } from "@web/views/view_compiler";
+
+function compileSettingsPage(el, params) {
+ const settings = createElement("SettingsPage");
+ settings.setAttribute("slots", "props.slots");
+ settings.setAttribute("initialTab", "props.initialApp");
+ settings.setAttribute("t-slot-scope", "settings");
+
+ //props
+ const modules = [];
+
+ for (const child of el.children) {
+ if (child.nodeName === "div" && child.classList.value.includes("app_settings_block")) {
+ params.module = {
+ key: child.getAttribute("data-key"),
+ string: child.getAttribute("string"),
+ imgurl: getAppIconUrl(child.getAttribute("data-key")),
+ isVisible: getModifier(child, "invisible"),
+ };
+ params.config = {};
+ if (!child.classList.value.includes("o_not_app")) {
+ modules.push(params.module);
+ append(settings, this.compileNode(child, params));
+ }
+ }
+ }
+
+ settings.setAttribute("modules", JSON.stringify(modules));
+ return settings;
+}
+
+function getAppIconUrl(module) {
+ return module === "general_settings"
+ ? "/base/static/description/settings.png"
+ : "/" + module + "/static/description/icon.png";
+}
+
+function compileSettingsApp(el, params) {
+ const settingsBlock = createElement("SettingsApp");
+ settingsBlock.setAttribute("t-props", JSON.stringify(params.module));
+ settingsBlock.setAttribute("selectedTab", "settings.selectedTab");
+
+ params.config.app = el.getAttribute("data-key");
+ params.config.groupTitleId = undefined;
+ params.config.groupTitle = "";
+ params.config.groupTipId = undefined;
+ params.config.groupTip = "";
+ params.config.container = undefined;
+ params.config.settingBox = undefined;
+
+ for (const child of el.children) {
+ append(settingsBlock, this.compileNode(child, params));
+ }
+
+ settingsBlock.setAttribute(
+ "t-if",
+ `!searchState.value or search("app", "${el.getAttribute("data-key")}")`
+ );
+
+ return settingsBlock;
+}
+
+function compileSettingsHeader(el, params) {
+ const header = el.cloneNode();
+ for (const child of el.children) {
+ append(header, this.compileNode(child, { ...params, config: null }));
+ }
+ return header;
+}
+
+let groupTitleId = 0;
+
+function compileSettingsGroupTitle(el, params) {
+ const res = this.compileGenericNode(el, params);
+ const groupTitle = res.textContent;
+
+ //HighlightText
+ const highlight = createElement("HighlightText");
+ highlight.setAttribute("originalText", `\`${groupTitle}\``);
+ append(res, highlight);
+ res.firstChild.remove();
+
+ if (params.config) {
+ params.config.groupTitleId = ++groupTitleId;
+ params.config.groupTitle = groupTitle;
+ params.config.groupTipId = undefined;
+ params.config.groupTip = undefined;
+ params.config.container = undefined;
+ params.config.settingBox = undefined;
+ params.labels.push({
+ label: groupTitle.trim(),
+ ...params.config,
+ });
+ res.setAttribute("t-if", `!searchState.value or search("groupTitleId", ${groupTitleId})`);
+ }
+
+ return res;
+}
+
+let groupTipId = 0;
+
+function compileSettingsGroupTip(el, params) {
+ const res = this.compileGenericNode(el, params);
+ const tip = res.textContent;
+
+ //HighlightText
+ const highlight = createElement("HighlightText");
+ highlight.setAttribute("originalText", `\`${tip}\``);
+ append(res, highlight);
+ res.firstChild.remove();
+
+ if (params.config) {
+ params.config.groupTipId = ++groupTipId;
+ params.config.groupTip = tip;
+ params.config.container = undefined;
+ params.config.settingBox = undefined;
+ params.labels.push({
+ label: tip.trim(),
+ ...params.config,
+ });
+ res.setAttribute("t-if", `!searchState.value or search("groupTipId", ${groupTipId})`);
+ }
+
+ return res;
+}
+
+let containerId = 0;
+
+function compileSettingsContainer(el, params) {
+ if (params.config) {
+ params.config.container = ++containerId;
+ params.config.settingBox = undefined;
+ params.containerLabels = [];
+ }
+ const res = this.compileGenericNode(el, params);
+ if (params.config) {
+ res.setAttribute("t-if", `!searchState.value or search("container", ${containerId})`);
+ }
+ return res;
+}
+
+let settingBoxId = 0;
+
+function compileSettingBox(el, params) {
+ if (params.config) {
+ settingBoxId++;
+ params.config.settingBox = settingBoxId;
+ }
+ const res = this.compileGenericNode(el, params);
+ if (params.config) {
+ res.setAttribute("t-if", `!searchState.value or search("settingBox", ${settingBoxId})`);
+ }
+ return res;
+}
+
+function compileField(el, params) {
+ const res = this.compileField(el, params);
+ if (params.config) {
+ let widgetName;
+ if (el.hasAttribute("widget")) {
+ widgetName = el.getAttribute("widget");
+ const label = params.getFieldExpr(el.getAttribute("name"), widgetName);
+ if (label) {
+ params.labels.push({
+ label,
+ ...params.config,
+ });
+ }
+ }
+ }
+ return res;
+}
+
+function compileLabel(el, params) {
+ const res = this.compileLabel(el, params);
+ if (res.textContent && res.tagName !== "FormLabel" && params.config) {
+ params.labels.push({
+ label: res.textContent.trim(),
+ ...params.config,
+ });
+ //HighlightText
+ const highlight = createElement("HighlightText");
+ highlight.setAttribute("originalText", `\`${res.textContent}\``);
+ append(res, highlight);
+ res.firstChild.remove();
+ }
+ return res;
+}
+
+function compileGenericLabel(el, params) {
+ const res = this.compileGenericNode(el, params);
+ if (res.textContent && params.config) {
+ params.labels.push({
+ label: res.textContent.trim(),
+ ...params.config,
+ });
+ //HighlightText
+ const highlight = createElement("HighlightText");
+ highlight.setAttribute("originalText", `\`${res.textContent}\``);
+ append(res, highlight);
+ res.firstChild.remove();
+ }
+ return res;
+}
+
+function compileForm() {
+ const res = this.compileForm(...arguments);
+ res.classList.remove("o_form_nosheet");
+ return res;
+}
+
+export class SettingsFormCompiler extends FormCompiler {
+ setup() {
+ super.setup();
+ this.compilers.unshift(
+ { selector: "form", fn: compileForm },
+ { selector: "div.settings", fn: compileSettingsPage },
+ { selector: "div.app_settings_block", fn: compileSettingsApp },
+ { selector: "div.app_settings_header", fn: compileSettingsHeader },
+ // objects to show/hide in the search
+ { selector: "div.o_setting_box", fn: compileSettingBox },
+ { selector: "div.o_settings_container", fn: compileSettingsContainer },
+ // h2
+ { selector: "h2", fn: compileSettingsGroupTitle },
+ { selector: "h3.o_setting_tip", fn: compileSettingsGroupTip },
+ // search terms and highlight :
+ { selector: "label", fn: compileLabel },
+ { selector: "span.o_form_label", fn: compileGenericLabel },
+ { selector: "div.text-muted", fn: compileGenericLabel },
+ { selector: "field", fn: compileField }
+ );
+ }
+ createLabelFromField(fieldId, fieldName, fieldString, label, params) {
+ const res = super.createLabelFromField(fieldId, fieldName, fieldString, label, params);
+ let labelText = label.textContent || fieldString;
+ labelText = labelText ? labelText : params.record.fields[fieldName].string;
+
+ params.labels.push({
+ label: labelText,
+ ...params.config,
+ });
+ return res;
+ }
+}
diff --git a/addons/web/static/src/webclient/settings_form_view/settings_form_controller.js b/addons/web/static/src/webclient/settings_form_view/settings_form_controller.js
new file mode 100644
index 00000000000..d4a31a890f4
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/settings_form_controller.js
@@ -0,0 +1,109 @@
+/** @odoo-module **/
+
+import { useAutofocus } from "@web/core/utils/hooks";
+import { formView } from "@web/views/form/form_view";
+import { useViewButtons } from "@web/views/view_button/view_button_hook";
+import { SettingsConfirmationDialog } from "./settings_confirmation_dialog";
+import { SettingsFormRenderer } from "./settings_form_renderer";
+
+const { useSubEnv, useState, useRef, useEffect } = owl;
+
+export class SettingsFormController extends formView.Controller {
+ setup() {
+ super.setup();
+ const beforeExecuteAction = async (clickParams) => {
+ let _continue = true;
+ if (
+ this.model.root.isDirty &&
+ !["cancel", "execute"].includes(clickParams.name) &&
+ !clickParams.noSaveDialog
+ ) {
+ const message = this.env._t("Would you like to save your changes?");
+ await new Promise((resolve) => {
+ this.dialogService.add(
+ SettingsConfirmationDialog,
+ {
+ body: message,
+ confirm: async () => {
+ await this.model.root.save({ stayInEdition: true });
+ await this.save();
+ },
+ cancel: () => {},
+ stayHere: () => {
+ _continue = false;
+ },
+ },
+ { onClose: resolve }
+ );
+ });
+ } else {
+ if (clickParams.name === "execute") {
+ _continue = await this.model.root.save({ stayInEdition: true });
+ }
+ }
+ return _continue;
+ };
+ useViewButtons(this.model, useRef("root"), { beforeExecuteAction });
+ useAutofocus();
+ this.state = useState({ displayNoContent: false });
+ this.searchState = useState({ value: "" });
+ this.rootRef = useRef("root");
+ useSubEnv({ searchState: this.searchState });
+ useEffect(
+ () => {
+ if (this.rootRef.el.querySelector(".settings .o_setting_box")) {
+ this.state.displayNoContent = false;
+ } else {
+ this.state.displayNoContent = true;
+ }
+ },
+ () => [this.searchState.value]
+ );
+ useEffect(() => {
+ if (this.env.__beforeLeave__) {
+ this.env.__beforeLeave__.remove(this);
+ }
+ if (this.env.__getLocalState__) {
+ this.env.__getLocalState__.remove(this);
+ }
+ });
+
+ this.initialApp = "module" in this.props.context && this.props.context.module;
+ }
+
+ displayName() {
+ return this.env._t("Settings");
+ }
+
+ //This is needed to avoid the auto save when unload
+ beforeUnload() {}
+
+ //This is needed to avoid writing the id on the url
+ updateURL() {}
+
+ async save() {
+ this.env.onClickViewButton({
+ clickParams: {
+ name: "execute",
+ type: "object",
+ },
+ record: this.model.root,
+ });
+ }
+
+ discard() {
+ this.env.onClickViewButton({
+ clickParams: {
+ name: "cancel",
+ type: "object",
+ },
+ record: this.model.root,
+ });
+ }
+}
+
+SettingsFormController.components = {
+ ...formView.Controller.components,
+ Renderer: SettingsFormRenderer,
+};
+SettingsFormController.template = "web.SettingsFormView";
diff --git a/addons/web/static/src/webclient/settings_form_view/settings_form_renderer.js b/addons/web/static/src/webclient/settings_form_view/settings_form_renderer.js
new file mode 100644
index 00000000000..c2fb3daace3
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/settings_form_renderer.js
@@ -0,0 +1,60 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { escapeRegExp } from "@web/core/utils/strings";
+import { FormRenderer } from "@web/views/form/form_renderer";
+import { FormLabelHighlightText } from "./highlight_text/form_label_highlight_text";
+import { HighlightText } from "./highlight_text/highlight_text";
+import { SettingsApp } from "./settings/settings_app";
+import { SettingsPage } from "./settings/settings_page";
+
+const { useState } = owl;
+
+const fieldRegistry = registry.category("fields");
+
+const labels = Object.create(null);
+
+export class SettingsFormRenderer extends FormRenderer {
+ setup() {
+ if (!labels[this.props.archInfo.arch]) {
+ labels[this.props.archInfo.arch] = [];
+ }
+ this.compileParams = {
+ labels: labels[this.props.archInfo.arch],
+ getFieldExpr: this.getFieldExpr,
+ record: this.props.record,
+ };
+ super.setup();
+ this.searchState = useState(this.env.searchState);
+ }
+ search(kind, value) {
+ const regexp = new RegExp(escapeRegExp(this.searchState.value), "i");
+ for (let x of labels[this.props.archInfo.arch]) {
+ if (x[kind] === value) {
+ if (regexp.test([x.label, x.groupTitle, x.groupTip].join())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ getFieldExpr(fieldName, fieldWidget) {
+ const name = `base_settings.${fieldWidget}`;
+ let fieldClass;
+ if (fieldRegistry.contains(name)) {
+ fieldClass = fieldRegistry.get(name);
+ }
+ if (fieldClass && fieldClass.extractStringExpr) {
+ return fieldClass.extractStringExpr(fieldName, this.record);
+ } else {
+ return "";
+ }
+ }
+}
+SettingsFormRenderer.components = {
+ ...FormRenderer.components,
+ SettingsPage,
+ SettingsApp,
+ HighlightText,
+ FormLabel: FormLabelHighlightText,
+};
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
new file mode 100644
index 00000000000..46bad63fee1
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/settings_form_view.js
@@ -0,0 +1,18 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+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";
+
+export const settingsFormView = {
+ ...formView,
+ display: {},
+ buttonTemplate: "web.SettingsFormView.Buttons",
+ Controller: SettingsFormController,
+ Compiler: SettingsFormCompiler,
+ Renderer: SettingsFormRenderer,
+};
+
+registry.category("views").add("base_settings", settingsFormView);
diff --git a/addons/web/static/src/webclient/settings_form_view/settings_form_view.scss b/addons/web/static/src/webclient/settings_form_view/settings_form_view.scss
new file mode 100644
index 00000000000..9059b090a6a
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/settings_form_view.scss
@@ -0,0 +1,18 @@
+.o-settings-form-view {
+ .o_base_settings {
+ height: 100%;
+ overflow: auto;
+ }
+
+ .o_setting_container {
+ height: 100%;
+ > * {
+ overflow: auto;
+ flex: 1 0 auto;
+ }
+ }
+
+ .o_view_nocontent {
+ position: static;
+ }
+}
diff --git a/addons/web/static/src/webclient/settings_form_view/settings_form_view.xml b/addons/web/static/src/webclient/settings_form_view/settings_form_view.xml
new file mode 100644
index 00000000000..b764893b1c9
--- /dev/null
+++ b/addons/web/static/src/webclient/settings_form_view/settings_form_view.xml
@@ -0,0 +1,37 @@
+
+
+
+
+ o-settings-form-view
+
+
+
+
+
+
+
+ initialApp
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Unsaved changes
+
+
+
diff --git a/addons/web/static/tests/webclient/actions/legacy_tests.js b/addons/web/static/tests/webclient/actions/legacy_tests.js
index 805ab321c2f..5485823dc4a 100644
--- a/addons/web/static/tests/webclient/actions/legacy_tests.js
+++ b/addons/web/static/tests/webclient/actions/legacy_tests.js
@@ -64,14 +64,14 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
- assert.containsOnce(target, ".o_list_view");
+ assert.containsOnce(target, ".o_legacy_list_view");
list.trigger_up("warning", {
title: "Warning!!!",
message: "This is a warning...",
});
await testUtils.nextTick();
await legacyExtraNextTick();
- assert.containsOnce(target, ".o_list_view");
+ assert.containsOnce(target, ".o_legacy_list_view");
assert.containsOnce(document.body, ".o_notification.border-warning");
assert.strictEqual($(".o_notification_title").text(), "Warning!!!");
assert.strictEqual($(".o_notification_content").text(), "This is a warning...");
@@ -90,7 +90,7 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
- assert.containsOnce(target, ".o_list_view");
+ assert.containsOnce(target, ".o_legacy_list_view");
list.trigger_up("warning", {
title: "Warning!!!",
message: "This is a warning...",
@@ -98,7 +98,7 @@ QUnit.module("ActionManager", (hooks) => {
});
await testUtils.nextTick();
await legacyExtraNextTick();
- assert.containsOnce(target, ".o_list_view");
+ assert.containsOnce(target, ".o_legacy_list_view");
assert.containsOnce(document.body, ".modal");
assert.strictEqual($(".modal-title").text(), "Warning!!!");
assert.strictEqual($(".modal-body").text(), "This is a warning...");
@@ -116,7 +116,7 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
- assert.containsOnce(target, ".o_list_view");
+ assert.containsOnce(target, ".o_legacy_list_view");
list.trigger_up("warning", {
title: "Warning!!!",
message: "This is a warning...\nabc",
@@ -124,7 +124,7 @@ QUnit.module("ActionManager", (hooks) => {
});
await testUtils.nextTick();
await legacyExtraNextTick();
- assert.containsOnce(target, ".o_list_view");
+ assert.containsOnce(target, ".o_legacy_list_view");
assert.containsOnce(document.body, ".modal");
assert.strictEqual($(".modal-title").text(), "Warning!!!");
assert.strictEqual($(".modal-body")[0].innerText, "This is a warning...\nabc");
@@ -615,7 +615,7 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData });
await doAction(webClient, 3);
- assert.containsOnce(target, ".o_list_view");
+ assert.containsOnce(target, ".o_legacy_list_view");
assert.strictEqual($(target).find(".breadcrumb-item").text(), "Partners");
await doAction(webClient, {
@@ -721,25 +721,25 @@ QUnit.module("ActionManager", (hooks) => {
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 3);
- assert.containsOnce(target, ".o_list_view");
+ assert.containsOnce(target, ".o_legacy_list_view");
- await testUtils.dom.click(target.querySelector(".o_list_view .o_data_row"));
+ await testUtils.dom.click(target.querySelector(".o_legacy_list_view .o_data_row"));
await legacyExtraNextTick();
- assert.containsOnce(target, ".o_form_view");
+ assert.containsOnce(target, ".o_legacy_form_view");
await testUtils.dom.click(target.querySelector(".o_back_button"));
await legacyExtraNextTick();
- assert.containsOnce(target, ".o_list_view");
+ assert.containsOnce(target, ".o_legacy_list_view");
- await testUtils.dom.click(target.querySelector(".o_list_view .o_data_row"));
- await testUtils.dom.click(target.querySelector(".o_list_view .o_data_row"));
+ await testUtils.dom.click(target.querySelector(".o_legacy_list_view .o_data_row"));
+ await testUtils.dom.click(target.querySelector(".o_legacy_list_view .o_data_row"));
await legacyExtraNextTick();
- assert.containsOnce(target, ".o_list_view");
+ assert.containsOnce(target, ".o_legacy_list_view");
def.resolve();
await testUtils.nextTick();
await legacyExtraNextTick();
- assert.containsOnce(target, ".o_form_view");
+ assert.containsOnce(target, ".o_legacy_form_view");
});
});
diff --git a/addons/web/static/tests/webclient/settings_form_view_tests.js b/addons/web/static/tests/webclient/settings_form_view_tests.js
new file mode 100644
index 00000000000..171b2b67cb2
--- /dev/null
+++ b/addons/web/static/tests/webclient/settings_form_view_tests.js
@@ -0,0 +1,1173 @@
+/** @odoo-module **/
+
+import {
+ click,
+ editInput,
+ getFixture,
+ makeDeferred,
+ mockTimeout,
+ nextTick,
+} from "@web/../tests/helpers/utils";
+import { editSearch } from "@web/../tests/search/helpers";
+import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
+import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
+
+let target;
+let serverData;
+let execTimeouts;
+
+QUnit.module("SettingsFormView", (hooks) => {
+ hooks.beforeEach(() => {
+ serverData = {
+ models: {
+ "res.config.settings": {
+ fields: {
+ foo: { string: "Foo", type: "boolean" },
+ bar: { string: "Bar", type: "boolean" },
+ tasks: { string: "one2many field", type: "one2many", relation: "task" },
+ baz: {
+ string: "Baz",
+ type: "selection",
+ selection: [
+ [1, "treads"],
+ [2, "treats"],
+ ],
+ default: 1,
+ },
+ },
+ },
+ task: {
+ fields: {},
+ },
+ },
+ };
+ target = getFixture();
+ setupViewRegistries();
+ const { execRegisteredTimeouts } = mockTimeout();
+ execTimeouts = () => {
+ execRegisteredTimeouts();
+ return nextTick();
+ };
+ });
+
+ QUnit.test("change setting on nav bar click in base settings", async function (assert) {
+ await makeView({
+ type: "form",
+ resModel: "res.config.settings",
+ serverData,
+ arch: `
+
`,
+ });
+
+ assert.hasAttrValue(
+ target.querySelector(".selected"),
+ "data-key",
+ "crm",
+ "crm setting selected"
+ );
+ assert.isVisible(
+ target.querySelector(".settings .app_settings_block"),
+ "res.config.settings settings show"
+ );
+ assert.deepEqual(
+ [...target.querySelectorAll(".settings .o_form_label")].map((x) => x.textContent),
+ ["Bar", "This is Big BAR", "Foo"]
+ );
+ assert.deepEqual(
+ [...target.querySelectorAll(".settings .text-muted")].map((x) => x.textContent),
+ ["this is bar", "this is big bar", "this is foo"]
+ );
+ assert.deepEqual(
+ [...target.querySelectorAll(".settings h2")].map((x) => x.textContent),
+ ["Title of group Bar", "Title of group Foo"]
+ );
+ assert.doesNotHaveClass(target.querySelector(".o_form_editable"), "o_form_nosheet");
+ assert.strictEqual(
+ document.activeElement,
+ target.querySelector(".o_searchview input"),
+ "searchview input should be focused"
+ );
+
+ await editSearch(target, "Hello there");
+ await execTimeouts();
+ assert.strictEqual(
+ target.querySelector(".o_searchview input").value,
+ "Hello there",
+ "input value should be updated"
+ );
+
+ await editSearch(target, "b");
+ await execTimeouts();
+ assert.strictEqual(
+ target.querySelector(".highlighter").textContent,
+ "B",
+ "b word highlighted"
+ );
+ assert.deepEqual(
+ [...target.querySelectorAll(".o_setting_box .o_form_label")].map((x) => x.textContent),
+ ["Bar", "This is Big BAR"],
+ "Foo is not shown"
+ );
+
+ assert.deepEqual(
+ [...target.querySelectorAll(".settings h2")].map((x) => x.textContent),
+ ["Title of group Bar"],
+ "The title of group Bar is also selected"
+ );
+
+ await editSearch(target, "Big");
+ await execTimeouts();
+ assert.deepEqual(
+ [...target.querySelectorAll(".o_setting_box .o_form_label")].map((x) => x.textContent),
+ ["This is Big BAR"],
+ "Only 'Big Bar' is shown"
+ );
+ assert.deepEqual(
+ [...target.querySelectorAll(".settings h2")].map((x) => x.textContent),
+ ["Title of group Bar"],
+ "The title of group Bar is also selected"
+ );
+
+ await editSearch(target, "group Bar");
+ await execTimeouts();
+ assert.deepEqual(
+ [...target.querySelectorAll(".o_setting_box .o_form_label")].map((x) => x.textContent),
+ ["Bar", "This is Big BAR"],
+ "When searching a title, all group is shown"
+ );
+
+ await editSearch(target, "bx");
+ await execTimeouts();
+ await nextTick();
+ assert.isVisible(
+ target.querySelector(".o_nocontent_help"),
+ "record not found message shown"
+ );
+ await editSearch(target, "Fo");
+ await execTimeouts();
+ assert.strictEqual(
+ target.querySelector(".highlighter").textContent,
+ "Fo",
+ "F word highlighted"
+ );
+ assert.deepEqual(
+ [...target.querySelectorAll(".o_setting_box .o_form_label")].map((x) => x.textContent),
+ ["Foo"],
+ "only Foo is shown"
+ );
+ });
+
+ QUnit.test("unhighlight section not matching anymore", async function (assert) {
+ await makeView({
+ type: "form",
+ resModel: "res.config.settings",
+ serverData,
+ arch: `
+
`,
+ });
+ assert.hasAttrValue(
+ target.querySelector(".selected"),
+ "data-key",
+ "crm",
+ "crm setting selected"
+ );
+ assert.isVisible(
+ target.querySelector(".settings .app_settings_block"),
+ "project settings show"
+ );
+
+ await editSearch(target, "trea");
+ await execTimeouts();
+ assert.containsN(target, ".highlighter", 2, "should have 2 options highlighted");
+ assert.deepEqual(
+ [...target.querySelectorAll(".highlighter")].map((x) => x.parentElement.textContent),
+ ["treads", "treats"]
+ );
+
+ await editSearch(target, "tread");
+ await execTimeouts();
+ assert.containsN(target, ".highlighter", 1, "should have only one highlighted");
+ assert.deepEqual(
+ [...target.querySelectorAll(".highlighter")].map((x) => x.parentElement.textContent),
+ ["treads"]
+ );
+ });
+
+ QUnit.test("hide / show setting tips properly", async function (assert) {
+ await makeView({
+ type: "form",
+ resModel: "res.config.settings",
+ serverData,
+ arch: `
+
`,
+ });
+ assert.containsOnce(target, ".o_setting_tip", "Tip should not be hidden initially");
+ await editSearch(target, "below");
+ await execTimeouts();
+ assert.containsOnce(target, ".o_setting_tip", "Tip should not be hidden");
+ await editSearch(target, "Foo");
+ await execTimeouts();
+ assert.containsNone(target, ".o_setting_tip", "Tip should not be displayed");
+ await editSearch(target, "");
+ await execTimeouts();
+ assert.containsOnce(target, ".o_setting_tip", "Tip should not be hidden");
+ });
+
+ QUnit.test(
+ "settings views does not read existing id when coming back in breadcrumbs",
+ async function (assert) {
+ assert.expect(9);
+
+ serverData.actions = {
+ 1: {
+ id: 1,
+ name: "Settings view",
+ res_model: "res.config.settings",
+ type: "ir.actions.act_window",
+ views: [[1, "form"]],
+ },
+ 4: {
+ id: 4,
+ name: "Other action",
+ res_model: "task",
+ type: "ir.actions.act_window",
+ views: [[2, "list"]],
+ },
+ };
+
+ serverData.views = {
+ "res.config.settings,1,form": `
+
`,
+ "task,2,list": `
+
+
+ `,
+ "res.config.settings,false,search": "
",
+ "task,false,search": "
",
+ };
+
+ const mockRPC = (route, args) => {
+ if (args.method) {
+ assert.step(args.method);
+ }
+ };
+
+ const webClient = await createWebClient({ serverData, mockRPC });
+
+ await doAction(webClient, 1);
+ assert.notOk(target.querySelector(".custom-control input").disabled);
+ await click(target.querySelector("button[name='4']"));
+ assert.strictEqual(
+ target.querySelector(".breadcrumb").textContent,
+ "SettingsOther action"
+ );
+ await click(target.querySelector(".o_control_panel .breadcrumb-item a"));
+ assert.notOk(target.querySelector(".custom-control input").disabled);
+ assert.verifySteps([
+ "get_views", // initial setting action
+ "onchange", // this is a setting view => create new record
+ "get_views", // for other action in breadcrumb,
+ "web_search_read", // with a searchread
+ "onchange", // when we come back, we want to restart from scratch
+ ]);
+ }
+ );
+
+ QUnit.test("Auto save: don't save on closing tab/browser", async function (assert) {
+ assert.expect(3);
+
+ await makeView({
+ type: "form",
+ resModel: "res.config.settings",
+ serverData,
+ arch: `
+
`,
+ mockRPC(route, { args, method, model }) {
+ if (method === "create" && model === "res.config.settings") {
+ assert.notOk(args, "settings should not be saved");
+ }
+ },
+ });
+
+ assert.containsNone(
+ target,
+ ".o_field_boolean input:checked",
+ "checkbox should not be checked"
+ );
+ assert.containsNone(target, ".o_dirty_warning", "warning message should not be shown");
+ await click(target.querySelector(".custom-control input[id=bar]"));
+ assert.containsOnce(target, ".o_field_boolean input:checked", "checkbox should be checked");
+
+ window.dispatchEvent(new Event("beforeunload"));
+ await nextTick();
+ });
+
+ QUnit.test("settings views does not write the id on the url", async function (assert) {
+ serverData.actions = {
+ 1: {
+ id: 1,
+ name: "Settings view",
+ res_model: "res.config.settings",
+ type: "ir.actions.act_window",
+ views: [[1, "form"]],
+ },
+ };
+
+ serverData.views = {
+ "res.config.settings,1,form": `
+
`,
+ "task,2,list": `
+
+
+ `,
+ "res.config.settings,false,search": "
",
+ "task,false,search": "
",
+ };
+
+ const mockRPC = (route, args) => {
+ if (route === "/web/dataset/call_button") {
+ if (args.method === "execute") {
+ return true;
+ }
+ }
+ };
+
+ const webClient = await createWebClient({ serverData, mockRPC });
+
+ await doAction(webClient, 1);
+ assert.notOk(target.querySelector(".custom-control input").disabled);
+ await click(target.querySelector(".custom-control input"));
+ assert.containsOnce(target, ".o_field_boolean input:checked", "checkbox should be checked");
+ await click(target.querySelector(".o_form_button_save"));
+
+ await nextTick();
+ assert.notOk(webClient.env.services.router.current.hash.id);
+ });
+
+ QUnit.test(
+ "settings views can search when coming back in breadcrumbs",
+ async function (assert) {
+ serverData.actions = {
+ 1: {
+ id: 1,
+ name: "Settings view",
+ res_model: "res.config.settings",
+ type: "ir.actions.act_window",
+ views: [[1, "form"]],
+ },
+ 4: {
+ id: 4,
+ name: "Other action",
+ res_model: "task",
+ type: "ir.actions.act_window",
+ views: [[2, "list"]],
+ },
+ };
+
+ serverData.views = {
+ "res.config.settings,1,form": `
+
`,
+ "task,2,list": `
+
+
+ `,
+ "res.config.settings,false,search": "
",
+ "task,false,search": "
",
+ };
+
+ const webClient = await createWebClient({ serverData });
+
+ await doAction(webClient, 1);
+ await click(target.querySelector("button[name='4']"));
+ await click(target.querySelector(".o_control_panel .breadcrumb-item a"));
+ await editSearch(target, "Fo");
+ await execTimeouts();
+ assert.strictEqual(
+ target.querySelector(".highlighter").textContent,
+ "Fo",
+ "Fo word highlighted"
+ );
+ }
+ );
+
+ QUnit.test(
+ "clicking on any button in setting should show discard warning if setting form is dirty",
+ async function (assert) {
+ assert.expect(12);
+
+ serverData.actions = {
+ 1: {
+ id: 1,
+ name: "Settings view",
+ res_model: "res.config.settings",
+ type: "ir.actions.act_window",
+ views: [[1, "form"]],
+ },
+ 4: {
+ id: 4,
+ name: "Other action",
+ res_model: "task",
+ type: "ir.actions.act_window",
+ views: [[2, "list"]],
+ },
+ };
+
+ serverData.views = {
+ "res.config.settings,1,form": `
+
`,
+ "task,2,list": '
',
+ "res.config.settings,false,search": "
",
+ "task,false,search": "
",
+ };
+
+ const mockRPC = (route, args) => {
+ if (route === "/web/dataset/call_button") {
+ if (args.method === "execute") {
+ assert.ok("execute method called");
+ return true;
+ }
+ if (args.method === "cancel") {
+ assert.ok("cancel method called");
+ return true;
+ }
+ }
+ };
+
+ const webClient = await createWebClient({ serverData, mockRPC });
+
+ await doAction(webClient, 1);
+ assert.containsNone(
+ target,
+ ".o_field_boolean input:checked",
+ "checkbox should not be checked"
+ );
+
+ await click(target.querySelector(".custom-control input"));
+ assert.containsOnce(
+ target,
+ ".o_field_boolean input:checked",
+ "checkbox should be checked"
+ );
+
+ await click(target.querySelector("button[name='4']"));
+ assert.containsOnce(document.body, ".modal", "should open a warning dialog");
+
+ await click(target.querySelectorAll(".modal-footer .btn")[2]); // Discard
+ await nextTick();
+ assert.containsOnce(target, ".o_list_view", "should be open list view");
+ await click(target.querySelector(".o_control_panel .breadcrumb-item a"));
+ assert.containsNone(
+ target,
+ ".o_field_boolean input:checked",
+ "checkbox should not be checked"
+ );
+
+ await click(target.querySelector(".custom-control input"));
+ await click(target.querySelector("button[name='4']"));
+ assert.containsOnce(document.body, ".modal", "should open a warning dialog");
+
+ await click(target.querySelectorAll(".modal-footer .btn")[1]); // Stay Here
+ assert.containsOnce(target, ".o_form_view", "should be remain on form view");
+
+ await click(target.querySelector(".o_form_button_save")); // Form Save button
+ assert.containsNone(document.body, ".modal", "should not open a warning dialog");
+ assert.notOk(target.querySelector(".custom-control input").disabled); // Everything must stay in edit
+
+ await click(target.querySelector(".custom-control input"));
+ await click(target.querySelector(".o_form_button_cancel")); // Form Discard button
+ assert.containsNone(document.body, ".modal", "should not open a warning dialog");
+ }
+ );
+
+ QUnit.test(
+ "clicking on a button with noSaveDialog will not show discard warning",
+ async function (assert) {
+ assert.expect(4);
+
+ serverData.actions = {
+ 1: {
+ id: 1,
+ name: "Settings view",
+ res_model: "res.config.settings",
+ type: "ir.actions.act_window",
+ views: [[1, "form"]],
+ },
+ 4: {
+ id: 4,
+ name: "Other action",
+ res_model: "task",
+ type: "ir.actions.act_window",
+ views: [[2, "list"]],
+ },
+ };
+
+ serverData.views = {
+ "res.config.settings,1,form": `
+
`,
+ "task,2,list": '
',
+ "res.config.settings,false,search": "
",
+ "task,false,search": "
",
+ };
+
+ const webClient = await createWebClient({ serverData });
+
+ await doAction(webClient, 1);
+ assert.containsNone(
+ target,
+ ".o_field_boolean input:checked",
+ "checkbox should not be checked"
+ );
+
+ await click(target.querySelector(".custom-control input"));
+ assert.containsOnce(
+ target,
+ ".o_field_boolean input:checked",
+ "checkbox should be checked"
+ );
+
+ await click(target.querySelector("button[name='4']"));
+ assert.containsNone(document.body, ".modal", "should not open a warning dialog");
+
+ assert.containsOnce(target, ".o_list_view", "should be open list view");
+ }
+ );
+
+ QUnit.test("settings view does not display o_not_app settings", async function (assert) {
+ await makeView({
+ type: "form",
+ resModel: "res.config.settings",
+ serverData,
+ arch: `
+
`,
+ });
+
+ assert.deepEqual(
+ [...target.querySelectorAll(".app_name")].map((x) => x.textContent),
+ ["CRM"]
+ );
+
+ assert.deepEqual(
+ [...target.querySelectorAll(".settings .o_form_label")].map((x) => x.textContent),
+ ["Bar"]
+ );
+ });
+
+ QUnit.test("settings view shows a message if there are changes", async function (assert) {
+ await makeView({
+ type: "form",
+ resModel: "res.config.settings",
+ serverData,
+ arch: `
+
`,
+ });
+
+ assert.containsNone(
+ target,
+ ".o_field_boolean input:checked",
+ "checkbox should not be checked"
+ );
+ assert.containsNone(target, ".o_dirty_warning", "warning message should not be shown");
+ await click(target.querySelector(".custom-control input[id=bar]"));
+ assert.containsOnce(target, ".o_field_boolean input:checked", "checkbox should be checked");
+ assert.containsOnce(target, ".o_dirty_warning", "warning message should be shown");
+ });
+
+ QUnit.test(
+ "settings view shows a message if there are changes even if the save failed",
+ async function (assert) {
+ const self = this;
+ self.alreadySavedOnce = false;
+ await makeView({
+ type: "form",
+ resModel: "res.config.settings",
+ serverData,
+ mockRPC: function (route, args) {
+ if (args.method === "create" && !self.alreadySavedOnce) {
+ self.alreadySavedOnce = true;
+ //fail on first create
+ return Promise.reject({});
+ }
+ },
+ arch: `
+
`,
+ });
+
+ await click(target.querySelector("input[id=bar]"));
+ assert.containsOnce(target, ".o_dirty_warning", "warning message should be shown");
+ await click(target.querySelector(".o_form_button_save"));
+ assert.containsOnce(target, ".o_dirty_warning", "warning message should be shown");
+ }
+ );
+
+ QUnit.test(
+ "execute action from settings view with several actions in the breadcrumb",
+ async function (assert) {
+ // This commit fixes a race condition, that's why we artificially slow down a read rpc
+ assert.expect(4);
+
+ serverData.actions = {
+ 1: {
+ id: 1,
+ name: "First action",
+ res_model: "task",
+ type: "ir.actions.act_window",
+ views: [[1, "list"]],
+ },
+ 2: {
+ id: 2,
+ name: "Settings view",
+ res_model: "res.config.settings",
+ type: "ir.actions.act_window",
+ views: [[2, "form"]],
+ },
+ 3: {
+ id: 3,
+ name: "Other action",
+ res_model: "task",
+ type: "ir.actions.act_window",
+ views: [[3, "list"]],
+ },
+ };
+
+ serverData.views = {
+ "task,1,list": '
',
+ "res.config.settings,2,form": `
+
`,
+ "task,3,list": '
',
+ "res.config.settings,false,search": "
",
+ "task,false,search": "
",
+ };
+
+ let def;
+ const mockRPC = async (route, args) => {
+ if (args.method === "web_search_read") {
+ await def; // slow down reload of settings view
+ }
+ };
+
+ const webClient = await createWebClient({ serverData, mockRPC });
+ await doAction(webClient, 1);
+ assert.strictEqual($(target).find(".breadcrumb").text(), "First action");
+
+ await doAction(webClient, 2);
+ assert.strictEqual($(target).find(".breadcrumb").text(), "First actionSettings");
+
+ def = makeDeferred();
+ await click(target.querySelector('button[name="3"]'));
+ assert.strictEqual($(target).find(".breadcrumb").text(), "First actionSettings");
+
+ def.resolve();
+ await nextTick();
+ assert.strictEqual(
+ $(target).find(".breadcrumb").text(),
+ "First actionSettingsOther action"
+ );
+ }
+ );
+
+ QUnit.test("settings can contain one2many fields", async function (assert) {
+ await makeView({
+ type: "form",
+ resModel: "res.config.settings",
+ serverData,
+ arch: `
+
`,
+ });
+
+ await click(target.querySelector(".o_field_x2many_list_row_add a"));
+ await editInput(target, ".modal-body input", "Added Task");
+ await click(target.querySelector(".modal-footer .btn.o_form_button_save"));
+
+ assert.strictEqual(
+ target.querySelector("table.o_list_table tr.o_data_row").textContent,
+ "Added Task",
+ "The one2many relation item should have been added"
+ );
+ });
+
+ QUnit.test(
+ 'call "call_button/execute" when clicking on a button in dirty settings',
+ async function (assert) {
+ assert.expect(6);
+
+ serverData.actions = {
+ 1: {
+ id: 1,
+ name: "Settings view",
+ res_model: "res.config.settings",
+ type: "ir.actions.act_window",
+ views: [[1, "form"]],
+ },
+ 4: {
+ id: 4,
+ name: "Other Action",
+ res_model: "task",
+ type: "ir.actions.act_window",
+ views: [[false, "list"]],
+ },
+ };
+
+ serverData.views = {
+ "res.config.settings,1,form": `
+
+
+
+ `,
+ "res.config.settings,false,search": "
",
+ "task,false,list": "
",
+ "task,false,search": "
",
+ };
+
+ const mockRPC = (route, args) => {
+ if (route === "/web/dataset/call_button" && args.method === "execute") {
+ assert.step("execute");
+ return true;
+ } else if (args.method === "create") {
+ assert.step("create");
+ }
+ };
+
+ const webClient = await createWebClient({ serverData, mockRPC });
+
+ await doAction(webClient, 1);
+ assert.containsNone(
+ target,
+ ".o_field_boolean input:checked",
+ "checkbox should not be checked"
+ );
+
+ await click(target.querySelector(".custom-control input"));
+ assert.containsOnce(
+ target,
+ ".o_field_boolean input:checked",
+ "checkbox should be checked"
+ );
+
+ await click(target.querySelector('button[name="4"]'));
+ assert.containsOnce(target, ".modal", "should open a warning dialog");
+
+ await click(target.querySelector(".modal-footer .btn-primary"));
+ assert.verifySteps([
+ "create", // saveRecord from modal
+ "execute", // execute_action
+ ]);
+ }
+ );
+ QUnit.test("Discard button clean the settings view", async function (assert) {
+ assert.expect(5);
+
+ serverData.actions = {
+ 1: {
+ id: 1,
+ name: "Settings view",
+ res_model: "res.config.settings",
+ type: "ir.actions.act_window",
+ views: [[1, "form"]],
+ },
+ };
+
+ serverData.views = {
+ "res.config.settings,1,form": `
+
+
+
+ `,
+ "res.config.settings,false,search": "
",
+ "task,false,list": "
",
+ "task,false,search": "
",
+ };
+
+ const mockRPC = (route, args) => {
+ if (route === "/web/dataset/call_button" && args.method === "cancel") {
+ assert.step("cancel");
+ return Promise.resolve({
+ name: "Settings view",
+ res_model: "res.config.settings",
+ type: "ir.actions.act_window",
+ target: "inline",
+ views: [[false, "form"]],
+ });
+ }
+ };
+
+ const webClient = await createWebClient({ serverData, mockRPC });
+
+ await doAction(webClient, 1);
+ assert.containsNone(
+ target,
+ ".o_field_boolean input:checked",
+ "checkbox should not be checked"
+ );
+
+ await click(target.querySelector(".custom-control input"));
+ assert.containsOnce(target, ".o_field_boolean input:checked", "checkbox should be checked");
+
+ await click(target.querySelector(".o_form_button_cancel"));
+
+ assert.containsNone(
+ target,
+ ".o_field_boolean input:checked",
+ "checkbox should not be checked"
+ );
+
+ assert.verifySteps(["cancel"]);
+ });
+
+ QUnit.test("Settings Radio widget: show and search", async function (assert) {
+ serverData.models["res.config.settings"].fields.product_id = {
+ string: "Product",
+ type: "many2one",
+ relation: "product",
+ };
+ serverData.models.product = {
+ fields: {
+ name: { string: "Product Name", type: "char" },
+ },
+ records: [
+ {
+ id: 37,
+ display_name: "xphone",
+ },
+ {
+ id: 41,
+ display_name: "xpad",
+ },
+ ],
+ };
+ await makeView({
+ type: "form",
+ resModel: "res.config.settings",
+ serverData,
+ arch: `
+
+
+ `,
+ });
+
+ assert.deepEqual(
+ [...target.querySelectorAll(".o_radio_item label")].map(
+ (x) => x.parentElement.textContent
+ ),
+ ["xphone", "xpad"]
+ );
+
+ await editSearch(target, "xp");
+ await execTimeouts();
+ assert.containsN(target, ".highlighter", 2, "should have 2 options highlighted");
+ assert.deepEqual(
+ [...target.querySelectorAll(".highlighter")].map((x) => x.parentElement.textContent),
+ ["xphone", "xpad"]
+ );
+
+ await editSearch(target, "xph");
+ await execTimeouts();
+ assert.containsN(target, ".highlighter", 1, "should have only one highlighted");
+ assert.deepEqual(
+ [...target.querySelectorAll(".highlighter")].map((x) => x.parentElement.textContent),
+ ["xphone"]
+ );
+ });
+});
diff --git a/addons/website/static/src/js/backend/res_config_settings.js b/addons/website/static/src/js/backend/res_config_settings.js
index 776e0995a34..20a94ae1589 100644
--- a/addons/website/static/src/js/backend/res_config_settings.js
+++ b/addons/website/static/src/js/backend/res_config_settings.js
@@ -1,51 +1,13 @@
odoo.define('website.settings', function (require) {
-const BaseSettingController = require('base.settings').Controller;
const core = require('web.core');
const Dialog = require('web.Dialog');
const FieldBoolean = require('web.basic_fields').FieldBoolean;
const fieldRegistry = require('web.field_registry');
-const FormController = require('web.FormController');
const QWeb = core.qweb;
const _t = core._t;
-BaseSettingController.include({
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Bypasses the discard confirmation dialog when going to a website because
- * the target website will be the one selected and when selecting a theme
- * because the theme will be installed on the selected website.
- *
- * Without this override, it is impossible to go to a website other than the
- * first because discarding will revert it back to the default value.
- *
- * Without this override, it is impossible to edit robots.txt website other than the
- * first because discarding will revert it back to the default value.
- *
- * Without this override, it is impossible to submit sitemap to google other than for the
- * first website because discarding will revert it back to the default value.
- *
- * Without this override, it is impossible to install a theme on a website
- * other than the first because discarding will revert it back to the
- * default value.
- *
- * @override
- */
- _onButtonClicked: function (ev) {
- if (ev.data.attrs.name === 'action_open_robots'
- || ev.data.attrs.name === 'action_ping_sitemap') {
- FormController.prototype._onButtonClicked.apply(this, arguments);
- } else {
- this._super.apply(this, arguments);
- }
- },
-});
-
const WebsiteCookiesbarField = FieldBoolean.extend({
xmlDependencies: ['/website/static/src/xml/website.res_config_settings.xml'],
diff --git a/addons/website/views/res_config_settings_views.xml b/addons/website/views/res_config_settings_views.xml
index 7f47773eb21..569fa26a6ee 100644
--- a/addons/website/views/res_config_settings_views.xml
+++ b/addons/website/views/res_config_settings_views.xml
@@ -213,7 +213,7 @@
Robots.txt: This file tells to search engine crawlers which pages or files they can or can't request from your site.