From 2c9db825c1487e80423a7adff17ea60b27fe94d3 Mon Sep 17 00:00:00 2001 From: Jorge Pinna Puissant Date: Fri, 1 Jul 2022 12:41:32 +0000 Subject: [PATCH] [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 --- addons/hr_timesheet/__manifest__.py | 4 +- .../src/app_store_widget/app_store_widget.js | 61 + .../app_store_widget/app_store_widget.scss | 3 + .../src/app_store_widget/app_store_widget.xml | 23 + .../src/js/timesheet_config_form_view.js | 14 +- .../views/res_config_settings_views.xml | 7 +- .../web_overrides/res_config_settings.js | 46 - addons/web/__manifest__.py | 2 - addons/web/static/src/legacy/xml/base.xml | 15 - .../src/views/fields/radio/radio_field.js | 24 +- .../static/src/views/form/form_controller.js | 6 +- addons/web/static/src/views/view_compiler.js | 3 + addons/web/static/src/views/widgets/widget.js | 9 +- .../form_label_highlight_text.js | 18 + .../form_label_highlight_text.xml | 8 + .../highlight_text/highlight_text.js | 25 + .../highlight_text/highlight_text.xml | 9 + .../highlight_text/settings_radio_field.js | 16 + .../highlight_text/settings_radio_field.xml | 10 + .../settings/settings_app.js | 12 + .../settings/settings_app.xml | 12 + .../settings/settings_page.js | 22 + .../settings/settings_page.xml | 16 + .../settings_confirmation_dialog.js | 21 + .../settings_confirmation_dialog.xml | 19 + .../settings_form_compiler.js | 248 ++++ .../settings_form_controller.js | 109 ++ .../settings_form_renderer.js | 60 + .../settings_form_view/settings_form_view.js | 18 + .../settings_form_view.scss | 18 + .../settings_form_view/settings_form_view.xml | 37 + .../tests/webclient/actions/legacy_tests.js | 30 +- .../webclient/settings_form_view_tests.js | 1173 +++++++++++++++++ .../src/js/backend/res_config_settings.js | 38 - .../views/res_config_settings_views.xml | 4 +- .../base/static/src/js/res_config_settings.js | 478 ------- .../base/static/tests/base_settings_tests.js | 708 ---------- .../base/views/res_config_settings_views.xml | 30 +- 38 files changed, 1991 insertions(+), 1365 deletions(-) create mode 100644 addons/hr_timesheet/static/src/app_store_widget/app_store_widget.js create mode 100644 addons/hr_timesheet/static/src/app_store_widget/app_store_widget.scss create mode 100644 addons/hr_timesheet/static/src/app_store_widget/app_store_widget.xml delete mode 100644 addons/point_of_sale/static/src/backend/web_overrides/res_config_settings.js create mode 100644 addons/web/static/src/webclient/settings_form_view/highlight_text/form_label_highlight_text.js create mode 100644 addons/web/static/src/webclient/settings_form_view/highlight_text/form_label_highlight_text.xml create mode 100644 addons/web/static/src/webclient/settings_form_view/highlight_text/highlight_text.js create mode 100644 addons/web/static/src/webclient/settings_form_view/highlight_text/highlight_text.xml create mode 100644 addons/web/static/src/webclient/settings_form_view/highlight_text/settings_radio_field.js create mode 100644 addons/web/static/src/webclient/settings_form_view/highlight_text/settings_radio_field.xml create mode 100644 addons/web/static/src/webclient/settings_form_view/settings/settings_app.js create mode 100644 addons/web/static/src/webclient/settings_form_view/settings/settings_app.xml create mode 100644 addons/web/static/src/webclient/settings_form_view/settings/settings_page.js create mode 100644 addons/web/static/src/webclient/settings_form_view/settings/settings_page.xml create mode 100644 addons/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.js create mode 100644 addons/web/static/src/webclient/settings_form_view/settings_confirmation_dialog.xml create mode 100644 addons/web/static/src/webclient/settings_form_view/settings_form_compiler.js create mode 100644 addons/web/static/src/webclient/settings_form_view/settings_form_controller.js create mode 100644 addons/web/static/src/webclient/settings_form_view/settings_form_renderer.js create mode 100644 addons/web/static/src/webclient/settings_form_view/settings_form_view.js create mode 100644 addons/web/static/src/webclient/settings_form_view/settings_form_view.scss create mode 100644 addons/web/static/src/webclient/settings_form_view/settings_form_view.xml create mode 100644 addons/web/static/tests/webclient/settings_form_view_tests.js delete mode 100644 odoo/addons/base/static/src/js/res_config_settings.js delete mode 100644 odoo/addons/base/static/tests/base_settings_tests.js diff --git a/addons/hr_timesheet/__manifest__.py b/addons/hr_timesheet/__manifest__.py index d74677ca892..29ffcbd066d 100644 --- a/addons/hr_timesheet/__manifest__.py +++ b/addons/hr_timesheet/__manifest__.py @@ -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', diff --git a/addons/hr_timesheet/static/src/app_store_widget/app_store_widget.js b/addons/hr_timesheet/static/src/app_store_widget/app_store_widget.js new file mode 100644 index 00000000000..5355a228432 --- /dev/null +++ b/addons/hr_timesheet/static/src/app_store_widget/app_store_widget.js @@ -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); diff --git a/addons/hr_timesheet/static/src/app_store_widget/app_store_widget.scss b/addons/hr_timesheet/static/src/app_store_widget/app_store_widget.scss new file mode 100644 index 00000000000..5c480670577 --- /dev/null +++ b/addons/hr_timesheet/static/src/app_store_widget/app_store_widget.scss @@ -0,0 +1,3 @@ +.o_widget.o_widget_hr_timesheet\.app_store_widget { + display: contents; +} diff --git a/addons/hr_timesheet/static/src/app_store_widget/app_store_widget.xml b/addons/hr_timesheet/static/src/app_store_widget/app_store_widget.xml new file mode 100644 index 00000000000..a0d11f14471 --- /dev/null +++ b/addons/hr_timesheet/static/src/app_store_widget/app_store_widget.xml @@ -0,0 +1,23 @@ + + + + + + + + + +
+

Scan this QR code to get the Awesome Timesheet app:

+
+
+ +
+ + + + +
+
+ +
diff --git a/addons/hr_timesheet/static/src/js/timesheet_config_form_view.js b/addons/hr_timesheet/static/src/js/timesheet_config_form_view.js index dfa49acdaa1..250bb0910b9 100644 --- a/addons/hr_timesheet/static/src/js/timesheet_config_form_view.js +++ b/addons/hr_timesheet/static/src/js/timesheet_config_form_view.js @@ -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}; }); diff --git a/addons/hr_timesheet/views/res_config_settings_views.xml b/addons/hr_timesheet/views/res_config_settings_views.xml index cccb9c3cd91..5db4003cba3 100644 --- a/addons/hr_timesheet/views/res_config_settings_views.xml +++ b/addons/hr_timesheet/views/res_config_settings_views.xml @@ -7,9 +7,6 @@ - - hr_timesheet_config_form -

Time Encoding

@@ -61,10 +58,10 @@
- Apple App Store +
- Google Play Store +
diff --git a/addons/point_of_sale/static/src/backend/web_overrides/res_config_settings.js b/addons/point_of_sale/static/src/backend/web_overrides/res_config_settings.js deleted file mode 100644 index 3c7983ebadd..00000000000 --- a/addons/point_of_sale/static/src/backend/web_overrides/res_config_settings.js +++ /dev/null @@ -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; -}); diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index aa03aa457ec..c8e340d56fc 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -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', diff --git a/addons/web/static/src/legacy/xml/base.xml b/addons/web/static/src/legacy/xml/base.xml index 4eadc50e243..9c9be3dfa23 100644 --- a/addons/web/static/src/legacy/xml/base.xml +++ b/addons/web/static/src/legacy/xml/base.xml @@ -1543,21 +1543,6 @@ - - -
-
@@ -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.
-
diff --git a/odoo/addons/base/static/src/js/res_config_settings.js b/odoo/addons/base/static/src/js/res_config_settings.js deleted file mode 100644 index 5de8b7521a8..00000000000 --- a/odoo/addons/base/static/src/js/res_config_settings.js +++ /dev/null @@ -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($('', {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)), - $('').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, -}; -}); diff --git a/odoo/addons/base/static/tests/base_settings_tests.js b/odoo/addons/base/static/tests/base_settings_tests.js deleted file mode 100644 index 1016f8c8420..00000000000 --- a/odoo/addons/base/static/tests/base_settings_tests.js +++ /dev/null @@ -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: '
' + - '
' + - ' ' + - '
' + - '
' + - '
' + - '
' + - '
'+ - '
' + - '
No Record Found
' + - '
' + - '
'+ - '
'+ - '
' + - ''+ - '
'+ - '
'+ - '
' + - '
'+ - '
'+ - '
' + - ''+ - '
'+ - '
'+ - 'Foo'+ - '
'+ - 'this is foo'+ - '
'+ - '
' + - '
'+ - '
' + - '
' + - '
' + - '
' + - '', - }); - - 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: ` -
-
- -
-
-
-
-
-
-
No Record Found
-
-
-
-
-
-
-
-
-
-
- ` - }); - 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: ` -
-
- -
-
-
-
-
No Record Found
-
-

Setting Header

-

Settings will appear below

-
-
-
- ` - }); - - 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": - `
-
-
-
`, - "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); - 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": - `
-
-
-
-
-
-
- -
-
- Foo -
- this is foo -
-
-
-
-
-
`, - "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 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: '
' + - '
' + - ' ' + - '
' + - '
' + - '
' + - '
' + - '
'+ - '
' + - '
No Record Found
' + - '
' + - 'crm tab' + - '
' + - '
' + - 'other app tab' + - '
' + - '
' + - '
' + - '', - }); - - 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: '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
No Record Found
' + - '
' + - 'Make Changes' + - '
' + - '
' + - '
' + - '', - }); - - 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: '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
No Record Found
' + - '
' + - 'Make Changes' + - '
' + - '
' + - '
' + - '', - }); - - - 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": '', - "res.config.settings,2,form": ` -
-
-
-
`, - "task,3,list": '', - "res.config.settings,false,search": "", - "task,false,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: ` -
-
-
-
-
-
-
No Record Found
-
- - - - -
-
-
- `, - }); - - 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": ` -
-
-
-
-
- -
-
- Foo -
- this is foo -
-
-
-
-
-
- `, - "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 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 - ]); - } - ); - -}); -}); diff --git a/odoo/addons/base/views/res_config_settings_views.xml b/odoo/addons/base/views/res_config_settings_views.xml index d7d552f0f44..6d0f1adbd6e 100644 --- a/odoo/addons/base/views/res_config_settings_views.xml +++ b/odoo/addons/base/views/res_config_settings_views.xml @@ -6,36 +6,8 @@ res.config.settings
-
-
- - -
-
-
-
-
-
-
-
-
-

- No setting found -

- Try searching for another keyword -

-
-
-
-
+