[REF] base,web,*: convert settings form view to owl
*hr_timesheet,point_of_sale,website This commit moves the settings form view implementation from base to web, and converts it to owl. The setting's search has been improved to take into account more elements. Before, it was possible to only search on the field's labels. Now, we can also search on the field's description, and the titles of setting's group. Part-of: odoo/odoo#78221 Co-authored-by: Samuel Degueldre <sad@odoo.com>
This commit is contained in:
committed by
Géry Debongnie
co-authored by
Samuel Degueldre
parent
350ad39d51
commit
2c9db825c1
@@ -47,7 +47,7 @@ up a management by affair.
|
||||
'uninstall_hook': '_uninstall_hook',
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'hr_timesheet/static/src/scss/timesheets_task_form.scss',
|
||||
'hr_timesheet/static/src/**/*.scss',
|
||||
'hr_timesheet/static/src/**/*.js',
|
||||
('remove', 'hr_timesheet/static/src/js/*_legacy.js')
|
||||
],
|
||||
@@ -58,7 +58,7 @@ up a management by affair.
|
||||
'hr_timesheet/static/tests/**/*',
|
||||
],
|
||||
'web.assets_qweb': [
|
||||
'hr_timesheet/static/src/xml/**/*',
|
||||
'hr_timesheet/static/src/**/*.xml',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { isAndroid, isIOS } from "@web/core/browser/feature_detection";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
const configs = {
|
||||
apple: {
|
||||
alt: "Apple App Store",
|
||||
className: "o_config_app_store",
|
||||
src: "project/static/src/img/app_store.png",
|
||||
storeUrl: "https://apps.apple.com/be/app/awesome-timesheet/id1078657549",
|
||||
},
|
||||
google: {
|
||||
alt: "Google Play Store",
|
||||
className: "o_config_play_store",
|
||||
src: "project/static/src/img/play_store.png",
|
||||
storeUrl: "https://play.google.com/store/apps/details?id=com.odoo.OdooTimesheets",
|
||||
},
|
||||
};
|
||||
|
||||
class AppStoreWidget extends Component {
|
||||
setup() {
|
||||
this.dialog = useService("dialog");
|
||||
this.config = configs[this.props.node.attrs.type];
|
||||
}
|
||||
|
||||
openQRDialog() {
|
||||
if (
|
||||
(this.props.node.attrs.type === "apple" && isIOS()) ||
|
||||
(this.props.node.attrs.type === "google" && isAndroid())
|
||||
) {
|
||||
this.env.services.action.doAction({
|
||||
type: "ir.actions.act_url",
|
||||
url: this.config.storeUrl,
|
||||
});
|
||||
} else {
|
||||
this.dialog.add(AppStoreQRDialog, { url: this.config.storeUrl });
|
||||
}
|
||||
}
|
||||
}
|
||||
AppStoreWidget.template = "hr_timesheet.AppStoreWidget";
|
||||
|
||||
class AppStoreQRDialog extends Component {
|
||||
setup() {
|
||||
this.title = _t("Download our app");
|
||||
this.qrCodeUrl = `/report/barcode/?barcode_type=QR&value=${this.props.url}&width=256&height=256&humanreadable=1`;
|
||||
}
|
||||
|
||||
viewApp() {
|
||||
window.open(this.props.url, "_blank");
|
||||
}
|
||||
}
|
||||
AppStoreQRDialog.components = { Dialog };
|
||||
AppStoreQRDialog.template = "hr_timesheet.AppStoreQRDialog";
|
||||
|
||||
registry.category("view_widgets").add("hr_timesheet.app_store_widget", AppStoreWidget);
|
||||
@@ -0,0 +1,3 @@
|
||||
.o_widget.o_widget_hr_timesheet\.app_store_widget {
|
||||
display: contents;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr_timesheet.AppStoreWidget" owl="1">
|
||||
<img t-att-alt="config.alt" class="img img-fluid mt-1" t-att-class="config.className" style="height: 85% !important; cursor: pointer;" t-att-src="config.src" t-on-click="openQRDialog"/>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_timesheet.AppStoreQRDialog" owl="1">
|
||||
<Dialog title="title">
|
||||
<div style="text-align:center;">
|
||||
<h3>Scan this QR code to get the Awesome Timesheet app:</h3>
|
||||
<br/>
|
||||
<br/>
|
||||
<img class="border border-dark rounded" t-att-src="qrCodeUrl"/>
|
||||
</div>
|
||||
<t t-set-slot="footer">
|
||||
<button class="btn btn-primary" t-on-click="viewApp">View App</button>
|
||||
<button class="btn" t-on-click="props.close">Discard</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -4,8 +4,6 @@ odoo.define('hr_timesheet.res.config.form', function (require) {
|
||||
const core = require('web.core');
|
||||
const config = require('web.config');
|
||||
const Dialog = require('web.Dialog');
|
||||
const viewRegistry = require('web.view_registry');
|
||||
const BaseSetting = require('base.settings');
|
||||
const QWeb = core.qweb;
|
||||
|
||||
const _t = core._t;
|
||||
@@ -59,16 +57,6 @@ odoo.define('hr_timesheet.res.config.form', function (require) {
|
||||
};
|
||||
|
||||
|
||||
var TimesheetConfigFormRenderer = BaseSetting.Renderer.extend(TimesheetConfigQRCodeMixin);
|
||||
const BaseSettingView = viewRegistry.get('base_settings');
|
||||
var TimesheetConfigFormView = BaseSettingView.extend({
|
||||
config: _.extend({}, BaseSettingView.prototype.config, {
|
||||
Renderer : TimesheetConfigFormRenderer,
|
||||
}),
|
||||
});
|
||||
|
||||
viewRegistry.add('hr_timesheet_config_form', TimesheetConfigFormView);
|
||||
|
||||
return {TimesheetConfigQRCodeDialog, TimesheetConfigQRCodeMixin, TimesheetConfigFormRenderer, TimesheetConfigFormView};
|
||||
return {TimesheetConfigQRCodeDialog, TimesheetConfigQRCodeMixin};
|
||||
|
||||
});
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="groups_id" eval="[(4, ref('hr_timesheet.group_timesheet_manager'), 0)]"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="attributes">
|
||||
<attribute name="js_class">hr_timesheet_config_form</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('settings')]" position="inside">
|
||||
<div class="app_settings_block" data-string="Timesheets" string="Timesheets" data-key="hr_timesheet" groups="hr_timesheet.group_timesheet_manager" id="timesheets">
|
||||
<h2>Time Encoding</h2>
|
||||
@@ -61,10 +58,10 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-3 pe-0">
|
||||
<img alt="Apple App Store" class="img img-fluid o_config_app_store mt-1" style="height: 85% !important; cursor: pointer;" src="project/static/src/img/app_store.png"/>
|
||||
<widget name="hr_timesheet.app_store_widget" type="apple"/>
|
||||
</div>
|
||||
<div class="col-lg-3 pe-0">
|
||||
<img alt="Google Play Store" class="img img-fluid o_config_play_store mt-1" style="height: 85% !important; cursor: pointer;" src="project/static/src/img/play_store.png"/>
|
||||
<widget name="hr_timesheet.app_store_widget" type="google"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
odoo.define('point_of_sale.res_config_settings', function (require) {
|
||||
'use strict';
|
||||
|
||||
const ResConfigSettings = require('base.settings');
|
||||
|
||||
ResConfigSettings.Renderer.include({
|
||||
_searchModule: function (module) {
|
||||
const isModuleVisible = this._super.apply(this, arguments);
|
||||
if (isModuleVisible && module.key === 'point_of_sale') {
|
||||
// Find all field elements (.o_field_widget) from shown (:not(.o_hidden)) setting boxes.
|
||||
// Then filter those with name attributes that starts with 'pos_'.
|
||||
const posConfigFields = module.settingView
|
||||
.find('.o_setting_box:not(.o_hidden) div.o_field_widget')
|
||||
.filter(function (_, el) {
|
||||
const name = $(el).attr('name');
|
||||
return name ? name.startsWith('pos_') : false;
|
||||
});
|
||||
|
||||
// Show the pos_config_id field if there are shown 'pos_*' fields.
|
||||
// But the search header has ugly bottom margin when the pos_config_id field is shown,
|
||||
// so we remove the bottom margin when the field is shown.
|
||||
const posSearchHeader = module.settingView.find('.settingSearchHeader');
|
||||
if (posConfigFields.length > 0) {
|
||||
module.settingView.find('div#pos_header .o_setting_box').removeClass('o_hidden');
|
||||
posSearchHeader.addClass('mb-0');
|
||||
} else {
|
||||
posSearchHeader.removeClass('mb-0');
|
||||
}
|
||||
}
|
||||
return isModuleVisible;
|
||||
},
|
||||
});
|
||||
|
||||
ResConfigSettings.Controller.include({
|
||||
_startRenderer() {
|
||||
return this._super.apply(this, arguments).then(result => {
|
||||
// Force tooltip to elements with `pos-data-bs-toggle="tooltip"` attribute.
|
||||
// We made it specific to ensure we don't interfere with other data-bs-toggle="tooltip" in the form.
|
||||
this.renderer.$('[pos-data-bs-toggle="tooltip"]').tooltip();
|
||||
return result;
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return ResConfigSettings;
|
||||
});
|
||||
@@ -92,7 +92,6 @@ This module provides the core of the Odoo Web Client.
|
||||
('include', 'web._assets_bootstrap'),
|
||||
|
||||
'base/static/src/css/modules.css',
|
||||
'base/static/src/js/res_config_settings.js',
|
||||
|
||||
'web/static/src/core/utils/transitions.scss',
|
||||
'web/static/src/core/**/*',
|
||||
@@ -526,7 +525,6 @@ This module provides the core of the Odoo Web Client.
|
||||
("include", "web.assets_backend_legacy_lazy"),
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'base/static/tests/base_settings_tests.js',
|
||||
'web/static/tests/env_tests.js',
|
||||
'web/static/tests/core/**/*.js',
|
||||
'web/static/tests/search/**/*.js',
|
||||
|
||||
@@ -1543,21 +1543,6 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="BaseSetting.Tabs">
|
||||
<t t-foreach="tabItems" t-as="tab" t-key="tab_index">
|
||||
<div class="tab" t-attf-data-key="#{tab.key}" role="tab">
|
||||
<div class="icon d-none d-md-block" t-attf-style="background : url('#{imgurl}') no-repeat center;background-size:contain;"/> <span class="app_name"><t t-esc="tab.string"/></span>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="BaseSetting.SearchHeader">
|
||||
<div class="settingSearchHeader o_hidden h4" role="search">
|
||||
<img class="icon" t-att-src="imgurl" alt="Search"></img>
|
||||
<span class="appName"><t t-esc="string"/></span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="AttachDocument">
|
||||
<button t-attf-class="btn o_attachment_button #{widget.node.attrs.highlight?'btn-primary':'btn-secondary'}">
|
||||
<span class="o_attach_document"><t t-esc="widget.node.attrs.string"/></span>
|
||||
|
||||
@@ -12,16 +12,7 @@ export class RadioField extends Component {
|
||||
}
|
||||
|
||||
get items() {
|
||||
switch (this.props.record.fields[this.props.name].type) {
|
||||
case "selection":
|
||||
return this.props.record.fields[this.props.name].selection;
|
||||
case "many2one": {
|
||||
const value = this.props.record.preloadedData[this.props.name] || [];
|
||||
return value.map((item) => [item.id, item.display_name]);
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
return RadioField.getItems(this.props.name, this.props.record);
|
||||
}
|
||||
get string() {
|
||||
return this.props.record.activeFields[this.props.name].string;
|
||||
@@ -73,6 +64,19 @@ RadioField.extractProps = ({ attrs }) => {
|
||||
};
|
||||
};
|
||||
|
||||
RadioField.getItems = (fieldName, record) => {
|
||||
switch (record.fields[fieldName].type) {
|
||||
case "selection":
|
||||
return record.fields[fieldName].selection;
|
||||
case "many2one": {
|
||||
const value = record.preloadedData[fieldName] || [];
|
||||
return value.map((item) => [item.id, item.display_name]);
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
registry.category("fields").add("radio", RadioField);
|
||||
|
||||
export async function preloadRadio(orm, record, fieldName) {
|
||||
|
||||
@@ -205,7 +205,7 @@ export class FormController extends Component {
|
||||
});
|
||||
|
||||
onRendered(() => {
|
||||
this.env.config.setDisplayName(this.model.root.data.display_name || this.env._t("New"));
|
||||
this.env.config.setDisplayName(this.displayName());
|
||||
});
|
||||
|
||||
const { autofocusFieldId, disableAutofocus } = this.archInfo;
|
||||
@@ -234,6 +234,10 @@ export class FormController extends Component {
|
||||
this.setupTranslateAlert();
|
||||
}
|
||||
|
||||
displayName() {
|
||||
return this.model.root.data.display_name || this.env._t("New");
|
||||
}
|
||||
|
||||
beforeUnload() {
|
||||
return this.model.root.urgentSave();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ const BUTTON_CLICK_PARAMS = [
|
||||
// WOWL SAD: is adding the support for debounce attribute here justified or should we
|
||||
// just override compileButton in kanban compiler to add the debounce?
|
||||
"debounce",
|
||||
// WOWL JPP: is adding the support for not oppening the dialog of confirmation in the settings view
|
||||
// This should be refactor someday
|
||||
"noSaveDialog",
|
||||
];
|
||||
const BUTTON_STRING_PROPS = ["string", "size", "title", "icon", "id"];
|
||||
const INTERP_REGEXP = /(\{\{|#\{)(.*?)(\}{1,2})/g;
|
||||
|
||||
@@ -23,6 +23,13 @@ export class Widget extends Component {
|
||||
return findWidgetComponent(this.props.name);
|
||||
}
|
||||
|
||||
get classNames() {
|
||||
return {
|
||||
o_widget: true,
|
||||
[`o_widget_${this.props.name}`]: true,
|
||||
[this.props.className]: Boolean(this.props.className),
|
||||
};
|
||||
}
|
||||
get widgetProps() {
|
||||
const { record, node: rawNode, options, readonly } = this.props;
|
||||
const node = rawNode ? decodeObjectForTemplate(rawNode) : {};
|
||||
@@ -30,7 +37,7 @@ export class Widget extends Component {
|
||||
}
|
||||
}
|
||||
Widget.template = xml/*xml*/ `
|
||||
<div class="o_widget" t-att-class="props.className" t-att-style="props.style">
|
||||
<div t-att-class="classNames" t-att-style="props.style">
|
||||
<t t-component="Widget" t-props="widgetProps" />
|
||||
</div>`;
|
||||
|
||||
|
||||
+18
@@ -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 };
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="web.FormLabelHighlightText" owl="1">
|
||||
<label class="o_form_label" t-att-for="props.id" t-att-class="className">
|
||||
<HighlightText originalText="props.string"/>
|
||||
</label>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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";
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="web.HighlightText" owl="1">
|
||||
<t t-foreach="splitText" t-as="name" t-key="name_index">
|
||||
<b t-if="name_index % 2" t-out="name" t-att-class="constructor.highlightClass"/>
|
||||
<t t-else="" t-out="name"/>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
+16
@@ -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);
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="web.SettingsRadioField" t-inherit="web.RadioField" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//label" position="replace">
|
||||
<FormLabelHighlightText className="'custom-control-label'" id="`${id}_${item[0]}`" string="item[1]"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -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";
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="web.SettingsApp" owl="1">
|
||||
<div class="app_settings_block" t-if="props.selectedTab === props.key or state.search.value.length !== 0" t-att-string="props.string" t-att-data-key="props.key">
|
||||
<div class="settingSearchHeader h4" t-if="state.search.value.length !== 0" role="search">
|
||||
<img class="icon" t-att-src="props.imgurl" alt="Search"></img>
|
||||
<span class="appName"><t t-esc="props.string"/></span>
|
||||
</div>
|
||||
<t t-slot="default"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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";
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="web.SettingsPage" owl="1">
|
||||
<div class="settings_tab">
|
||||
<t t-foreach="props.modules" t-as="module" t-key="module.key">
|
||||
<div class="tab" t-if="!module.isVisible" t-att-class="(state.selectedTab === module.key and state.search.value.length === 0) ? 'selected': ''" t-att-data-key="module.key" role="tab" t-on-click="() => this.onSettingTabClick(module.key)">
|
||||
<div class="icon d-none d-md-block" t-attf-style="background : url('{{module.imgurl}}') no-repeat center;background-size:contain;"/> <span class="app_name"><t t-esc="module.string"/></span>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="settings">
|
||||
<t t-slot="NoContentHelper" t-if="props.slots['NoContentHelper'].isVisible"/>
|
||||
<t t-slot="default" selectedTab="state.selectedTab"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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 },
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="web.SettingsConfirmationDialog" owl="1">
|
||||
<Dialog size="'md'" title="props.title">
|
||||
<t t-esc="props.body" />
|
||||
<t t-set-slot="footer" owl="1">
|
||||
<button class="btn btn-primary" t-on-click="_confirm">
|
||||
Save
|
||||
</button>
|
||||
<button class="btn btn-secondary" t-on-click="_stayHere">
|
||||
Stay Here
|
||||
</button>
|
||||
<button class="btn btn-secondary" t-on-click="_cancel">
|
||||
Discard
|
||||
</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="web.SettingsFormView" t-inherit="web.FormView" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="./div[@t-ref='root']" position="attributes">
|
||||
<attribute name="class">o-settings-form-view</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//Layout" position="inside">
|
||||
<t t-set-slot="control-panel-top-right">
|
||||
<div class="o_cp_searchview" role="search">
|
||||
<div class="o_searchview" role="search" aria-autocomplete="list">
|
||||
<i class="o_searchview_icon oi oi-search" role="img" aria-label="Search..." title="Search..." />
|
||||
<div class="o_searchview_input_container">
|
||||
<input type="text" class="o_searchview_input" accesskey="Q" placeholder="Search..." role="searchbox" t-model="searchState.value" t-ref="autofocus"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//Layout/t[@t-component='props.Renderer']" position="attributes">
|
||||
<attribute name="initialApp">initialApp</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//Layout/t[@t-component='props.Renderer']" position="inside">
|
||||
<t t-set-slot="NoContentHelper" isVisible="state.displayNoContent">
|
||||
<t t-call="web.NoContentHelper">
|
||||
<t t-set="title" t-value="'No setting found'"/>
|
||||
<t t-set="description" t-value="'Try searching for another keyword'"/>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="web.SettingsFormView.Buttons" t-inherit="web.FormView.Buttons" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[hasclass('o_form_buttons_edit')]" position="inside">
|
||||
<span t-if="model.root.isDirty" class="text-muted ms-2 o_dirty_warning">Unsaved changes</span>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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'],
|
||||
|
||||
|
||||
@@ -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.<br/>
|
||||
</div>
|
||||
<div class="mt4">
|
||||
<button type="object" name="action_open_robots" string="Edit robots.txt" class="btn-link" icon="fa-android"/>
|
||||
<button type="object" name="action_open_robots" string="Edit robots.txt" class="btn-link" icon="fa-android" noSaveDialog="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,7 +224,7 @@
|
||||
Sitemap.xml: Help search engine crawlers to find out what pages are present and which have recently changed, and to crawl your site accordingly. This file is automatically generated by Odoo.
|
||||
</div>
|
||||
<div class="mt4">
|
||||
<button type="object" name="action_ping_sitemap" string="Submit sitemap to Google" class='btn-link' icon="fa-google"/>
|
||||
<button type="object" name="action_ping_sitemap" string="Submit sitemap to Google" class='btn-link' icon="fa-google" noSaveDialog="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,478 +0,0 @@
|
||||
odoo.define('base.settings', function (require) {
|
||||
"use strict";
|
||||
|
||||
var BasicModel = require('web.BasicModel');
|
||||
var core = require('web.core');
|
||||
var FormView = require('web.FormView');
|
||||
var FormController = require('web.FormController');
|
||||
var FormRenderer = require('web.FormRenderer');
|
||||
var view_registry = require('web.view_registry');
|
||||
const Dialog = require('web.Dialog');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var BaseSettingRenderer = FormRenderer.extend({
|
||||
events: _.extend({}, FormRenderer.prototype.events, {
|
||||
'click .tab': '_onSettingTabClick',
|
||||
'keyup .searchInput': '_onKeyUpSearch',
|
||||
}),
|
||||
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.activeView = false;
|
||||
this.activeTab = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* overridden to show a message, informing user that there are changes
|
||||
*/
|
||||
confirmChange: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
if (!self.$(".o_dirty_warning").length) {
|
||||
self.$('.o_statusbar_buttons')
|
||||
.append($('<span/>', {text: _t("Unsaved changes"), class: 'text-muted ms-2 o_dirty_warning'}))
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
this._super.apply(this, arguments);
|
||||
// set default focus on searchInput
|
||||
this.searchInput.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
displayTranslationAlert: function () {
|
||||
// Translation alerts are disabled for res.config.settings:
|
||||
// those are designed to warn user to translate field he just changed, but
|
||||
// * in res.config.settings almost all fields marked as changed (because
|
||||
// it's not a usual record and all values are set via default_get)
|
||||
// * page is reloaded after saving, so those alerts would be visible
|
||||
// only for short time after clicking Save
|
||||
},
|
||||
/**
|
||||
* initialize modules list.
|
||||
* remove module that restricted in groups
|
||||
* data contains
|
||||
* {
|
||||
* key: moduel key
|
||||
* string: moduel string
|
||||
* imgurl: icon url
|
||||
* }
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_initModules: function () {
|
||||
var self = this;
|
||||
this.modules = [];
|
||||
_.each(this.$('.app_settings_block'), function (settingView, index) {
|
||||
var group = !$(settingView).hasClass('o_invisible_modifier');
|
||||
var isNotApp = $(settingView).hasClass('o_not_app');
|
||||
if(group && !isNotApp) {
|
||||
var data = $(settingView).data();
|
||||
data.string = $(settingView).attr('string') || data.string;
|
||||
self.modules.push({
|
||||
key: data.key,
|
||||
string: data.string,
|
||||
imgurl: self._getAppIconUrl(data.key),
|
||||
});
|
||||
} else {
|
||||
$(settingView).remove();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* initialize searchtext variable
|
||||
* initialize jQuery search input element
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_initSearch: function () {
|
||||
this.searchInput = this.$('.searchInput');
|
||||
if (this.searchText) {
|
||||
this.searchInput.val(this.searchText);
|
||||
this._onKeyUpSearch();
|
||||
} else {
|
||||
this.searchText = "";
|
||||
}
|
||||
},
|
||||
/**
|
||||
* find current app index in modules
|
||||
*
|
||||
*/
|
||||
_currentAppIndex: function () {
|
||||
var self = this;
|
||||
var index = _.findIndex(this.modules, function (module) {
|
||||
return module.key === self.activeSettingTab;
|
||||
});
|
||||
return index;
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
* @param {string} module
|
||||
* @returns {string} icon url
|
||||
*/
|
||||
_getAppIconUrl: function (module) {
|
||||
return module === "general_settings" ? "/base/static/description/settings.png" : "/"+module+"/static/description/icon.png";
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
* @param {string} imgurl
|
||||
* @param {string} string(moduel name)
|
||||
* @returns {object}
|
||||
*/
|
||||
_getSearchHeader: function (imgurl, string) {
|
||||
return $(QWeb.render('BaseSetting.SearchHeader', {
|
||||
imgurl: imgurl,
|
||||
string: string
|
||||
}));
|
||||
},
|
||||
/**
|
||||
* move to selected setting
|
||||
*
|
||||
* @private
|
||||
* @param {int} index
|
||||
*/
|
||||
_moveToTab: function (index) {
|
||||
this.currentIndex = !index || index === -1 ? 0 : (index === this.modules.length ? index - 1 : index);
|
||||
if (this.currentIndex !== -1) {
|
||||
if (this.activeView) {
|
||||
this.activeView.addClass("o_hidden");
|
||||
}
|
||||
if (this.activeTab) {
|
||||
this.activeTab.removeClass("selected");
|
||||
}
|
||||
var view = this.modules[this.currentIndex].settingView;
|
||||
var tab = this.$(".tab[data-key='" + this.modules[this.currentIndex].key + "']");
|
||||
view.removeClass("o_hidden");
|
||||
this.activeView = view;
|
||||
this.activeTab = tab;
|
||||
tab.addClass("selected");
|
||||
}
|
||||
},
|
||||
|
||||
_onSettingTabClick: function (event) {
|
||||
this.searchInput.focus();
|
||||
if (this.searchText.length > 0) {
|
||||
this.searchInput.val('');
|
||||
this.searchText = "";
|
||||
this._searchSetting();
|
||||
}
|
||||
var settingKey = this.$(event.currentTarget).data('key');
|
||||
this._moveToTab(_.findIndex(this.modules, function (m) {
|
||||
return m.key === settingKey;
|
||||
}));
|
||||
},
|
||||
|
||||
_onKeyUpSearch: function (event) {
|
||||
this.searchText = this.searchInput.val();
|
||||
this.activeTab.removeClass('selected');
|
||||
this._searchSetting();
|
||||
},
|
||||
/**
|
||||
* reset setting view
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_resetSearch: function () {
|
||||
this.searchInput.val("");
|
||||
_.each(this.modules, function (module) {
|
||||
module.settingView.addClass('o_hidden');
|
||||
module.settingView.find('h2, .o_setting_box, .o_setting_tip').removeClass('o_hidden');
|
||||
module.settingView.find('.settingSearchHeader').addClass('o_hidden');
|
||||
module.settingView.find('.o_settings_container').addClass('mt16').removeClass('mb-0');
|
||||
});
|
||||
this.activeTab.removeClass('o_hidden').addClass('selected');
|
||||
this.activeView.removeClass('o_hidden');
|
||||
},
|
||||
|
||||
_render: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function() {
|
||||
self._initModules();
|
||||
self._renderLeftPanel();
|
||||
self._initSearch();
|
||||
});
|
||||
},
|
||||
|
||||
_renderLeftPanel: function () {
|
||||
var self = this;
|
||||
_.each(this.modules, function (module) {
|
||||
module.settingView = self.$('.app_settings_block[data-key="' + module.key + '"]');
|
||||
module.settingView.addClass("o_hidden");
|
||||
module.settingView.prepend(self._getSearchHeader(module.imgurl, module.string));
|
||||
});
|
||||
this._renderTabs();
|
||||
this._moveToTab(this.currentIndex || this._currentAppIndex());
|
||||
},
|
||||
|
||||
_renderTabs: function () {
|
||||
var tabs = $(QWeb.render('BaseSetting.Tabs', {tabItems : this.modules}));
|
||||
tabs.appendTo(this.$(".settings_tab"));
|
||||
},
|
||||
/**
|
||||
* search setting in DOM
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_searchSetting: function () {
|
||||
this.count = 0;
|
||||
_.each(this.modules, module => this._searchModule(module));
|
||||
this.count === _.size(this.modules) ? this.$('.notFound').removeClass('o_hidden') : this.$('.notFound').addClass('o_hidden');
|
||||
if (this.searchText.length === 0) {
|
||||
this._resetSearch();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {*} module
|
||||
* @returns whether the module is visible or not
|
||||
*/
|
||||
_searchModule: function (module) {
|
||||
var self = this;
|
||||
let inVisibleCount = 0;
|
||||
let isModuleVisible = false;
|
||||
module.settingView.find('h2, .o_setting_box, .o_setting_tip').addClass('o_hidden');
|
||||
module.settingView.find('.settingSearchHeader').addClass('o_hidden');
|
||||
module.settingView.find('.o_settings_container').removeClass('mt16').addClass('mb-0');
|
||||
|
||||
const upperCasedSearchText = self.searchText.toUpperCase();
|
||||
const [matches, others] = _.partition(module.settingView.find(".o_form_label"),
|
||||
(e) => e.textContent.toUpperCase().includes(upperCasedSearchText));
|
||||
if (matches.length) {
|
||||
for (let result of matches) {
|
||||
const settingBox = $(result).closest('.o_setting_box');
|
||||
if (!settingBox.hasClass('o_invisible_modifier')) {
|
||||
settingBox.removeClass('o_hidden');
|
||||
self._wordHighlighter(result, upperCasedSearchText);
|
||||
} else {
|
||||
inVisibleCount++;
|
||||
}
|
||||
}
|
||||
isModuleVisible = inVisibleCount !== matches.length
|
||||
if (isModuleVisible) {
|
||||
module.settingView.find('.settingSearchHeader').removeClass('o_hidden');
|
||||
module.settingView.removeClass('o_hidden');
|
||||
}
|
||||
} else {
|
||||
++self.count;
|
||||
}
|
||||
others.filter(e => e.firstElementChild).forEach(e => self._removeHighlight(e));
|
||||
return isModuleVisible;
|
||||
},
|
||||
/**
|
||||
* highlight search word
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLElement} node
|
||||
* @param {string} upperCasedSearchText
|
||||
*/
|
||||
_wordHighlighter: function (node, upperCasedSearchText) {
|
||||
const text = node.textContent;
|
||||
const startIndex = text.toUpperCase().indexOf(upperCasedSearchText);
|
||||
const endIndex = startIndex + upperCasedSearchText.length;
|
||||
$(node).empty().append(
|
||||
document.createTextNode(text.substring(0, startIndex)),
|
||||
$('<span class="highlighter">').text(text.substring(startIndex, endIndex)),
|
||||
document.createTextNode(text.substring(endIndex))
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} node
|
||||
* @private
|
||||
*/
|
||||
_removeHighlight: function(node) {
|
||||
node.textContent = node.textContent;
|
||||
},
|
||||
});
|
||||
|
||||
var BaseSettingController = FormController.extend({
|
||||
custom_events: _.extend({}, FormController.prototype.custom_events, {
|
||||
button_clicked: '_onButtonClicked',
|
||||
}),
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.disableAutofocus = true;
|
||||
this.renderer.activeSettingTab = this.initialState.context.module;
|
||||
// discardingDef is used to ensure that we don't ask twice the user if
|
||||
// he wants to discard changes, when 'canBeDiscarded' is called several
|
||||
// times "in parallel"
|
||||
this.discardingDef = null;
|
||||
},
|
||||
/**
|
||||
* Settings view should always be in edit mode, so we have to override
|
||||
* default behaviour
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
willRestore: function () {
|
||||
this.mode = 'edit';
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @returns {Promise}
|
||||
*/
|
||||
canBeRemoved: function () {
|
||||
return this.discardChanges(undefined, {
|
||||
noAbandon: true,
|
||||
readonlyIfRealDiscard: true,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @param {string} recordId
|
||||
* @returns {Promise}
|
||||
*/
|
||||
canBeDiscarded: function (recordId) {
|
||||
if (this.discardingDef) {
|
||||
return this.discardingDef;
|
||||
}
|
||||
if (!this.isDirty(recordId)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
const message = _t('Would you like to save your changes?');
|
||||
this.discardingDef = new Promise((resolve, reject) => {
|
||||
const reset = () => {
|
||||
// enable buttons if user first save which fails because of required field missed
|
||||
// and then cancel confirmation dialog
|
||||
this._enableButtons();
|
||||
this.discardingDef = null;
|
||||
};
|
||||
const cancel = () => {
|
||||
reject();
|
||||
reset();
|
||||
};
|
||||
const dialog = Dialog.confirm(this, message, {
|
||||
title: _t('Unsaved changes'),
|
||||
buttons: [{
|
||||
text: _t('Save'),
|
||||
classes: 'btn-primary',
|
||||
click: async () => {
|
||||
this._disableButtons();
|
||||
try {
|
||||
// _onButtonClicked always saves the record even if
|
||||
// it's discarded. Here we need to save before
|
||||
// triggering the changes on the server.
|
||||
await this.saveRecord(recordId, {
|
||||
stayInEdit: true,
|
||||
});
|
||||
const record = this.model.get(recordId);
|
||||
this.trigger_up('execute_action', {
|
||||
action_data: {
|
||||
context: record.getContext({
|
||||
additionalContext: {},
|
||||
}),
|
||||
name: "execute",
|
||||
type: "object",
|
||||
},
|
||||
env: {
|
||||
context: record.getContext(),
|
||||
currentID: record.data.id,
|
||||
model: record.model,
|
||||
resIDs: record.res_ids,
|
||||
},
|
||||
on_success() {
|
||||
resolve(false);
|
||||
dialog.close();
|
||||
},
|
||||
on_fail() {
|
||||
cancel();
|
||||
dialog.close();
|
||||
},
|
||||
});
|
||||
} catch (_e) {
|
||||
cancel();
|
||||
dialog.close();
|
||||
}
|
||||
},
|
||||
}, {
|
||||
text: _t('Stay Here'),
|
||||
close: true,
|
||||
click: cancel,
|
||||
}, {
|
||||
text: _t('Discard'),
|
||||
close: true,
|
||||
click: () => {
|
||||
resolve(true);
|
||||
reset();
|
||||
},
|
||||
}],
|
||||
});
|
||||
dialog.on('closed', this.discardingDef, cancel);
|
||||
});
|
||||
return this.discardingDef;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onButtonClicked: function (ev) {
|
||||
var self = this;
|
||||
if (ev.data.attrs.name !== 'execute' && ev.data.attrs.name !== 'cancel') {
|
||||
var recordID = ev.data.recordID;
|
||||
var _super = this._super;
|
||||
var args = arguments;
|
||||
this._discardChanges(recordID, { noAbandon: true }).then(function () {
|
||||
_super.apply(self, args);
|
||||
});
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_onBeforeUnload: function () {
|
||||
// We should not save when leaving Odoo in the settings
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
const BaseSettingsModel = BasicModel.extend({
|
||||
save(recordID, options) {
|
||||
const savePoint = options && options.savePoint;
|
||||
return this._super.apply(this, arguments).then(result => {
|
||||
if (!savePoint && this.localData[recordID].model === 'res.config.settings') {
|
||||
// we remove here the res_id, because the record should still be
|
||||
// considered new. We want the web client to always perform a
|
||||
// onchange to fetch the settings anew.
|
||||
delete this.localData[recordID].res_id;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
var BaseSettingView = FormView.extend({
|
||||
jsLibs: [],
|
||||
|
||||
config: _.extend({}, FormView.prototype.config, {
|
||||
Model: BaseSettingsModel,
|
||||
Renderer: BaseSettingRenderer,
|
||||
Controller: BaseSettingController,
|
||||
}),
|
||||
});
|
||||
|
||||
view_registry.add('base_settings', BaseSettingView);
|
||||
|
||||
return {
|
||||
Model: BaseSettingsModel,
|
||||
Renderer: BaseSettingRenderer,
|
||||
Controller: BaseSettingController,
|
||||
};
|
||||
});
|
||||
@@ -1,708 +0,0 @@
|
||||
odoo.define('base.settings_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
var view_registry = require('web.view_registry');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
var BaseSettingsView = view_registry.get('base_settings');
|
||||
|
||||
const { getFixture, legacyExtraNextTick } = require("@web/../tests/helpers/utils");
|
||||
const { createWebClient, doAction } = require("@web/../tests/webclient/helpers");
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
QUnit.module('base_settings_tests', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
'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: {}
|
||||
}
|
||||
};
|
||||
serverData = { models: this.data };
|
||||
target = getFixture();
|
||||
}
|
||||
}, function () {
|
||||
|
||||
QUnit.module('BaseSetting');
|
||||
|
||||
QUnit.test('change setting on nav bar click in base settings', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var form = await createView({
|
||||
View: BaseSettingsView,
|
||||
model: 'res.config.settings',
|
||||
data: this.data,
|
||||
arch: '<form string="Settings" class="oe_form_configuration o_base_settings">' +
|
||||
'<div class="o_panel">' +
|
||||
'<div class="setting_search">' +
|
||||
'<input type="text" class="searchInput" placeholder="Search..."/>' +
|
||||
'</div> ' +
|
||||
'</div> ' +
|
||||
'<header>' +
|
||||
'<button string="Save" type="object" name="execute" class="oe_highlight" />' +
|
||||
'<button string="Cancel" type="object" name="cancel" class="oe_link" />' +
|
||||
'</header>' +
|
||||
'<div class="o_setting_container">' +
|
||||
'<div class="settings_tab"/>'+
|
||||
'<div class="settings">' +
|
||||
'<div class="notFound o_hidden">No Record Found</div>' +
|
||||
'<div class="app_settings_block" string="CRM" data-key="crm">' +
|
||||
'<div class="row mt16 o_settings_container">'+
|
||||
'<div class="col-12 col-lg-6 o_setting_box">'+
|
||||
'<div class="o_setting_left_pane">' +
|
||||
'<field name="bar"/>'+
|
||||
'</div>'+
|
||||
'<div class="o_setting_right_pane">'+
|
||||
'<label for="bar"/>'+
|
||||
'<div class="text-muted">'+
|
||||
'this is bar'+
|
||||
'</div>'+
|
||||
'</div>' +
|
||||
'</div>'+
|
||||
'<div class="col-12 col-lg-6 o_setting_box">'+
|
||||
'<div class="o_setting_left_pane">' +
|
||||
'<field name="foo"/>'+
|
||||
'</div>'+
|
||||
'<div class="o_setting_right_pane">'+
|
||||
'<span class="o_form_label">Foo</span>'+
|
||||
'<div class="text-muted">'+
|
||||
'this is foo'+
|
||||
'</div>'+
|
||||
'</div>' +
|
||||
'</div>'+
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
});
|
||||
|
||||
assert.hasAttrValue(form.$('.selected'), 'data-key',"crm","crm setting selected");
|
||||
assert.isVisible(form.$(".settings .app_settings_block"), "res.config.settings settings show");
|
||||
await testUtils.fields.editAndTrigger(form.$('.searchInput'), 'b', 'keyup');
|
||||
assert.strictEqual(form.$('.highlighter').html(), "B", "b word highlighted");
|
||||
await testUtils.fields.editAndTrigger(form.$('.searchInput'), 'bx', 'keyup');
|
||||
assert.isVisible(form.$('.notFound'), "record not found message shown");
|
||||
form.$('.searchInput').val('f').trigger('keyup');
|
||||
assert.strictEqual(form.$('span.o_form_label .highlighter').html(), "F", "F word highlighted");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('unhighlight section not matching anymore', async function(assert) {
|
||||
assert.expect(7);
|
||||
|
||||
const form = await createView({
|
||||
View: BaseSettingsView,
|
||||
model: 'res.config.settings',
|
||||
data: this.data,
|
||||
arch: `
|
||||
<form string="Settings" class="oe_form_configuration o_base_settings">
|
||||
<div class="o_panel">
|
||||
<div class="setting_search">
|
||||
<input type="text" class="searchInput" placeholder="Search..." />
|
||||
</div>
|
||||
</div>
|
||||
<header>
|
||||
<button string="Save" type="object" name="execute" class="oe_highlight" />
|
||||
<button string="Cancel" type="object" name="cancel" class="oe_link" />
|
||||
</header>
|
||||
<div class="o_setting_container">
|
||||
<div class="settings_tab" />
|
||||
<div class="settings">
|
||||
<div class="notFound o_hidden">No Record Found</div>
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="baz"/>
|
||||
<div class="content-group">
|
||||
<div class="mt16">
|
||||
<field name="baz" class="o_light_label" widget="radio"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>`
|
||||
});
|
||||
assert.hasAttrValue(form.$('.selected'), 'data-key',"crm","crm setting selected");
|
||||
assert.isVisible(form.$(".settings .app_settings_block"), "project settings show");
|
||||
|
||||
await testUtils.fields.editAndTrigger(form.$('.searchInput'), 'trea', 'keyup');
|
||||
assert.containsN(form, '.highlighter', 2, 'should have 2 options highlighted');
|
||||
assert.equal(form.$('.highlighter:eq(0)').parent().text(), 'treads');
|
||||
assert.equal(form.$('.highlighter:eq(1)').parent().text(), 'treats');
|
||||
|
||||
await testUtils.fields.editAndTrigger(form.$('.searchInput'), 'tread', 'keyup');
|
||||
assert.containsN(form, '.highlighter', 1, 'should have only one highlighted');
|
||||
assert.equal(form.$('.highlighter').parent().text(), 'treads');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('hide / show setting tips properly', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const form = await createView({
|
||||
View: BaseSettingsView,
|
||||
model: 'res.config.settings',
|
||||
data: this.data,
|
||||
arch: `
|
||||
<form string="Settings" class="oe_form_configuration o_base_settings">
|
||||
<div class="o_panel">
|
||||
<div class="setting_search">
|
||||
<input type="text" class="searchInput" placeholder="Search..." />
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_setting_container">
|
||||
<div class="settings_tab" />
|
||||
<div class="settings">
|
||||
<div class="notFound o_hidden">No Record Found</div>
|
||||
<div class="app_settings_block" string="Settings" data-key="settings">
|
||||
<h2>Setting Header</h2>
|
||||
<h3 class="o_setting_tip">Settings will appear below</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>`
|
||||
});
|
||||
|
||||
assert.containsOnce(form, '.o_setting_tip:not(.o_hidden)', 'Tip should not be hidden initially');
|
||||
|
||||
await testUtils.fields.editAndTrigger(form.$('.searchInput'), 'Setting', 'keyup');
|
||||
assert.containsOnce(form, '.o_setting_tip.o_hidden', 'Tip should be hidden when user searches in settings');
|
||||
|
||||
await testUtils.fields.editAndTrigger(form.$('.searchInput'), '', 'keyup');
|
||||
assert.containsOnce(form, '.o_setting_tip:not(.o_hidden)', 'Tip should be displayed again');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"settings views does not read existing id 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":
|
||||
`<form string="Settings" js_class="base_settings">
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</div>
|
||||
</form>`,
|
||||
"task,2,list": '<tree><field name="display_name"/></tree>',
|
||||
"res.config.settings,false,search": "<search></search>",
|
||||
"task,false,search": "<search></search>",
|
||||
};
|
||||
|
||||
const mockRPC = (route, args) => {
|
||||
if (args.method) {
|
||||
assert.step(args.method);
|
||||
}
|
||||
};
|
||||
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
|
||||
await doAction(webClient, 1);
|
||||
await testUtils.dom.click($(target).find('button[name="4"]'));
|
||||
await legacyExtraNextTick();
|
||||
await testUtils.dom.click($(".o_control_panel .breadcrumb-item a"));
|
||||
await legacyExtraNextTick();
|
||||
assert.hasClass($(target).find(".o_form_view"), "o_form_editable");
|
||||
assert.verifySteps([
|
||||
"get_views", // initial setting action
|
||||
"onchange", // this is a setting view => create new record
|
||||
"create", // when we click on action button => save
|
||||
"read", // with save, we have a reload... (not necessary actually)
|
||||
"get_views", // for other action in breadcrumb,
|
||||
"web_search_read", // same
|
||||
"onchange", // when we come back, we want to restart from scratch
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"clicking on any button in setting should show discard warning if setting form is dirty",
|
||||
async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
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":
|
||||
`<form string="Settings" js_class="base_settings">
|
||||
<header>
|
||||
<button string="Save" type="object" name="execute" class="oe_highlight" />
|
||||
<button string="Cancel" type="object" name="cancel" class="oe_link" />
|
||||
</header>
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="foo"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Foo</span>
|
||||
<div class="text-muted">
|
||||
this is foo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</div>
|
||||
</form>`,
|
||||
"task,2,list": '<tree><field name="display_name"/></tree>',
|
||||
"res.config.settings,false,search": "<search></search>",
|
||||
"task,false,search": "<search></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 testUtils.dom.click($(target).find("input[type='checkbox']"));
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should be checked"
|
||||
);
|
||||
|
||||
await testUtils.dom.click($(target).find('button[name="4"]'));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(document.body, ".modal", "should open a warning dialog");
|
||||
|
||||
await testUtils.dom.click($(".modal button:contains(Discard)"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".o_list_view", "should be open list view");
|
||||
|
||||
await testUtils.dom.click($(".o_control_panel .breadcrumb-item a"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsNone(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should not be checked"
|
||||
);
|
||||
|
||||
await testUtils.dom.click($(target).find("input[type='checkbox']"));
|
||||
await testUtils.dom.click($(target).find('button[name="4"]'));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(document.body, ".modal", "should open a warning dialog");
|
||||
|
||||
await testUtils.dom.click($(".modal button:contains(Stay Here)"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsOnce(target, ".o_form_view", "should be remain on form view");
|
||||
|
||||
await testUtils.dom.click($(target).find("button[name='execute']"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".modal",
|
||||
"should not open a warning dialog"
|
||||
);
|
||||
|
||||
await testUtils.dom.click($(target).find("input[type='checkbox']"));
|
||||
await testUtils.dom.click($(target).find("button[name='cancel']"));
|
||||
await legacyExtraNextTick();
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
".modal",
|
||||
"should not open a warning dialog"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test('settings view does not display other settings after reload', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = await createView({
|
||||
View: BaseSettingsView,
|
||||
model: 'res.config.settings',
|
||||
data: this.data,
|
||||
arch: '<form string="Settings" class="oe_form_configuration o_base_settings">' +
|
||||
'<div class="o_panel">' +
|
||||
'<div class="setting_search">' +
|
||||
'<input type="text" class="searchInput" placeholder="Search..."/>' +
|
||||
'</div> ' +
|
||||
'</div> ' +
|
||||
'<header>' +
|
||||
'<button string="Save" type="object" name="execute" class="oe_highlight" />' +
|
||||
'<button string="Cancel" type="object" name="cancel" class="oe_link" />' +
|
||||
'</header>' +
|
||||
'<div class="o_setting_container">' +
|
||||
'<div class="settings_tab"/>'+
|
||||
'<div class="settings">' +
|
||||
'<div class="notFound o_hidden">No Record Found</div>' +
|
||||
'<div class="app_settings_block" string="CRM" data-key="crm">' +
|
||||
'crm tab' +
|
||||
'</div>' +
|
||||
'<div class="app_settings_block o_not_app" string="Other App" data-key="otherapp">' +
|
||||
'other app tab' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('.app_settings_block').text().replace(/\s/g,''), 'CRMcrmtab');
|
||||
await form.reload();
|
||||
assert.strictEqual(form.$('.app_settings_block').text().replace(/\s/g,''), 'CRMcrmtab');
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('settings view shows a message if there are changes', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
var form = await createView({
|
||||
View: BaseSettingsView,
|
||||
model: 'res.config.settings',
|
||||
data: this.data,
|
||||
arch: '<form string="Settings" class="oe_form_configuration o_base_settings">' +
|
||||
'<header>' +
|
||||
'<button string="Save" type="object" name="execute" class="oe_highlight" />' +
|
||||
'<button string="Discard" type="object" name="cancel" special="cancel" />'+
|
||||
'</header>' +
|
||||
'<div class="o_setting_container">' +
|
||||
'<div class="settings_tab"/>' +
|
||||
'<div class="settings">' +
|
||||
'<div class="notFound o_hidden">No Record Found</div>' +
|
||||
'<div class="app_settings_block" string="Base Setting" data-key="base-setting">' +
|
||||
'<field name="bar"/>Make Changes' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
});
|
||||
|
||||
testUtils.mock.intercept(form, "field_changed", function (event) {
|
||||
assert.ok(true,"field changed");
|
||||
}, true);
|
||||
|
||||
assert.containsNone(form, '.o_field_boolean input:checked', "checkbox should not be checked");
|
||||
assert.containsNone(form, ".o_dirty_warning", "warning message should not be shown");
|
||||
await testUtils.dom.click(form.$("input[type='checkbox']"));
|
||||
assert.containsOnce(form, '.o_field_boolean input:checked' ,"checkbox should be checked");
|
||||
assert.containsOnce(form, ".o_dirty_warning", "warning message should be shown");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('settings view shows a message if there are changes even if the save failed', async function (assert) {
|
||||
assert.expect(3);
|
||||
var self = this;
|
||||
self.alreadySavedOnce = false;
|
||||
|
||||
var form = await createView({
|
||||
View: BaseSettingsView,
|
||||
model: 'res.config.settings',
|
||||
data: this.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "create" && !self.alreadySavedOnce) {
|
||||
self.alreadySavedOnce = true;
|
||||
//fail on first create
|
||||
return Promise.reject({});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
arch: '<form string="Settings" class="oe_form_configuration o_base_settings">' +
|
||||
'<header>' +
|
||||
'<button string="Save" type="object" name="execute" class="oe_highlight" />' +
|
||||
'<button string="Discard" type="object" name="cancel" special="cancel" />'+
|
||||
'</header>' +
|
||||
'<div class="o_setting_container">' +
|
||||
'<div class="settings_tab"/>' +
|
||||
'<div class="settings">' +
|
||||
'<div class="notFound o_hidden">No Record Found</div>' +
|
||||
'<div class="app_settings_block" string="Base Setting" data-key="base-setting">' +
|
||||
'<field name="bar"/>Make Changes' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
});
|
||||
|
||||
|
||||
await testUtils.dom.click(form.$("input[type='checkbox']"));
|
||||
assert.containsOnce(form, ".o_dirty_warning", "warning message should be shown");
|
||||
await testUtils.form.clickSave(form);
|
||||
assert.containsOnce(form, ".o_dirty_warning", "warning message should be shown");
|
||||
await testUtils.form.clickSave(form);
|
||||
assert.containsNone(form, ".o_dirty_warning", "warning message should be shown");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
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": '<tree><field name="display_name"/></tree>',
|
||||
"res.config.settings,2,form": `
|
||||
<form string="Settings" js_class="base_settings">
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<button name="3" string="Execute action" type="action"/>
|
||||
</div>
|
||||
</form>`,
|
||||
"task,3,list": '<tree><field name="display_name"/></tree>',
|
||||
"res.config.settings,false,search": "<search></search>",
|
||||
"task,false,search": "<search></search>",
|
||||
};
|
||||
|
||||
let loadViewsDef;
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "read") {
|
||||
await loadViewsDef; // 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 actionNew"
|
||||
);
|
||||
|
||||
loadViewsDef = testUtils.makeTestPromise();
|
||||
await testUtils.dom.click($(target).find('button[name="3"]'));
|
||||
await legacyExtraNextTick();
|
||||
assert.strictEqual(
|
||||
$(target).find(".breadcrumb").text(),
|
||||
"First actionNew"
|
||||
);
|
||||
|
||||
loadViewsDef.resolve();
|
||||
await testUtils.nextTick();
|
||||
await legacyExtraNextTick();
|
||||
assert.strictEqual(
|
||||
$(target).find(".breadcrumb").text(),
|
||||
"First actionNewOther action"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test('settings can contain one2many fields', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const form = await createView({
|
||||
View: BaseSettingsView,
|
||||
model: 'res.config.settings',
|
||||
data: this.data,
|
||||
arch: `
|
||||
<form string="Settings" class="oe_form_configuration o_base_settings">
|
||||
<header>
|
||||
<button string="Save" type="object" name="execute" class="oe_highlight" />
|
||||
<button string="Discard" type="object" name="cancel" special="cancel" />
|
||||
</header>
|
||||
<div class="o_setting_container">
|
||||
<div class="settings_tab"/>
|
||||
<div class="settings">
|
||||
<div class="notFound o_hidden">No Record Found</div>
|
||||
<div class="app_settings_block" string="Base Setting" data-key="base-setting">
|
||||
<field name="tasks">
|
||||
<tree><field name="display_name"/></tree>
|
||||
<form><field name="display_name"/></form>
|
||||
</field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.fields.editInput($('.modal-body input[name=display_name]'), 'Added Task');
|
||||
await testUtils.dom.click($('.modal-dialog footer button:first-child'));
|
||||
|
||||
assert.strictEqual(form.$('table.o_list_table:eq(0) tr.o_data_row td.o_data_cell:eq(0)').text(),
|
||||
'Added Task',
|
||||
'The one2many relation item should have been added');
|
||||
|
||||
await testUtils.form.clickSave(form);
|
||||
|
||||
assert.strictEqual(form.$('table.o_list_table:eq(0) tr.o_data_row td.o_data_cell:eq(0)').text(),
|
||||
'Added Task',
|
||||
'The one2many relation item should still be present');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
'call "call_button/execute" when clicking on a button in dirty settings',
|
||||
async function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
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": `
|
||||
<form string="Settings" js_class="base_settings">
|
||||
<div class="app_settings_block" string="CRM" data-key="crm">
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="foo"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<span class="o_form_label">Foo</span>
|
||||
<div class="text-muted">
|
||||
this is foo
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button name="4" string="Execute action" type="action"/>
|
||||
</div>
|
||||
</form>
|
||||
`,
|
||||
"res.config.settings,false,search": "<search></search>",
|
||||
"task,false,list": "<tree></tree>",
|
||||
"task,false,search": "<search></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 testUtils.dom.click($(target).find('input[type="checkbox"]'));
|
||||
assert.containsOnce(
|
||||
target,
|
||||
".o_field_boolean input:checked",
|
||||
"checkbox should be checked"
|
||||
);
|
||||
|
||||
await testUtils.dom.click($(target).find('button[name="4"]'));
|
||||
assert.containsOnce(document.body, ".modal", "should open a warning dialog");
|
||||
|
||||
await testUtils.dom.click($(".modal-footer .btn-primary"));
|
||||
assert.verifySteps([
|
||||
"create", // saveRecord from modal
|
||||
"execute", // execute_action
|
||||
"create", // saveRecord from FormController._onButtonClicked
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
});
|
||||
@@ -6,36 +6,8 @@
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Settings" class="oe_form_configuration o_base_settings" js_class="base_settings">
|
||||
<div class="o_control_panel">
|
||||
<div class="o_panel">
|
||||
<ol role="navigation" class="breadcrumb o_settings_title d-none d-md-block">
|
||||
<li class="breadcrumb-item active">Settings</li>
|
||||
</ol>
|
||||
<div class="o_setting_search">
|
||||
<input type="text" class="searchInput float-end p-0 pb-1" placeholder="Search..."/>
|
||||
<span class="searchIcon"><i class="fa fa-search" role="img" aria-label="Search" title="Search"/></span>
|
||||
</div>
|
||||
</div>
|
||||
<header>
|
||||
<button string="Save" type="object" name="execute" class="oe_highlight" />
|
||||
<button string="Discard" type="object" name="cancel" special="cancel" data-hotkey="z" />
|
||||
</header>
|
||||
</div>
|
||||
<div class="o_setting_container">
|
||||
<div class="settings_tab"/>
|
||||
<div class="settings">
|
||||
<div class="notFound o_hidden">
|
||||
<div class="o_view_nocontent">
|
||||
<div class="o_nocontent_help">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No setting found
|
||||
</p><p>
|
||||
Try searching for another keyword
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
Reference in New Issue
Block a user