From bc95c6a348f2e5f1bbbcd271a8d56b979f9d3af5 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Tue, 25 Jul 2023 12:20:49 +0000 Subject: [PATCH] [REF] web,*: remove legacy client action support This commit removes a lot of legacy code that we kept for the few legacy client actions that were still to be converted to owl. Now that the last one has been converted, we can finally get rid of a lot of legacy code (ActionAdapter, ActionMixin, ControlPanel, ActionMenus, DropdownMenu(Item), Pager, DomainSelector...). Part of task~3439226 closes odoo/odoo#129314 Related: odoo/enterprise#44465 Signed-off-by: Aaron Bohy (aab) --- addons/mrp_subcontracting/__manifest__.py | 11 - addons/point_of_sale/__manifest__.py | 1 - addons/project/__manifest__.py | 11 - .../question_page_one2many_field_tests.js | 7 + addons/web/__manifest__.py | 15 - .../web/static/src/legacy/action_adapters.js | 244 ---- .../src/legacy/js/chrome/abstract_action.js | 82 -- .../src/legacy/js/chrome/action_mixin.js | 181 --- .../src/legacy/js/components/action_menus.js | 206 ---- .../legacy/js/components/custom_file_input.js | 87 -- .../src/legacy/js/components/dropdown_menu.js | 189 --- .../js/components/dropdown_menu_item.js | 127 -- .../static/src/legacy/js/components/pager.js | 224 ---- .../legacy/js/control_panel/control_panel.js | 181 --- .../legacy/js/control_panel/search_utils.js | 547 --------- addons/web/static/src/legacy/js/core/mvc.js | 247 ---- .../web/static/src/legacy/js/services/core.js | 3 - .../web/static/src/legacy/js/tools/tools.js | 19 - .../legacy/js/widgets/colorpicker_dialog.js | 47 - .../src/legacy/js/widgets/domain_selector.js | 1026 ----------------- .../js/widgets/domain_selector_dialog.js | 67 -- .../legacy/js/widgets/model_field_selector.js | 180 --- .../widgets/model_field_selector_popover.js | 734 ------------ .../src/legacy/legacy_client_actions.js | 84 -- .../src/legacy/scss/domain_selector.scss | 185 --- addons/web/static/src/legacy/xml/base.xml | 367 ------ .../src/views/kanban/kanban_controller.js | 1 - .../legacy/components/action_menus_tests.js | 262 ----- .../components/custom_file_input_tests.js | 84 -- .../components/dropdown_menu_mobile_tests.js | 66 -- .../legacy/components/dropdown_menu_tests.js | 428 ------- .../tests/legacy/components/pager_tests.js | 186 --- .../control_panel/search_utils_tests.js | 367 ------ .../static/tests/legacy/core/mixins_tests.js | 44 - .../static/tests/legacy/helpers/test_utils.js | 10 +- .../tests/legacy/helpers/test_utils_create.js | 11 - ...ice_provider_adapter_notification_tests.js | 80 -- .../widgets/model_field_selector_tests.js | 323 ------ .../web/static/tests/views/list_view_tests.js | 5 + .../webclient/actions/client_action_tests.js | 287 +---- .../webclient/actions/load_state_tests.js | 174 --- .../tests/webclient/actions/misc_tests.js | 48 +- .../tests/webclient/actions/target_tests.js | 54 - addons/web/static/tests/webclient/helpers.js | 31 +- 44 files changed, 23 insertions(+), 7510 deletions(-) delete mode 100644 addons/web/static/src/legacy/action_adapters.js delete mode 100644 addons/web/static/src/legacy/js/chrome/abstract_action.js delete mode 100644 addons/web/static/src/legacy/js/chrome/action_mixin.js delete mode 100644 addons/web/static/src/legacy/js/components/action_menus.js delete mode 100644 addons/web/static/src/legacy/js/components/custom_file_input.js delete mode 100644 addons/web/static/src/legacy/js/components/dropdown_menu.js delete mode 100644 addons/web/static/src/legacy/js/components/dropdown_menu_item.js delete mode 100644 addons/web/static/src/legacy/js/components/pager.js delete mode 100644 addons/web/static/src/legacy/js/control_panel/control_panel.js delete mode 100644 addons/web/static/src/legacy/js/control_panel/search_utils.js delete mode 100644 addons/web/static/src/legacy/js/core/mvc.js delete mode 100644 addons/web/static/src/legacy/js/tools/tools.js delete mode 100644 addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js delete mode 100644 addons/web/static/src/legacy/js/widgets/domain_selector.js delete mode 100644 addons/web/static/src/legacy/js/widgets/domain_selector_dialog.js delete mode 100644 addons/web/static/src/legacy/js/widgets/model_field_selector.js delete mode 100644 addons/web/static/src/legacy/js/widgets/model_field_selector_popover.js delete mode 100644 addons/web/static/src/legacy/legacy_client_actions.js delete mode 100644 addons/web/static/src/legacy/scss/domain_selector.scss delete mode 100644 addons/web/static/tests/legacy/components/action_menus_tests.js delete mode 100644 addons/web/static/tests/legacy/components/custom_file_input_tests.js delete mode 100644 addons/web/static/tests/legacy/components/dropdown_menu_mobile_tests.js delete mode 100644 addons/web/static/tests/legacy/components/dropdown_menu_tests.js delete mode 100644 addons/web/static/tests/legacy/components/pager_tests.js delete mode 100644 addons/web/static/tests/legacy/control_panel/search_utils_tests.js delete mode 100644 addons/web/static/tests/legacy/services/service_provider_adapter_notification_tests.js delete mode 100644 addons/web/static/tests/legacy/widgets/model_field_selector_tests.js diff --git a/addons/mrp_subcontracting/__manifest__.py b/addons/mrp_subcontracting/__manifest__.py index a7e2574d2f6..35c5d0de1fb 100644 --- a/addons/mrp_subcontracting/__manifest__.py +++ b/addons/mrp_subcontracting/__manifest__.py @@ -89,9 +89,7 @@ # Form style should be computed before 'web/static/src/views/form/button_box/*.scss', - 'web/static/src/legacy/action_adapters.js', 'web/static/src/legacy/legacy_service_provider.js', - 'web/static/src/legacy/legacy_client_actions.js', 'web/static/src/legacy/legacy_dialog.js', 'web/static/src/legacy/legacy_load_views.js', 'web/static/src/legacy/legacy_promise_error_handler.js', @@ -102,11 +100,7 @@ 'web/static/src/legacy/backend_utils.js', 'web/static/src/legacy/utils.js', 'web/static/src/legacy/web_client.js', - 'web/static/src/legacy/js/chrome/*', - 'web/static/src/legacy/js/components/*', - 'web/static/src/legacy/js/control_panel/*', 'web/static/src/legacy/js/core/domain.js', - 'web/static/src/legacy/js/core/mvc.js', 'web/static/src/legacy/js/core/py_utils.js', 'web/static/src/legacy/js/core/context.js', 'web/static/src/legacy/js/core/misc.js', @@ -118,12 +112,7 @@ 'web/static/src/legacy/js/fields/field_utils.js', 'web/static/src/legacy/js/services/data_manager.js', 'web/static/src/legacy/js/services/session.js', - 'web/static/src/legacy/js/tools/tools.js', 'web/static/src/legacy/js/widgets/date_picker.js', - 'web/static/src/legacy/js/widgets/domain_selector_dialog.js', - 'web/static/src/legacy/js/widgets/domain_selector.js', - 'web/static/src/legacy/js/widgets/model_field_selector.js', - 'web/static/src/legacy/js/widgets/model_field_selector_popover.js', 'web/static/src/legacy/js/env.js', 'web/static/src/legacy/js/owl_compatibility.js', 'web/static/src/legacy/pseudo_web_client.js', diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py index 0393ac77e6f..a312ffe5508 100644 --- a/addons/point_of_sale/__manifest__.py +++ b/addons/point_of_sale/__manifest__.py @@ -156,7 +156,6 @@ 'web/static/src/legacy/js/**/*', ('remove', 'web/static/src/legacy/js/libs/**/*'), ('remove', 'web/static/src/legacy/js/public/**/*'), - 'web/static/src/legacy/action_adapters.js', 'web/static/src/legacy/backend_utils.js', 'web/static/src/legacy/legacy_load_views.js', 'web/static/src/legacy/legacy_service_provider.js', diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index cbafebfcdd8..55339861096 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -140,9 +140,7 @@ # Form style should be computed before 'web/static/src/views/form/button_box/*.scss', - 'web/static/src/legacy/action_adapters.js', 'web/static/src/legacy/legacy_service_provider.js', - 'web/static/src/legacy/legacy_client_actions.js', 'web/static/src/legacy/legacy_dialog.js', 'web/static/src/legacy/legacy_load_views.js', 'web/static/src/legacy/legacy_promise_error_handler.js', @@ -154,11 +152,7 @@ 'web/static/src/legacy/utils.js', 'web/static/src/legacy/pseudo_web_client.js', 'web/static/src/legacy/web_client.js', - 'web/static/src/legacy/js/chrome/*', - 'web/static/src/legacy/js/components/*', - 'web/static/src/legacy/js/control_panel/*', 'web/static/src/legacy/js/core/domain.js', - 'web/static/src/legacy/js/core/mvc.js', 'web/static/src/legacy/js/core/py_utils.js', 'web/static/src/legacy/js/core/context.js', 'web/static/src/legacy/js/core/misc.js', @@ -171,12 +165,7 @@ 'web/static/src/legacy/js/fields/field_utils.js', 'web/static/src/legacy/js/services/data_manager.js', 'web/static/src/legacy/js/services/session.js', - 'web/static/src/legacy/js/tools/tools.js', 'web/static/src/legacy/js/widgets/date_picker.js', - 'web/static/src/legacy/js/widgets/domain_selector_dialog.js', - 'web/static/src/legacy/js/widgets/domain_selector.js', - 'web/static/src/legacy/js/widgets/model_field_selector.js', - 'web/static/src/legacy/js/widgets/model_field_selector_popover.js', 'web/static/src/legacy/js/env.js', 'web/static/src/legacy/js/owl_compatibility.js', diff --git a/addons/survey/static/tests/components/question_page_one2many_field_tests.js b/addons/survey/static/tests/components/question_page_one2many_field_tests.js index 7e909ecb6d0..afc182029ce 100644 --- a/addons/survey/static/tests/components/question_page_one2many_field_tests.js +++ b/addons/survey/static/tests/components/question_page_one2many_field_tests.js @@ -2,6 +2,7 @@ import { makeServerError } from "@web/../tests/helpers/mock_server"; import { click, editInput, getFixture, nextTick, triggerHotkey } from "@web/../tests/helpers/utils"; +import { registerCleanup } from "@web/../tests/helpers/cleanup"; import { makeView, setupViewRegistries } from "@web/../tests/views/helpers"; import { errorService } from "@web/core/errors/error_service"; import { registry } from "@web/core/registry"; @@ -172,6 +173,12 @@ QUnit.module("QuestionPageOneToManyField", (hooks) => { QUnit.test("A validation error from saving parent form notifies and prevents dialog from closing", async (assert) => { registry.category("services").add("error", errorService); + + // need to preventDefault to remove error from console (so python test pass) + const handler = (ev) => ev.preventDefault(); + window.addEventListener("unhandledrejection", handler); + registerCleanup(() => window.removeEventListener("unhandledrejection", handler)); + await makeView({ type: "form", resModel: "survey", diff --git a/addons/web/__manifest__.py b/addons/web/__manifest__.py index 87af9250a1f..00735fdc53b 100644 --- a/addons/web/__manifest__.py +++ b/addons/web/__manifest__.py @@ -200,7 +200,6 @@ This module provides the core of the Odoo Web Client. 'web/static/lib/py.js/lib/py_extras.js', 'web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js', - 'web/static/src/legacy/scss/domain_selector.scss', 'web/static/src/legacy/scss/model_field_selector.scss', 'web/static/src/legacy/scss/dropdown.scss', 'web/static/src/legacy/scss/tooltip.scss', @@ -217,9 +216,7 @@ This module provides the core of the Odoo Web Client. # Form style should be computed before 'web/static/src/views/form/button_box/*.scss', - 'web/static/src/legacy/action_adapters.js', 'web/static/src/legacy/legacy_service_provider.js', - 'web/static/src/legacy/legacy_client_actions.js', 'web/static/src/legacy/legacy_dialog.js', 'web/static/src/legacy/legacy_load_views.js', 'web/static/src/legacy/legacy_promise_error_handler.js', @@ -229,24 +226,14 @@ This module provides the core of the Odoo Web Client. 'web/static/src/legacy/utils.js', 'web/static/src/legacy/pseudo_web_client.js', 'web/static/src/legacy/web_client.js', - 'web/static/src/legacy/js/chrome/*', - 'web/static/src/legacy/js/components/*', - 'web/static/src/legacy/js/control_panel/*', 'web/static/src/legacy/js/core/domain.js', - 'web/static/src/legacy/js/core/mvc.js', 'web/static/src/legacy/js/core/py_utils.js', 'web/static/src/legacy/js/core/context.js', 'web/static/src/legacy/js/core/misc.js', 'web/static/src/legacy/js/fields/*', 'web/static/src/legacy/js/services/data_manager.js', 'web/static/src/legacy/js/services/session.js', - 'web/static/src/legacy/js/tools/tools.js', - 'web/static/src/legacy/js/widgets/colorpicker_dialog.js', 'web/static/src/legacy/js/widgets/date_picker.js', - 'web/static/src/legacy/js/widgets/domain_selector_dialog.js', - 'web/static/src/legacy/js/widgets/domain_selector.js', - 'web/static/src/legacy/js/widgets/model_field_selector.js', - 'web/static/src/legacy/js/widgets/model_field_selector_popover.js', 'web/static/src/legacy/js/env.js', 'web/static/src/legacy/js/owl_compatibility.js', @@ -653,7 +640,6 @@ This module provides the core of the Odoo Web Client. # Legacy 'web/static/lib/tempusdominus/tempusdominus.js', 'web/static/tests/legacy/**/*.js', - ('remove', 'web/static/tests/legacy/**/*_mobile_tests.js'), ('remove', 'web/static/tests/legacy/helpers/**/*.js'), ('remove', 'web/static/tests/legacy/legacy_setup.js'), @@ -661,7 +647,6 @@ This module provides the core of the Odoo Web Client. ], 'web.qunit_mobile_suite_tests': [ 'web/static/tests/mobile/**/*.js', - 'web/static/tests/legacy/components/dropdown_menu_mobile_tests.js', ], # Used during the transition of the web architecture diff --git a/addons/web/static/src/legacy/action_adapters.js b/addons/web/static/src/legacy/action_adapters.js deleted file mode 100644 index fa24d42f823..00000000000 --- a/addons/web/static/src/legacy/action_adapters.js +++ /dev/null @@ -1,244 +0,0 @@ -/** @odoo-module **/ - -import Context from "web.Context"; -import core from "web.core"; -import { ComponentAdapter } from "web.OwlCompatibility"; -import { objectToQuery } from "../core/browser/router_service"; -import { useDebugCategory } from "../core/debug/debug_context"; -import { Dialog } from "../core/dialog/dialog"; -import { useService } from "@web/core/utils/hooks"; -import { cleanDomFromBootstrap, wrapSuccessOrFail, useLegacyRefs } from "./utils"; -import { mapDoActionOptionAPI } from "./backend_utils"; - -import { - Component, - onMounted, - onWillUnmount, - onWillUpdateProps, - useEffect, - useExternalListener, - xml, -} from "@odoo/owl"; - -class WarningDialog extends Component {} -WarningDialog.template = xml` -

-

`; -WarningDialog.components = { Dialog }; - -class ActionAdapter extends ComponentAdapter { - setup() { - super.setup(); - this.actionService = useService("action"); - this.router = useService("router"); - this.title = useService("title"); - this.notifications = useService("notification"); - this.dialogs = useService("dialog"); - this.wowlEnv = this.env; - const legacyRefs = useLegacyRefs(); - legacyRefs.component = this; - // a legacy widget widget can push_state anytime including during its async rendering - // In Wowl, we want to have all states pushed during the same setTimeout. - // This is protected in legacy (backward compatibility) but should not e supported in Wowl - this.tempQuery = {}; - let originalUpdateControlPanel; - useEffect( - () => { - legacyRefs.widget = this.widget; - const query = this.widget.getState(); - Object.assign(query, this.tempQuery); - this.tempQuery = null; - this.__widget = this.widget; - if (!this.wowlEnv.inDialog) { - this.pushState(query); - } - const onActionManagerUpdate = () => { - this.env.bus.trigger("close_dialogs"); - cleanDomFromBootstrap(); - }; - this.wowlEnv.bus.addEventListener("ACTION_MANAGER:UPDATE", onActionManagerUpdate); - originalUpdateControlPanel = this.__widget.updateControlPanel.bind(this.__widget); - this.__widget.updateControlPanel = (newProps) => { - this.wowlEnv.config.setDisplayName(this.__widget.getTitle()); - return originalUpdateControlPanel(newProps); - }; - core.bus.trigger("DOM_updated"); - - return () => { - this.__widget.updateControlPanel = originalUpdateControlPanel; - this.wowlEnv.bus.removeEventListener( - "ACTION_MANAGER:UPDATE", - onActionManagerUpdate - ); - }; - }, - () => [] - ); - useExternalListener(window, "click", () => { - cleanDomFromBootstrap(); - }); - - onWillUpdateProps(() => { - if (this.widget === null) { - this.widget = this.__widget; - } - }); - onWillUnmount(() => { - if (this.__widget && this.__widget.on_detach_callback) { - this.__widget.on_detach_callback(); - } - }); - - this.onScrollTo = (payload) => { - const contentEl = this.el.querySelector(".o_content"); - if (contentEl) { - contentEl.scrollLeft = payload.left || 0; - contentEl.scrollTop = payload.top || 0; - } - }; - } - - get actionId() { - throw new Error("Should be implement by specific adapters"); - } - - pushState(state) { - if (this.wowlEnv.inDialog) { - return; - } - const query = objectToQuery(state); - if (this.tempQuery) { - Object.assign(this.tempQuery, query); - return; - } - if (this.widget) { - const actionTitle = this.widget.getTitle(); - if (actionTitle) { - this.wowlEnv.config.setDisplayName(actionTitle); - } - } - if (this.props.onPushState) { - this.props.onPushState(query); - } else { - this.router.pushState(query); - } - } - - _trigger_up(ev) { - const payload = ev.data; - if (ev.name === "do_action") { - if (payload.action.context) { - payload.action.context = new Context(payload.action.context).eval(); - } - this.onReverseBreadcrumb = payload.options && payload.options.on_reverse_breadcrumb; - const legacyOptions = mapDoActionOptionAPI(payload.options); - wrapSuccessOrFail(this.actionService.doAction(payload.action, legacyOptions), payload); - } else if (ev.name === "breadcrumb_clicked") { - this.actionService.restore(payload.controllerID); - } else if (ev.name === "push_state") { - this.pushState(payload.state); - } else if (ev.name === "set_title_part") { - const { part, title } = payload; - this.title.setParts({ [part]: title || null }); - } else if (ev.name === "warning") { - if (payload.type === "dialog") { - this.dialogs.add(WarningDialog, { - title: payload.title, - message: payload.message, - }); - } else { - this.notifications.add(payload.message, { - className: payload.className, - sticky: payload.sticky, - title: payload.title, - type: "warning", - }); - } - } else if (ev.name === "history_back") { - this.wowlEnv.config.historyBack(); - } else if (ev.name === "scrollTo") { - this.onScrollTo(payload); - } else { - super._trigger_up(ev); - } - } - - /** - * This function is called just before the component will be unmounted, - * because it will be replaced by another one. However, we need to keep it - * alive, because we might come back to this one later. We thus return the - * widget instance, and set this.widget to null so that it is not destroyed - * by the compatibility layer. That instance will be destroyed by the - * ActionManager service when it will be removed from the controller stack, - * and if we ever come back to that controller, the instance will be given - * in props so that we can re-use it. - */ - exportState() { - this.widget = null; - return { - __legacy_widget__: this.__widget, - __on_reverse_breadcrumb__: this.onReverseBreadcrumb, - }; - } - - canBeRemoved() { - return this.__widget.canBeRemoved(); - } - - __destroy() { - if (this.actionService.__legacy__isActionInStack(this.actionId)) { - this.widget = null; - } - super.__destroy(...arguments); - } - get el() { - return (this.widget || this.__widget).el; - } -} - -export class ClientActionAdapter extends ActionAdapter { - setup() { - super.setup(); - useDebugCategory("action", { action: this.props.widgetArgs[0] }); - onMounted(() => { - const action = this.props.widgetArgs[0]; - if ("params" in action) { - const newState = {}; - Object.entries(action.params).forEach(([k, v]) => { - if (typeof v === "string" || typeof v === "number") { - newState[k] = v; - } - }); - this.wowlEnv.services.router.pushState(newState); - } - }); - this.env = Component.env; - } - - get actionId() { - return this.props.widgetArgs[0].jsId; - } - - async onWillStart() { - if (this.props.widget) { - this.widget = this.props.widget; - this.widget.setParent(this); - if (this.props.onReverseBreadcrumb) { - await this.props.onReverseBreadcrumb(); - } - return this.updateWidget(); - } - return super.onWillStart(); - } - - /** - * @override - */ - updateWidget() { - return this.widget.do_show(); - } - - do_push_state(state) { - this.pushState(state); - } -} diff --git a/addons/web/static/src/legacy/js/chrome/abstract_action.js b/addons/web/static/src/legacy/js/chrome/abstract_action.js deleted file mode 100644 index 8ac1386619a..00000000000 --- a/addons/web/static/src/legacy/js/chrome/abstract_action.js +++ /dev/null @@ -1,82 +0,0 @@ -/** @odoo-module alias=web.AbstractAction **/ - -/** - * We define here the AbstractAction widget, which implements the ActionMixin. - * All client actions must extend this widget. - * - * @module @web/chrome/abstract_action - */ - -import ActionMixin from "web.ActionMixin"; -import ControlPanel from "web.ControlPanel"; -import Widget from "web.Widget"; -import { ComponentWrapper } from "web.OwlCompatibility"; - -const AbstractAction = Widget.extend(ActionMixin, { - config: { - ControlPanel: ControlPanel, - }, - - /** - * If this flag is set to true, the client action will create a control - * panel whenever it is created. - * - * @type boolean - */ - hasControlPanel: false, - - /** - * @override - * - * @param {Widget} parent - * @param {Object} action - * @param {Object} [options] - */ - init: function (parent, action, options) { - this._super(parent); - this._title = action.display_name || action.name; - - if (this.hasControlPanel) { - this.controlPanelProps = { - action, - breadcrumbs: options && options.breadcrumbs, - }; - } - }, - /** - * @override - */ - start: async function () { - await this._super(...arguments); - if (this.hasControlPanel) { - if ('title' in this.controlPanelProps) { - this._setTitle(this.controlPanelProps.title); - } - this.controlPanelProps.title = this.getTitle(); - this._controlPanelWrapper = new ComponentWrapper(this, this.config.ControlPanel, this.controlPanelProps); - await this._controlPanelWrapper.mount(this.el, { position: 'first-child' }); - - } - }, - /** - * @override - */ - destroy: function() { - this._super.apply(this, arguments); - ActionMixin.destroy.call(this); - }, - /** - * @override - */ - on_attach_callback: function () { - ActionMixin.on_attach_callback.call(this); - }, - /** - * @override - */ - on_detach_callback: function () { - ActionMixin.on_detach_callback.call(this); - }, -}); - -export default AbstractAction; diff --git a/addons/web/static/src/legacy/js/chrome/action_mixin.js b/addons/web/static/src/legacy/js/chrome/action_mixin.js deleted file mode 100644 index 4b2f32a8079..00000000000 --- a/addons/web/static/src/legacy/js/chrome/action_mixin.js +++ /dev/null @@ -1,181 +0,0 @@ -/** @odoo-module alias=web.ActionMixin **/ - - /** - * We define here the ActionMixin, the generic notion of action (from the point - * of view of the web client). In short, an action is a widget which controls - * the main part of the screen (everything below the navbar). - * - * More precisely, the action manager is the component that coordinates a stack - * of actions. Whenever the user navigates in the interface, switches views, - * open different menus, the action manager creates/updates/destroys special - * widgets which implements the ActionMixin. These actions need to answer to a - * standardised API, which is the reason for this mixin. - * - * In practice, most actions are view controllers (coming from an - * ir.action.act_window). However, some actions are 'client actions'. They - * also need to implement the ActionMixin for a better cooperation with the - * action manager. - * - * @module web.ActionMixin - * @extends WidgetAdapterMixin - */ - - import core from "web.core"; - import { WidgetAdapterMixin } from "web.OwlCompatibility"; - - const ActionMixin = Object.assign({}, WidgetAdapterMixin, { - template: 'Action', - - /** - * The action mixin assumes that it is rendered with the 'Action' template. - * This template has a special zone ('.o_content') where the content should - * be added. Actions that want to automatically render a template there - * should define the contentTemplate key. In short, client actions should - * probably define a contentTemplate key, and not a template key. - */ - contentTemplate: null, - - /** - * If an action wants to use a control panel, it will be created and - * registered in this _controlPanel key (the widget). The way this control - * panel is created is up to the implementation (so, view controllers or - * client actions may have different needs). - * - * Note that most of the time, this key should be set by the framework, not - * by the code of the client action. - */ - _controlPanel: null, - - /** - * String containing the title of the client action (which may be needed to - * display in the breadcrumbs zone of the control panel). - * - * @see _setTitle - */ - _title: '', - - /** - * @override - */ - renderElement: function () { - this._super.apply(this, arguments); - if (this.contentTemplate) { - const content = core.qweb.render(this.contentTemplate, { widget: this }); - this.$('.o_content').append(content); - } - }, - - /** - * Called by the action manager when action is restored (typically, when - * the user clicks on the action in the breadcrumb) - * - * @returns {Promise|undefined} - */ - willRestore: function () { }, - - //--------------------------------------------------------------------- - // Public - //--------------------------------------------------------------------- - - /** - * In some situations, we need confirmation from the controller that the - * current state can be destroyed without prejudice to the user. For - * example, if the user has edited a form, maybe we should ask him if we - * can discard all his changes when we switch to another action. In that - * case, the action manager will call this method. If the returned - * promise is successfully resolved, then we can destroy the current action, - * otherwise, we need to stop. - * - * @returns {Promise} resolved if the action can be removed, rejected - * otherwise - */ - canBeRemoved: function () { - return Promise.resolve(); - }, - - /** - * Returns a serializable state that will be pushed in the URL by - * the action manager, allowing the action to be restarted correctly - * upon refresh. This function should be overriden to add extra information. - * Note that some keys are reserved by the framework and will thus be - * ignored ('action', 'active_id', 'active_ids' and 'title', for all - * actions, and 'model' and 'view_type' for act_window actions). - * - * @returns {Object} - */ - getState: function () { - return {}; - }, - - /** - * Returns a title that may be displayed in the breadcrumb area. For - * example, the name of the record (for a form view). This is actually - * important for the action manager: this is the way it is able to give - * the proper titles for other actions. - * - * @returns {string} - */ - getTitle: function () { - return this._title; - }, - - /** - * Renders the buttons to append, in most cases, to the control panel (in - * the bottom left corner). When the action is rendered in a dialog, those - * buttons might be moved to the dialog's footer. - * - * @param {jQuery Node} $node - */ - renderButtons: function ($node) { }, - - /** - * Method used to update the widget buttons state. - */ - updateButtons: function () { }, - - /** - * The parameter newProps is used to update the props of - * the controlPanelWrapper before render it. The key 'cp_content' - * is not a prop of the control panel itself. One should if possible use - * the slot mechanism. - * - * @param {Object} [newProps={}] - * @returns {Promise} - */ - updateControlPanel: async function (newProps = {}) { - if (!this.withControlPanel && !this.hasControlPanel) { - return; - } - const props = Object.assign({}, newProps); // Work with a clean new object - if ('title' in props) { - this._setTitle(props.title); - this.controlPanelProps.title = this.getTitle(); - delete props.title; - } - if ('cp_content' in props) { - // cp_content has been updated: refresh it. - this.controlPanelProps.cp_content = Object.assign({}, - this.controlPanelProps.cp_content, - props.cp_content, - ); - delete props.cp_content; - } - // Update props state - Object.assign(this.controlPanelProps, props); - return this._controlPanelWrapper.update(this.controlPanelProps); - }, - - //--------------------------------------------------------------------- - // Private - //--------------------------------------------------------------------- - - /** - * @private - * @param {string} title - */ - _setTitle: function (title) { - this._title = title; - }, - }); - - export default ActionMixin; diff --git a/addons/web/static/src/legacy/js/components/action_menus.js b/addons/web/static/src/legacy/js/components/action_menus.js deleted file mode 100644 index c93e6e976f6..00000000000 --- a/addons/web/static/src/legacy/js/components/action_menus.js +++ /dev/null @@ -1,206 +0,0 @@ -/** @odoo-module alias=web.ActionMenus **/ - - import Context from "web.Context"; - import DropdownMenu from "web.DropdownMenu"; - import Registry from "web.Registry"; - import { LegacyComponent } from "@web/legacy/legacy_component"; - - const { - onMounted, - onPatched, - onWillStart, - onWillUpdateProps, - } = owl; - - let registryActionId = 1; - - /** - * Action menus (or Action/Print bar, previously called 'Sidebar') - * - * The side bar is the group of dropdown menus located on the left side of the - * control panel. Its role is to display a list of items depending on the view - * type and selected records and to execute a set of actions on active records. - * It is made out of 2 dropdown menus: Print and Action. - * - * This component also provides a registry to use custom components in the ActionMenus's - * Action menu. - * @extends Component - */ - class ActionMenus extends LegacyComponent { - setup() { - this.actionButtonStrings = { - title: this.env._t("Action"), - hotkey: this.env._t("Additionnal actions"), - }; - this.printButtonStrings = { - title: this.env._t("Print"), - hotkey: this.env._t("Printing options"), - }; - onWillStart(async () => { - this.actionItems = await this._setActionItems(this.props); - this.printItems = await this._setPrintItems(this.props); - }); - onWillUpdateProps(async (nextProps) => { - this.actionItems = await this._setActionItems(nextProps); - this.printItems = await this._setPrintItems(nextProps); - }); - onMounted(() => { - this._addTooltips(); - }); - onPatched(() => { - this._addTooltips(); - }); - } - - //--------------------------------------------------------------------- - // Private - //--------------------------------------------------------------------- - - /** - * Add the tooltips to the items - * @private - */ - _addTooltips() { - $(this.el.querySelectorAll('[title]')).tooltip({ - delay: { show: 500, hide: 0 } - }); - } - - /** - * @private - * @param {Object} props - * @returns {Promise} - */ - async _setActionItems(props) { - // Callback based actions - const callbackActions = (props.items.other || []).map( - action => Object.assign({ key: `action-${action.description}` }, action) - ); - // Action based actions - const actionActions = props.items.action || []; - const relateActions = props.items.relate || []; - const formattedActions = [...actionActions, ...relateActions].map( - action => ({ action, description: action.name, key: action.id }) - ); - // ActionMenus action registry components - const registryActions = []; - const rpc = this.rpc.bind(this); - for (const { Component, getProps } of this.constructor.registry.values()) { - const itemProps = await getProps(props, this.env, rpc); - if (itemProps) { - registryActions.push({ - Component, - key: `registry-action-${registryActionId++}`, - props: itemProps, - }); - } - } - - return [...callbackActions, ...formattedActions, ...registryActions]; - } - - /** - * @private - * @param {Object} props - * @returns {Promise} - */ - async _setPrintItems(props) { - const printActions = props.items.print || []; - const printItems = printActions.map( - action => ({ action, description: action.name, key: action.id }) - ); - return printItems; - } - - //--------------------------------------------------------------------- - // Handlers - //--------------------------------------------------------------------- - - /** - * Perform the action for the item clicked after getting the data - * necessary with a trigger. - * @private - * @param {OwlEvent} ev - */ - async _executeAction(action) { - let activeIds = this.props.activeIds; - if (this.props.isDomainSelected) { - activeIds = await this.rpc({ - model: this.env.action.res_model, - method: 'search', - args: [this.props.domain], - kwargs: { - limit: this.env.session.active_ids_limit, - context: this.props.context, - }, - }); - } - const activeIdsContext = { - active_id: activeIds[0], - active_ids: activeIds, - active_model: this.env.action.res_model, - }; - if (this.props.domain) { - // keep active_domain in context for backward compatibility - // reasons, and to allow actions to bypass the active_ids_limit - activeIdsContext.active_domain = this.props.domain; - } - - const context = new Context(this.props.context, activeIdsContext).eval(); - const result = await this.rpc({ - route: '/web/action/load', - params: { action_id: action.id, context }, - }); - result.context = new Context(result.context || {}, activeIdsContext) - .set_eval_context(context); - this.trigger('do-action', { - action: result, - options: { - on_close: () => this.trigger('reload'), - }, - }); - } - - /** - * Handler used to determine which way must be used to execute a selected - * action: it will be either: - * - a callback (function given by the view controller); - * - an action ID (string); - * - an URL (string). - * @private - * @param {OwlEvent} ev - */ - _onItemSelected(ev) { - ev.stopPropagation(); - const { item } = ev.detail; - if (item.callback) { - item.callback([item]); - } else if (item.action) { - this._executeAction(item.action); - } else if (item.url) { - // Event has been prevented at its source: we need to redirect manually. - this.env.services.navigate(item.url); - } - } - } - - ActionMenus.registry = new Registry(); - - ActionMenus.components = { DropdownMenu }; - ActionMenus.props = { - activeIds: { type: Array, element: [Number, String] }, // virtual IDs are strings. - context: Object, - domain: { type: Array, optional: 1 }, - isDomainSelected: { type: Boolean, optional: 1 }, - items: { - type: Object, - shape: { - action: { type: Array, optional: 1 }, - print: { type: Array, optional: 1 }, - other: { type: Array, optional: 1 }, - }, - }, - }; - ActionMenus.template = 'web.Legacy.ActionMenus'; - - export default ActionMenus; diff --git a/addons/web/static/src/legacy/js/components/custom_file_input.js b/addons/web/static/src/legacy/js/components/custom_file_input.js deleted file mode 100644 index e46f75b17be..00000000000 --- a/addons/web/static/src/legacy/js/components/custom_file_input.js +++ /dev/null @@ -1,87 +0,0 @@ -/** @odoo-module alias=web.CustomFileInput **/ - - import { LegacyComponent } from "@web/legacy/legacy_component"; - const { useRef } = owl; - - /** - * Custom file input - * - * Component representing a customized input of type file. It takes a sub-template - * in its default t-slot and uses it as the trigger to open the file upload - * prompt. - * @extends Component - */ - class CustomFileInput extends LegacyComponent { - /** - * @param {Object} [props] - * @param {string} [props.accepted_file_extensions='*'] Comma-separated - * list of authorized file extensions (default to all). - * @param {string} [props.action='/web/binary/upload'] Route called when - * a file is uploaded in the input. - * @param {string} [props.id] - * @param {string} [props.model] - * @param {string} [props.multi_upload=false] Whether the input should allow - * to upload multiple files at once. - */ - setup() { - this.fileInputRef = useRef('file-input'); - } - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Upload an attachment to the given action with the given parameters: - * - ufile: list of files contained in the file input - * - csrf_token: CSRF token provided by the odoo global object - * - model: a specific model which will be given when creating the attachment - * - id: the id of the model target instance - * @private - */ - async _onFileInputChange() { - const { action, model, id } = this.props; - const params = { - csrf_token: odoo.csrf_token, - ufile: [...this.fileInputRef.el.files], - }; - if (model) { - params.model = model; - } - if (id) { - params.id = id; - } - const fileData = await this.env.services.httpRequest(action, params, 'text'); - const parsedFileData = JSON.parse(fileData); - if (parsedFileData.error) { - throw new Error(parsedFileData.error); - } - this.trigger('uploaded', { files: parsedFileData }); - } - - /** - * Redirect clicks from the trigger element to the input. - * @private - */ - _onTriggerClicked() { - this.fileInputRef.el.click(); - } - } - CustomFileInput.defaultProps = { - accepted_file_extensions: '*', - action: '/web/binary/upload', - multi_upload: false, - onUpload: () => {}, - }; - CustomFileInput.props = { - accepted_file_extensions: { type: String, optional: true }, - action: { type: String, optional: true }, - id: { type: Number, optional: true }, - model: { type: String, optional: true }, - multi_upload: { type: Boolean, optional: true }, - onUpload: { type: Function, optional: true }, - slots: { type: Object, optional: true }, - }; - CustomFileInput.template = 'web.CustomFileInput'; - - export default CustomFileInput; diff --git a/addons/web/static/src/legacy/js/components/dropdown_menu.js b/addons/web/static/src/legacy/js/components/dropdown_menu.js deleted file mode 100644 index e459c54ea26..00000000000 --- a/addons/web/static/src/legacy/js/components/dropdown_menu.js +++ /dev/null @@ -1,189 +0,0 @@ -/** @odoo-module alias=web.DropdownMenu **/ - - import { _t } from "web.core"; - import DropdownMenuItem from "web.DropdownMenuItem"; - import { LegacyComponent } from "@web/legacy/legacy_component"; - - const { useExternalListener, useRef, useState } = owl; - - /** - * Dropdown menu - * - * Generic component used to generate a list of interactive items. It uses some - * bootstrap classes but most interactions are handled in here or in the dropdown - * menu item class definition, including some keyboard navigation and escaping - * system (click outside to close the dropdown). - * - * The layout of a dropdown menu is as following: - * > a Button (always rendered) with a `title` and an optional `icon`; - * > a Dropdown (rendered when open) containing a collection of given items. - * These items must be objects and can have two shapes: - * 1. item.Component & item.props > will instantiate the given Component with - * the given props. Any additional key will be useless. - * 2. any other shape > will instantiate a DropdownMenuItem with the item - * object being its props. There is no props validation as this object - * will be passed as-is when `selected` and can contain additional meta-keys - * that will not affect the displayed item. For more information regarding - * the behaviour of these items, @see DropdownMenuItem. - * @extends Component - */ - class DropdownMenu extends LegacyComponent { - setup() { - this.dropdownMenu = useRef('dropdown'); - this.state = useState({ open: false }); - - useExternalListener(window, 'click', this._onWindowClick, true); - useExternalListener(window, 'keydown', this._onWindowKeydown); - } - - //--------------------------------------------------------------------- - // Getters - //--------------------------------------------------------------------- - - /** - * In desktop, by default, we do not display a caret icon next to the - * dropdown. - * @returns {boolean} - */ - get displayCaret() { - return false; - } - - /** - * In mobile, by default, we display a chevron icon next to the dropdown - * button. Note that when 'displayCaret' is true, we display a caret - * instead of a chevron, no matter the value of 'displayChevron'. - * @returns {boolean} - */ - get displayChevron() { - return this.env.device.isMobile; - } - - /** - * In mobile, by default, we use the middle of the screen as a threshold value to choose - * the alignment of the open dropdown against its triggering button. - * - * FIXME: Needs to be adapted for desktop, current calculation are only relevant on mobile screen - * - * @return {string} Bootstrap's dropdown alignment class - */ - get dropdownMenuAlignClass() { - if (this.env.device.isMobile) { - const threshold = document.documentElement.clientWidth / 2; - const { left, right } = this.el.getBoundingClientRect(); - if (_t.database.parameters.direction === 'rtl') { - return right > threshold ? 'dropdown-menu-start' : 'dropdown-menu-end'; - } - return left > threshold ? 'dropdown-menu-end' : 'dropdown-menu-start'; - } - return ''; - } - - /** - * Can be overriden to force an icon on an inheriting class. - * @returns {string} Font Awesome icon class - */ - get icon() { - return this.props.icon; - } - - /** - * Meant to be overriden to provide the list of items to display. - * @returns {Object[]} - */ - get items() { - return this.props.items; - } - - /** - * @returns {string} - */ - get title() { - return this.props.title; - } - - //--------------------------------------------------------------------- - // Handlers - //--------------------------------------------------------------------- - - /** - * @private - * @param {KeyboardEvent} ev - */ - _onButtonKeydown(ev) { - switch (ev.key) { - case 'ArrowLeft': - case 'ArrowRight': - case 'ArrowUp': - case 'ArrowDown': - const firstItem = this.el.querySelector('.dropdown-item'); - if (firstItem) { - ev.preventDefault(); - firstItem.focus(); - } - } - } - - /** - * @private - * @param {OwlEvent} ev - */ - _onItemSelected(/* ev */) { - if (this.props.closeOnSelected) { - this.state.open = false; - } - } - - /** - * @private - * @param {MouseEvent} ev - */ - _onWindowClick(ev) { - if ( - this.state.open && - !this.el.contains(ev.target) && - !this.el.contains(document.activeElement) - ) { - if (document.body.classList.contains("modal-open")) { - // retrieve the active modal and check if the dropdown is a child of this modal - const modal = this.el.closest(".modal[role='dialog']:not(.o_inactive_modal)") - if (!modal) { - return; - } - } - // check for an active open bootstrap calendar like the filter dropdown inside the search panel) - if (document.querySelector('body > .bootstrap-datetimepicker-widget')) { - return; - } - this.state.open = false; - } - } - - /** - * @private - * @param {KeyboardEvent} ev - */ - _onWindowKeydown(ev) { - if (this.state.open && ev.key === 'Escape') { - this.state.open = false; - } - } - } - - DropdownMenu.components = { DropdownMenuItem }; - DropdownMenu.defaultProps = { items: [] }; - DropdownMenu.props = { - icon: { type: String, optional: 1 }, - items: { - type: Array, - element: Object, - optional: 1, - }, - title: { type: String, optional: 1 }, - closeOnSelected: { type: Boolean, optional: 1 }, - hotkey: { type: String, optional: 1 }, - hotkeyTitle: { type: String, optional: 1 }, - }; - DropdownMenu.template = 'web.DropdownMenu'; - - export default DropdownMenu; diff --git a/addons/web/static/src/legacy/js/components/dropdown_menu_item.js b/addons/web/static/src/legacy/js/components/dropdown_menu_item.js deleted file mode 100644 index 18558b3c41d..00000000000 --- a/addons/web/static/src/legacy/js/components/dropdown_menu_item.js +++ /dev/null @@ -1,127 +0,0 @@ -/** @odoo-module alias=web.DropdownMenuItem **/ - - import { useListener } from "@web/core/utils/hooks"; - import { LegacyComponent } from "@web/legacy/legacy_component"; - - const { useExternalListener, useRef, useState } = owl; - - /** - * Dropdown menu item - * - * Generic component instantiated by a dropdown menu (@see DropdownMenu) in - * the absence of `Component` and `props` keys in a given item object. - * - * In its simplest form, a dropdown menu item will be given a description (optional, - * but highly recommended) and will trigger a 'select-item' when clicked on. - * Additionaly it can receive the following props: - * - isActive: will add a `checked` symbol on the left side of the item - * - removable: will add a `remove` trash icon on the right side of the item. - * when clicked, will trigger a 'remove-item' event. - * - options: will change the behaviour of the item ; instead of triggering - * an event, the item will act as a nested dropdown menu and display - * its given options. These will have the same definition as another - * dropdown item but cannot have options of their own. - * - * It is recommended to extend this class when defining a Component which will - * be put inside of a dropdown menu (@see CustomFilterItem as example). - * @extends Component - */ - class DropdownMenuItem extends LegacyComponent { - setup() { - this.canBeOpened = Boolean(this.props.options && this.props.options.length); - - this.fallbackFocusRef = useRef('fallback-focus'); - this.state = useState({ open: false }); - - useExternalListener(window, 'click', this._onWindowClick); - useListener('keydown', this._onKeydown); - } - - //--------------------------------------------------------------------- - // Handlers - //--------------------------------------------------------------------- - - /** - * @private - * @param {KeyboardEvent} ev - */ - _onKeydown(ev) { - if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) { - return; - } - // Inspired from BS5 dropdown - if (['ArrowUp', 'ArrowDown'].includes(ev.key)) { - const { key, target } = ev; - const items = [].concat(...Element.prototype.querySelectorAll.call(this.el.parentElement, '.o-dropdown-menu .dropdown-item:not(.disabled):not(:disabled)')) - - if (!items.length) { - return - } - const shouldGetNext = key === 'ArrowDown'; - - const isCycleAllowed = !items.includes(target); - - let index = items.indexOf(target) - - // if the element does not exist in the list return an element depending on the direction and if cycle is allowed - if (index === -1) { - items[!shouldGetNext && isCycleAllowed ? items.length - 1 : 0].focus(); - } else { - const listLength = items.length - - index += shouldGetNext ? 1 : -1 - - if (isCycleAllowed) { - index = (index + listLength) % listLength - } - - items[Math.max(0, Math.min(index, listLength - 1))].focus(); - } - } - switch (ev.key) { - case 'ArrowLeft': - if (this.canBeOpened && this.state.open) { - ev.preventDefault(); - if (this.fallbackFocusRef.el) { - this.fallbackFocusRef.el.focus(); - } - this.state.open = false; - } - break; - case 'ArrowRight': - if (this.canBeOpened && !this.state.open) { - ev.preventDefault(); - this.state.open = true; - } - break; - case 'Escape': - ev.target.blur(); - if (this.canBeOpened && this.state.open) { - ev.preventDefault(); - ev.stopPropagation(); - if (this.fallbackFocusRef.el) { - this.fallbackFocusRef.el.focus(); - } - this.state.open = false; - } - } - } - - /** - * @private - * @param {MouseEvent} ev - */ - _onWindowClick(ev) { - if ( - this.state.open && - !this.el.contains(ev.target) && - !this.el.contains(document.activeElement) - ) { - this.state.open = false; - } - } - } - - DropdownMenuItem.template = 'web.DropdownMenuItem'; - - export default DropdownMenuItem; diff --git a/addons/web/static/src/legacy/js/components/pager.js b/addons/web/static/src/legacy/js/components/pager.js deleted file mode 100644 index 6b552f27c87..00000000000 --- a/addons/web/static/src/legacy/js/components/pager.js +++ /dev/null @@ -1,224 +0,0 @@ -/** @odoo-module alias=web.Pager **/ - - import { useAutofocus } from "@web/core/utils/hooks"; - import { LegacyComponent } from "@web/legacy/legacy_component"; - - const { onWillUpdateProps, useState } = owl; - - /** - * Pager - * - * The pager goes from 1 to size (included). - * The current value is currentMinimum if limit === 1 or the interval: - * [currentMinimum, currentMinimum + limit[ if limit > 1]. - * The value can be manually changed by clicking on the pager value and giving - * an input matching the pattern: min[,max] (in which the comma can be a dash - * or a semicolon). - * The pager also provides two buttons to quickly change the current page (next - * or previous). - * @extends Component - */ - class Pager extends LegacyComponent { - /** - * @param {Object} [props] - * @param {int} [props.size] the total number of elements - * @param {int} [props.currentMinimum] the first element of the current_page - * @param {int} [props.limit] the number of elements per page - * @param {boolean} [props.editable] editable feature of the pager - * @param {function} [props.validate] callback returning a Promise to - * validate changes - * @param {boolean} [props.withAccessKey] can be disabled, for example, - * for x2m widgets - */ - setup() { - this.state = useState({ - disabled: false, - editing: false, - }); - - useAutofocus(); - - onWillUpdateProps(this.onWillUpdateProps); - } - - async onWillUpdateProps() { - this.state.editing = false; - this.state.disabled = false; - } - - //--------------------------------------------------------------------- - // Getters - //--------------------------------------------------------------------- - - /** - * @returns {number} - */ - get maximum() { - return Math.min(this.props.currentMinimum + this.props.limit - 1, this.props.size); - } - - /** - * @returns {boolean} true iff there is only one page - */ - get singlePage() { - return (1 === this.props.currentMinimum) && (this.maximum === this.props.size); - } - - /** - * @returns {number} - */ - get value() { - return this.props.currentMinimum + (this.props.limit > 1 ? `-${this.maximum}` : ''); - } - - //--------------------------------------------------------------------- - // Private - //--------------------------------------------------------------------- - - /** - * Update the pager's state according to a pager action - * @private - * @param {number} [direction] the action (previous or next) on the pager - */ - async _changeSelection(direction) { - try { - await this.props.validate(); - } catch { - return; - } - const { limit, size } = this.props; - - // Compute the new currentMinimum - let currentMinimum = (this.props.currentMinimum + limit * direction); - if (currentMinimum > size) { - currentMinimum = 1; - } else if ((currentMinimum < 1) && (limit === 1)) { - currentMinimum = size; - } else if ((currentMinimum < 1) && (limit > 1)) { - currentMinimum = size - ((size % limit) || limit) + 1; - } - - // The re-rendering of the pager must be done before the trigger of - // event 'pager-changed' as the rendering may enable the pager - // (and a common use is to disable the pager when this event is - // triggered, and to re-enable it when the data have been reloaded). - this._updateAndDisable(currentMinimum, limit); - } - - /** - * Save the state from the content of the input - * @private - * @param {string} value the new raw pager value - * @returns {Promise} - */ - async _saveValue(value) { - try { - await this.props.validate(); - } catch { - return; - } - const [min, max] = value.trim().split(/\s*[\-\s,;]\s*/); - - let currentMinimum = Math.max(Math.min(parseInt(min, 10), this.props.size), 1); - let maximum = max ? Math.max(Math.min(parseInt(max, 10), this.props.size), 1) : min; - - if ( - !isNaN(currentMinimum) && - !isNaN(maximum) && - currentMinimum <= maximum - ) { - const limit = Math.max(maximum - currentMinimum) + 1; - this._updateAndDisable(currentMinimum, limit); - } - } - - /** - * Commits the current input value. There are two scenarios: - * - the value is the same: the pager toggles back to readonly - * - the value changed: the pager is disabled to prevent furtherchanges - * Either way the "pager-changed" event is triggered to reload the - * view. - * @private - * @param {number} currentMinimum - * @param {number} limit - */ - _updateAndDisable(currentMinimum, limit) { - if ( - currentMinimum !== this.props.currentMinimum || - limit !== this.props.limit - ) { - this.state.disabled = true; - } else { - // In this case we want to trigger an update, but since it will - // not re-render the pager (current props === next props) we - // have to disable the edition manually here. - this.state.editing = false; - } - this.props.onPagerChanged({ currentMinimum, limit }); - } - - //--------------------------------------------------------------------- - // Handlers - //--------------------------------------------------------------------- - - /** - * @private - */ - _onEdit() { - if ( - this.props.editable && // editable - !this.state.editing && // not already editing - !this.state.disabled // not being changed already - ) { - this.state.editing = true; - } - } - - /** - * @private - * @param {InputEvent} ev - */ - _onValueChange(ev) { - this._saveValue(ev.currentTarget.value); - if (!this.state.disabled) { - ev.preventDefault(); - } - } - - /** - * @private - * @param {KeyboardEvent} ev - */ - _onValueKeydown(ev) { - switch (ev.key) { - case 'Enter': - ev.preventDefault(); - ev.stopPropagation(); - this._saveValue(ev.currentTarget.value); - break; - case 'Escape': - ev.preventDefault(); - ev.stopPropagation(); - this.state.editing = false; - break; - } - } - } - - Pager.defaultProps = { - editable: true, - validate: async () => { }, - withAccessKey: true, - }; - Pager.props = { - currentMinimum: { type: Number, optional: 1 }, - editable: { type: Boolean, optional: true }, - limit: { validate: l => !isNaN(l), optional: 1 }, - size: { type: Number, optional: 1 }, - validate: { type: Function, optional: true }, - withAccessKey: { type: Boolean, optional: true }, - onPagerChanged: Function, - }; - Pager.template = 'web.legacy.Pager'; - - export default Pager; diff --git a/addons/web/static/src/legacy/js/control_panel/control_panel.js b/addons/web/static/src/legacy/js/control_panel/control_panel.js deleted file mode 100644 index 255ad8ecae8..00000000000 --- a/addons/web/static/src/legacy/js/control_panel/control_panel.js +++ /dev/null @@ -1,181 +0,0 @@ -/** @odoo-module alias=web.ControlPanel **/ - - import ActionMenus from "web.ActionMenus"; - import Pager from "web.Pager"; - import { LegacyComponent } from "@web/legacy/legacy_component"; - import { sprintf } from "@web/core/utils/strings"; - - const { - onMounted, - onPatched, - onWillDestroy, - onWillUpdateProps, - toRaw, - useRef, - useSubEnv, - } = owl; - - /** - * TODO: remove this whole mechanism as soon as `cp_content` is completely removed. - * Extract the 'cp_content' key of the given props and return them as well as - * the extracted content. - * @param {Object} props - * @returns {Object} - */ - function getAdditionalContent(props) { - const additionalContent = {}; - if ('cp_content' in props) { - const content = props.cp_content || {}; - if ('$buttons' in content) { - additionalContent.buttons = content.$buttons; - } - if ('$searchview' in content) { - additionalContent.searchView = content.$searchview; - } - if ('$pager' in content) { - additionalContent.pager = content.$pager; - } - if ('$searchview_buttons' in content) { - additionalContent.searchViewButtons = content.$searchview_buttons; - } - } - return additionalContent; - } - - /** - * Control panel - * - * The control panel of the action|view. In its standard form, it is composed of - * several sections/subcomponents. - * - * Note: an additional temporary (and ugly) mechanic allows to inject a jQuery element - * given in `props.cp_content` in a related section: - * $buttons -> [Buttons] - * $searchview -> [Search View] - * $searchview_buttons -> [Search Menus] - * $pager -> [Pager] - * This system must be replaced by proper slot usage and the static template - * inheritance mechanism when converting the views/actions. - * @extends Component - */ - class ControlPanel extends LegacyComponent { - setup() { - this.additionalContent = getAdditionalContent(this.props); - - let subEnvView = this.props.view; - useSubEnv({ - action: this.props.action, - get view() { - return subEnvView; - }, - }); - - // Reference hooks - this.contentRefs = { - buttons: useRef('buttons'), - pager: useRef('pager'), - searchView: useRef('searchView'), - searchViewButtons: useRef('searchViewButtons'), - }; - - this.fields = this._formatFields(toRaw(this.props.fields || {})); - - this.sprintf = sprintf; - - onWillDestroy(() => { - const content = this.props.cp_content; - if (content) { - if (content.$buttons) { - content.$buttons.remove(); - } - if (content.$searchview) { - content.$searchview.remove(); - } - if (content.$pager) { - content.$pager.remove(); - } - if (content.$searchview_buttons) { - content.$searchview_buttons.remove(); - } - } - }); - - // Cannot use useEffect. See prepareForFinish in owl_compatibility.js - onMounted(() => this._attachAdditionalContent()); - onPatched(() => this._attachAdditionalContent()); - onWillUpdateProps((nextProps) => { - // Note: action and searchModel are not likely to change during - // the lifespan of a ControlPanel instance, so we only need to update - // the view information. - if ("view" in nextProps) { - subEnvView = nextProps.view; - } - if ("fields" in nextProps) { - this.fields = this._formatFields(toRaw(nextProps.fields)); - } - this.additionalContent = getAdditionalContent(nextProps); - }); - } - - //--------------------------------------------------------------------- - // Private - //--------------------------------------------------------------------- - - /** - * Attach additional content extracted from the props 'cp_content' key, if any. - * @private - */ - _attachAdditionalContent() { - for (const key in this.additionalContent) { - if (this.additionalContent[key] && this.additionalContent[key].length) { - const target = this.contentRefs[key].el; - if (target) { - target.innerHTML = ""; - target.append(...this.additionalContent[key]); - } - } - } - } - - /** - * Give `name` and `description` keys to the fields given to the control - * panel. - * @private - * @param {Object} fields - * @returns {Object} - */ - _formatFields(fields) { - const formattedFields = {}; - for (const fieldName in fields) { - formattedFields[fieldName] = Object.assign({ - description: fields[fieldName].string, - name: fieldName, - }, fields[fieldName]); - } - return formattedFields; - } - } - ControlPanel.components = { - ActionMenus, Pager, - }; - ControlPanel.defaultProps = { - breadcrumbs: [], - fields: {}, - views: [], - withBreadcrumbs: true, - }; - ControlPanel.props = { - action: Object, - breadcrumbs: { type: Array, optional: true }, - cp_content: { type: Object, optional: 1 }, - fields: { type: Object, optional: true }, - pager: { validate: p => typeof p === 'object' || p === null, optional: 1 }, - actionMenus: { validate: s => typeof s === 'object' || s === null, optional: 1 }, - title: { type: String, optional: 1 }, - view: { type: Object, optional: 1 }, - views: { type: Array, optional: true }, - withBreadcrumbs: { type: Boolean, optional: true }, - }; - ControlPanel.template = 'web.Legacy.ControlPanel'; - - export default ControlPanel; diff --git a/addons/web/static/src/legacy/js/control_panel/search_utils.js b/addons/web/static/src/legacy/js/control_panel/search_utils.js deleted file mode 100644 index df654791ab1..00000000000 --- a/addons/web/static/src/legacy/js/control_panel/search_utils.js +++ /dev/null @@ -1,547 +0,0 @@ -/** @odoo-module alias=web.searchUtils **/ - - import { _lt, _t } from "web.core"; - import Domain from "web.Domain"; - import pyUtils from "web.py_utils"; - - //------------------------------------------------------------------------- - // Constants - //------------------------------------------------------------------------- - - // Filter menu parameters - const FIELD_OPERATORS = { - binary: [ - { symbol: "!=", description: _lt("is set"), value: false }, - { symbol: "=", description: _lt("is not set"), value: false }, - ], - boolean: [ - { symbol: "=", description: _lt("is Yes"), value: true }, - { symbol: "!=", description: _lt("is No"), value: true }, - ], - char: [ - { symbol: "ilike", description: _lt("contains") }, - { symbol: "not ilike", description: _lt("doesn't contain") }, - { symbol: "=", description: _lt("is equal to") }, - { symbol: "!=", description: _lt("is not equal to") }, - { symbol: "!=", description: _lt("is set"), value: false }, - { symbol: "=", description: _lt("is not set"), value: false }, - ], - date: [ - { symbol: "=", description: _lt("is equal to") }, - { symbol: "!=", description: _lt("is not equal to") }, - { symbol: ">", description: _lt("is after") }, - { symbol: "<", description: _lt("is before") }, - { symbol: ">=", description: _lt("is after or equal to") }, - { symbol: "<=", description: _lt("is before or equal to") }, - { symbol: "between", description: _lt("is between") }, - { symbol: "!=", description: _lt("is set"), value: false }, - { symbol: "=", description: _lt("is not set"), value: false }, - ], - datetime: [ - { symbol: "between", description: _lt("is between") }, - { symbol: "=", description: _lt("is equal to") }, - { symbol: "!=", description: _lt("is not equal to") }, - { symbol: ">", description: _lt("is after") }, - { symbol: "<", description: _lt("is before") }, - { symbol: ">=", description: _lt("is after or equal to") }, - { symbol: "<=", description: _lt("is before or equal to") }, - { symbol: "!=", description: _lt("is set"), value: false }, - { symbol: "=", description: _lt("is not set"), value: false }, - ], - id: [ - { symbol: "=", description: _lt("is") }, - { symbol: "<=", description: _lt("less than or equal to")}, - { symbol: ">", description: _lt("greater than")}, - ], - number: [ - { symbol: "=", description: _lt("is equal to") }, - { symbol: "!=", description: _lt("is not equal to") }, - { symbol: ">", description: _lt("greater than") }, - { symbol: "<", description: _lt("less than") }, - { symbol: ">=", description: _lt("greater than or equal to") }, - { symbol: "<=", description: _lt("less than or equal to") }, - { symbol: "!=", description: _lt("is set"), value: false }, - { symbol: "=", description: _lt("is not set"), value: false }, - ], - selection: [ - { symbol: "=", description: _lt("is") }, - { symbol: "!=", description: _lt("is not") }, - { symbol: "!=", description: _lt("is set"), value: false }, - { symbol: "=", description: _lt("is not set"), value: false }, - ], - }; - const FIELD_TYPES = { - binary: 'binary', - boolean: 'boolean', - char: 'char', - date: 'date', - datetime: 'datetime', - float: 'number', - id: 'id', - integer: 'number', - html: 'char', - many2many: 'char', - many2one: 'char', - monetary: 'number', - one2many: 'char', - text: 'char', - selection: 'selection', - }; - const DEFAULT_PERIOD = 'this_month'; - const QUARTERS = { - 1: { description: _lt("Q1"), coveredMonths: [0, 1, 2] }, - 2: { description: _lt("Q2"), coveredMonths: [3, 4, 5] }, - 3: { description: _lt("Q3"), coveredMonths: [6, 7, 8] }, - 4: { description: _lt("Q4"), coveredMonths: [9, 10, 11] }, - }; - const MONTH_OPTIONS = { - this_month: { - id: 'this_month', groupNumber: 1, format: 'MMMM', - addParam: {}, granularity: 'month', - }, - last_month: { - id: 'last_month', groupNumber: 1, format: 'MMMM', - addParam: { months: -1 }, granularity: 'month', - }, - antepenultimate_month: { - id: 'antepenultimate_month', groupNumber: 1, format: 'MMMM', - addParam: { months: -2 }, granularity: 'month', - }, - }; - const QUARTER_OPTIONS = { - fourth_quarter: { - id: 'fourth_quarter', groupNumber: 1, description: QUARTERS[4].description, - setParam: { quarter: 4 }, granularity: 'quarter', - }, - third_quarter: { - id: 'third_quarter', groupNumber: 1, description: QUARTERS[3].description, - setParam: { quarter: 3 }, granularity: 'quarter', - }, - second_quarter: { - id: 'second_quarter', groupNumber: 1, description: QUARTERS[2].description, - setParam: { quarter: 2 }, granularity: 'quarter', - }, - first_quarter: { - id: 'first_quarter', groupNumber: 1, description: QUARTERS[1].description, - setParam: { quarter: 1 }, granularity: 'quarter', - }, - }; - const YEAR_OPTIONS = { - this_year: { - id: 'this_year', groupNumber: 2, format: 'YYYY', - addParam: {}, granularity: 'year', - }, - last_year: { - id: 'last_year', groupNumber: 2, format: 'YYYY', - addParam: { years: -1 }, granularity: 'year', - }, - antepenultimate_year: { - id: 'antepenultimate_year', groupNumber: 2, format: 'YYYY', - addParam: { years: -2 }, granularity: 'year', - }, - }; - const PERIOD_OPTIONS = Object.assign({}, MONTH_OPTIONS, QUARTER_OPTIONS, YEAR_OPTIONS); - - // GroupBy menu parameters - const GROUPABLE_TYPES = [ - 'boolean', - 'char', - 'date', - 'datetime', - 'integer', - 'many2one', - 'many2many', - 'selection', - ]; - const DEFAULT_INTERVAL = 'month'; - const INTERVAL_OPTIONS = { - year: { description: _lt("Year"), id: 'year', groupNumber: 1 }, - quarter: { description: _lt("Quarter"), id: 'quarter', groupNumber: 1 }, - month: { description: _lt("Month"), id: 'month', groupNumber: 1 }, - week: { description: _lt("Week"), id: 'week', groupNumber: 1 }, - day: { description: _lt("Day"), id: 'day', groupNumber: 1 } - }; - - // Comparison menu parameters - const COMPARISON_OPTIONS = { - previous_period: { - description: _lt("Previous Period"), id: 'previous_period', - }, - previous_year: { - description: _lt("Previous Year"), id: 'previous_year', addParam: { years: -1 }, - }, - }; - const PER_YEAR = { - year: 1, - quarter: 4, - month: 12, - }; - // Search bar - const FACET_ICONS = { - filter: 'fa fa-filter', - groupBy: 'oi oi-group', - favorite: 'fa fa-star', - comparison: 'fa fa-adjust', - }; - - //------------------------------------------------------------------------- - // Functions - //------------------------------------------------------------------------- - - /** - * Constructs the string representation of a domain and its description. The - * domain is of the form: - * ['|',..., '|', d_1,..., d_n] - * where d_i is a time range of the form - * ['&', [fieldName, >=, leftBound_i], [fieldName, <=, rightBound_i]] - * where leftBound_i and rightBound_i are date or datetime computed accordingly - * to the given options and reference moment. - * (@see constructDateRange). - * @param {moment} referenceMoment - * @param {string} fieldName - * @param {string} fieldType - * @param {string[]} selectedOptionIds - * @param {string} [comparisonOptionId] - * @returns {{ domain: string, description: string }} - */ - function constructDateDomain( - referenceMoment, - fieldName, - fieldType, - selectedOptionIds, - comparisonOptionId - ) { - let addParam; - let selectedOptions; - if (comparisonOptionId) { - [addParam, selectedOptions] = getComparisonParams( - referenceMoment, - selectedOptionIds, - comparisonOptionId); - } else { - selectedOptions = getSelectedOptions(referenceMoment, selectedOptionIds); - } - - const yearOptions = selectedOptions.year; - const otherOptions = [ - ...(selectedOptions.quarter || []), - ...(selectedOptions.month || []) - ]; - - sortPeriodOptions(yearOptions); - sortPeriodOptions(otherOptions); - - const ranges = []; - for (const yearOption of yearOptions) { - const constructRangeParams = { - referenceMoment, - fieldName, - fieldType, - addParam, - }; - if (otherOptions.length) { - for (const option of otherOptions) { - const setParam = Object.assign({}, - yearOption.setParam, - option ? option.setParam : {} - ); - const { granularity } = option; - const range = constructDateRange(Object.assign( - { granularity, setParam }, - constructRangeParams - )); - ranges.push(range); - } - } else { - const { granularity, setParam } = yearOption; - const range = constructDateRange(Object.assign( - { granularity, setParam }, - constructRangeParams - )); - ranges.push(range); - } - } - - const domain = pyUtils.assembleDomains(ranges.map(range => range.domain), 'OR'); - const description = ranges.map(range => range.description).join("/"); - - return { domain, description }; - } - - /** - * Constructs the string representation of a domain and its description. The - * domain is a time range of the form: - * ['&', [fieldName, >=, leftBound],[fieldName, <=, rightBound]] - * where leftBound and rightBound are some date or datetime determined by setParam, - * addParam, granularity and the reference moment. - * @param {Object} params - * @param {moment} params.referenceMoment - * @param {string} params.fieldName - * @param {string} params.fieldType - * @param {string} params.granularity - * @param {Object} params.setParam - * @param {Object} [params.addParam] - * @returns {{ domain: string, description: string }} - */ - function constructDateRange({ - referenceMoment, - fieldName, - fieldType, - granularity, - setParam, - addParam, - }) { - const date = referenceMoment.clone().set(setParam).add(addParam || {}); - - // compute domain - let leftBound = date.clone().locale('en').startOf(granularity); - let rightBound = date.clone().locale('en').endOf(granularity); - if (fieldType === 'date') { - leftBound = leftBound.format('YYYY-MM-DD'); - rightBound = rightBound.format('YYYY-MM-DD'); - } else { - leftBound = leftBound.utc().format('YYYY-MM-DD HH:mm:ss'); - rightBound = rightBound.utc().format('YYYY-MM-DD HH:mm:ss'); - } - const domain = Domain.prototype.arrayToString([ - '&', - [fieldName, '>=', leftBound], - [fieldName, '<=', rightBound] - ]); - - // compute description - const descriptions = [date.format("YYYY")]; - const method = _t.database.parameters.direction === "rtl" ? "push" : "unshift"; - if (granularity === "month") { - descriptions[method](date.format("MMMM")); - } else if (granularity === "quarter") { - descriptions[method](QUARTERS[date.quarter()].description); - } - const description = descriptions.join(" "); - - return { domain, description, }; - } - - /** - * Returns a version of the options in COMPARISON_OPTIONS with translated descriptions. - * @see getOptionsWithDescriptions - */ - function getComparisonOptions() { - return getOptionsWithDescriptions(COMPARISON_OPTIONS); - } - - /** - * Returns the params addParam and selectedOptions necessary for the computation - * of a comparison domain. - * @param {moment} referenceMoment - * @param {string{}} selectedOptionIds - * @param {string} comparisonOptionId - * @returns {Object[]} - */ - function getComparisonParams(referenceMoment, selectedOptionIds, comparisonOptionId) { - const comparisonOption = COMPARISON_OPTIONS[comparisonOptionId]; - const selectedOptions = getSelectedOptions(referenceMoment, selectedOptionIds); - let addParam = comparisonOption.addParam; - if (addParam) { - return [addParam, selectedOptions]; - } - addParam = {}; - - let globalGranularity = 'year'; - if (selectedOptions.month) { - globalGranularity = 'month'; - } else if (selectedOptions.quarter) { - globalGranularity = 'quarter'; - } - const granularityFactor = PER_YEAR[globalGranularity]; - const years = selectedOptions.year.map(o => o.setParam.year); - const yearMin = Math.min(...years); - const yearMax = Math.max(...years); - - let optionMin = 0; - let optionMax = 0; - if (selectedOptions.quarter) { - const quarters = selectedOptions.quarter.map(o => o.setParam.quarter); - if (globalGranularity === 'month') { - delete selectedOptions.quarter; - for (const quarter of quarters) { - for (const month of QUARTERS[quarter].coveredMonths) { - const monthOption = selectedOptions.month.find( - o => o.setParam.month === month - ); - if (!monthOption) { - selectedOptions.month.push({ - setParam: { month, }, granularity: 'month', - }); - } - } - } - } else { - optionMin = Math.min(...quarters); - optionMax = Math.max(...quarters); - } - } - if (selectedOptions.month) { - const months = selectedOptions.month.map(o => o.setParam.month); - optionMin = Math.min(...months); - optionMax = Math.max(...months); - } - - addParam[globalGranularity] = -1 + - granularityFactor * (yearMin - yearMax) + - optionMin - optionMax; - - return [addParam, selectedOptions]; - } - - /** - * Returns a version of the options in INTERVAL_OPTIONS with translated descriptions. - * @see getOptionsWithDescriptions - */ - function getIntervalOptions() { - return getOptionsWithDescriptions(INTERVAL_OPTIONS); - } - - /** - * Returns a version of the options in PERIOD_OPTIONS with translated descriptions - * and a key defautlYearId used in the control panel model when toggling a period option. - * @param {moment} referenceMoment - * @returns {Object[]} - */ - function getPeriodOptions(referenceMoment) { - const options = []; - for (const option of Object.values(PERIOD_OPTIONS)) { - const { id, groupNumber, description, } = option; - const res = { id, groupNumber, }; - const date = referenceMoment.clone().set(option.setParam).add(option.addParam); - if (description) { - res.description = description.toString(); - } else { - res.description = date.format(option.format.toString()); - } - res.setParam = getSetParam(option, referenceMoment); - res.defaultYear = date.year(); - options.push(res); - } - for (const option of options) { - const yearOption = options.find( - o => o.setParam && o.setParam.year === option.defaultYear - ); - option.defaultYearId = yearOption.id; - delete option.defaultYear; - delete option.setParam; - } - return options; - } - - /** - * Returns a version of the options in OPTIONS with translated descriptions (if any). - * @param {Object{}} OPTIONS - * @returns {Object[]} - */ - function getOptionsWithDescriptions(OPTIONS) { - const options = []; - for (const option of Object.values(OPTIONS)) { - const { id, groupNumber, description, } = option; - const res = { id, }; - if (description) { - res.description = description.toString(); - } - if (groupNumber) { - res.groupNumber = groupNumber; - } - options.push(res); - } - return options; - } - - /** - * Returns a version of the period options whose ids are in selectedOptionIds - * partitioned by granularity. - * @param {moment} referenceMoment - * @param {string[]} selectedOptionIds - * @param {Object} - */ - function getSelectedOptions(referenceMoment, selectedOptionIds) { - const selectedOptions = { year: [] }; - for (const optionId of selectedOptionIds) { - const option = PERIOD_OPTIONS[optionId]; - const setParam = getSetParam(option, referenceMoment); - const granularity = option.granularity; - if (!selectedOptions[granularity]) { - selectedOptions[granularity] = []; - } - selectedOptions[granularity].push({ granularity, setParam }); - } - return selectedOptions; - } - - /** - * Returns the setParam object associated with the given periodOption and - * referenceMoment. - * @param {Object} periodOption - * @param {moment} referenceMoment - * @returns {Object} - */ - function getSetParam(periodOption, referenceMoment) { - if (periodOption.setParam) { - return periodOption.setParam; - } - const date = referenceMoment.clone().add(periodOption.addParam); - const setParam = {}; - setParam[periodOption.granularity] = date[periodOption.granularity](); - return setParam; - } - - /** - * @param {string} intervalOptionId - * @returns {number} index - */ - function rankInterval(intervalOptionId) { - return Object.keys(INTERVAL_OPTIONS).indexOf(intervalOptionId); - } - - /** - * Sorts in place an array of 'period' options. - * @param {Object[]} options supposed to be of the form: - * { granularity, setParam, } - */ - function sortPeriodOptions(options) { - options.sort((o1, o2) => { - const granularity1 = o1.granularity; - const granularity2 = o2.granularity; - if (granularity1 === granularity2) { - return o1.setParam[granularity1] - o2.setParam[granularity1]; - } - return granularity1 < granularity2 ? -1 : 1; - }); - } - - /** - * Checks if a year id is among the given array of period option ids. - * @param {string[]} selectedOptionIds - * @returns {boolean} - */ - function yearSelected(selectedOptionIds) { - return selectedOptionIds.some(optionId => !!YEAR_OPTIONS[optionId]); - } - - export default { - COMPARISON_OPTIONS, - DEFAULT_INTERVAL, - DEFAULT_PERIOD, - FACET_ICONS, - FIELD_OPERATORS, - FIELD_TYPES, - GROUPABLE_TYPES, - INTERVAL_OPTIONS, - PERIOD_OPTIONS, - - constructDateRange, - constructDateDomain, - getComparisonOptions, - getIntervalOptions, - getPeriodOptions, - rankInterval, - yearSelected, - }; diff --git a/addons/web/static/src/legacy/js/core/mvc.js b/addons/web/static/src/legacy/js/core/mvc.js deleted file mode 100644 index 074bae32baa..00000000000 --- a/addons/web/static/src/legacy/js/core/mvc.js +++ /dev/null @@ -1,247 +0,0 @@ -/** @odoo-module alias=web.mvc **/ - -/** - * This file contains a 'formalization' of a MVC pattern, applied to Odoo - * idioms. - * - * For a simple widget/component, this is definitely overkill. However, when - * working on complex systems, such as Odoo views (or the control panel, or some - * client actions), it is useful to clearly separate the code in concerns. - * - * We define here 4 classes: Factory, Model, Renderer, Controller. Note that - * for various historical reasons, we use the term Renderer instead of View. The - * main issue is that the term 'View' is used way too much in Odoo land, and - * adding it would increase the confusion. - * - * In short, here are the responsabilities of the four classes: - * - Model: this is where the main state of the system lives. This is the part - * that will talk to the server, process the results and is the owner of the - * state - * - Renderer: this is the UI code: it should only be concerned with the look - * and feel of the system: rendering, binding handlers, ... - * - Controller: coordinates the model with the renderer and the parents widgets. - * This is more a 'plumbing' widget. - * - Factory: setting up the MRC components is a complex task, because each of - * them needs the proper arguments/options, it needs to be extensible, they - * needs to be created in the proper order, ... The job of the factory is - * to process all the various arguments, and make sure each component is as - * simple as possible. - */ - -import Class from "web.Class"; -import mixins from "web.mixins"; -import ServicesMixin from "web.ServicesMixin"; -import Widget from "web.Widget"; -import { loadBundle } from "@web/core/assets"; - - -/** - * Owner of the state, this component is tasked with fetching data, processing - * it, updating it, ... - * - * Note that this is not a widget: it is a class which has not UI representation. - * - * @class Model - */ -var Model = Class.extend(mixins.EventDispatcherMixin, ServicesMixin, { - /** - * @param {Widget} parent - * @param {Object} params - */ - init: function (parent, params) { - mixins.EventDispatcherMixin.init.call(this); - this.setParent(parent); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * This method should return the complete state necessary for the renderer - * to display the current data. - * - * @returns {*} - */ - get: function () { - }, - /** - * The load method is called once in a model, when we load the data for the - * first time. The method returns (a promise that resolves to) some kind - * of token/handle. The handle can then be used with the get method to - * access a representation of the data. - * - * @param {Object} params - * @returns {Promise} The promise resolves to some kind of handle - */ - load: function () { - return Promise.resolve(); - }, -}); - -/** - * Only responsibility of this component is to display the user interface, and - * react to user changes. - * - * @class Renderer - */ -var Renderer = Widget.extend({ - /** - * @override - * @param {any} state - * @param {Object} params - */ - init: function (parent, state, params) { - this._super(parent); - this.state = state; - }, -}); - -/** - * The controller has to coordinate between parent, renderer and model. - * - * @class Controller - */ -var Controller = Widget.extend({ - /** - * @override - * @param {Model} model - * @param {Renderer} renderer - * @param {Object} params - * @param {any} [params.handle=null] - */ - init: function (parent, model, renderer, params) { - this._super.apply(this, arguments); - this.handle = params.handle || null; - this.model = model; - this.renderer = renderer; - }, - /** - * @returns {Promise} - */ - start: function () { - return Promise.all( - [this._super.apply(this, arguments), - this._startRenderer()] - ); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Appends the renderer in the $el. To override to insert it elsewhere. - * - * @private - */ - _startRenderer: function () { - return this.renderer.appendTo(this.$el); - }, -}); - -var Factory = Class.extend({ - config: { - Model: Model, - Renderer: Renderer, - Controller: Controller, - }, - /** - * @override - */ - init: function () { - this.rendererParams = {}; - this.controllerParams = {}; - this.modelParams = {}; - this.loadParams = {}; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Main method of the Factory class. Create a controller, and make sure that - * data and libraries are loaded. - * - * There is a unusual thing going in this method with parents: we create - * renderer/model with parent as parent, then we have to reassign them at - * the end to make sure that we have the proper relationships. This is - * necessary to solve the problem that the controller needs the model and - * the renderer to be instantiated, but the model need a parent to be able - * to load itself, and the renderer needs the data in its constructor. - * - * @param {Widget} parent the parent of the resulting Controller (most - * likely an action manager) - * @returns {Promise} - */ - getController: function (parent) { - var self = this; - var model = this.getModel(parent); - return Promise.all([this._loadData(model), loadBundle(this)]).then(function (result) { - const { state, handle } = result[0]; - var renderer = self.getRenderer(parent, state); - var Controller = self.Controller || self.config.Controller; - const initialState = model.get(handle); - var controllerParams = Object.assign({ - initialState, - handle, - }, self.controllerParams); - var controller = new Controller(parent, model, renderer, controllerParams); - model.setParent(controller); - renderer.setParent(controller); - return controller; - }); - }, - /** - * Returns a new model instance - * - * @param {Widget} parent the parent of the model - * @returns {Model} instance of the model - */ - getModel: function (parent) { - var Model = this.config.Model; - return new Model(parent, this.modelParams); - }, - /** - * Returns a new renderer instance - * - * @param {Widget} parent the parent of the renderer - * @param {Object} state the information related to the rendered data - * @returns {Renderer} instance of the renderer - */ - getRenderer: function (parent, state) { - var Renderer = this.config.Renderer; - return new Renderer(parent, state, this.rendererParams); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Loads initial data from the model - * - * @private - * @param {Model} model a Model instance - * @param {Object} [options={}] - * @param {boolean} [options.withSampleData=true] - * @returns {Promise<*>} a promise that resolves to the value returned by - * the get method from the model - * @todo: get rid of loadParams (use modelParams instead) - */ - _loadData: function (model, options = {}) { - options.withSampleData = 'withSampleData' in options ? options.withSampleData : true; - return model.load(this.loadParams).then(function (handle) { - return { state: model.get(handle, options), handle }; - }); - }, -}); - - -export default { - Factory: Factory, - Model: Model, - Renderer: Renderer, - Controller: Controller, -}; diff --git a/addons/web/static/src/legacy/js/services/core.js b/addons/web/static/src/legacy/js/services/core.js index 59389298235..726bbbf7528 100644 --- a/addons/web/static/src/legacy/js/services/core.js +++ b/addons/web/static/src/legacy/js/services/core.js @@ -31,13 +31,10 @@ export default { // core classes and functions Class: Class, bus: bus, - main_bus: new Bus(), _t: translation._t, _lt: translation._lt, // registries - action_registry: new Registry(), - crash_registry: new Registry(), serviceRegistry: new Registry(), /** * @type {String} diff --git a/addons/web/static/src/legacy/js/tools/tools.js b/addons/web/static/src/legacy/js/tools/tools.js deleted file mode 100644 index 94e4e35dab7..00000000000 --- a/addons/web/static/src/legacy/js/tools/tools.js +++ /dev/null @@ -1,19 +0,0 @@ -/** @odoo-module alias=web.tools **/ - -/** - * Wrapper for deprecated functions that display a warning message. - * - * @param {Function} fn the deprecated function - * @param {string} [message=''] optional message to display - * @returns {Function} - */ -function deprecated(fn, message) { - return function () { - console.warn(message || (fn.name + ' is deprecated.')); - return fn.apply(this, arguments); - }; -} - -export default { - deprecated: deprecated, -}; diff --git a/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js b/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js deleted file mode 100644 index ddffd55debc..00000000000 --- a/addons/web/static/src/legacy/js/widgets/colorpicker_dialog.js +++ /dev/null @@ -1,47 +0,0 @@ -/** @odoo-module **/ - -import { _t } from "@web/core/l10n/translation"; -import { ComponentWrapper } from "web.OwlCompatibility"; - -import Dialog from "web.Dialog"; -import { Colorpicker } from '@web/core/colorpicker/colorpicker'; - -export const ColorpickerDialog = Dialog.extend({ - /** - * @override - */ - init: function (parent, options) { - this.options = options || {}; - this._super(parent, Object.assign({ - size: 'small', - title: _t('Pick a color'), - buttons: [ - {text: _t('Choose'), classes: 'btn-primary', close: true, click: this._onFinalPick.bind(this)}, - {text: _t('Discard'), close: true}, - ], - }, this.options)); - }, - /** - * @override - */ - start: function () { - const proms = [this._super(...arguments)]; - this.colorpicker = new ComponentWrapper(this, Colorpicker, { - colorPreview: true, - }); - this._colorpickerComponent = this.colorpicker.node.component; - proms.push(this.colorpicker.mount(this.$el[0])); - return Promise.all(proms); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @private - */ - _onFinalPick: function () { - this.trigger_up('colorpicker:saved', this._colorpickerComponent.colorComponents); - }, -}); diff --git a/addons/web/static/src/legacy/js/widgets/domain_selector.js b/addons/web/static/src/legacy/js/widgets/domain_selector.js deleted file mode 100644 index 898e2a64baf..00000000000 --- a/addons/web/static/src/legacy/js/widgets/domain_selector.js +++ /dev/null @@ -1,1026 +0,0 @@ -/** @odoo-module alias=web.DomainSelector **/ - -import { pick } from "@web/core/utils/objects"; -import core from "web.core"; -import datepicker from "web.datepicker"; -import dom from "web.dom"; -import Domain from "web.Domain"; -import field_utils from "web.field_utils"; -import ModelFieldSelector from "web.ModelFieldSelector"; -import Widget from "web.Widget"; - -var _t = core._t; -var _lt = core._lt; - -// "child_of", "parent_of", "like", "not like", "=like", "=ilike" -// are only used if user entered them manually or if got from demo data -var operator_mapping = { - "=": "=", - "!=": _lt("is not ="), - ">": ">", - "<": "<", - ">=": ">=", - "<=": "<=", - "ilike": _lt("contains"), - "not ilike": _lt("does not contain"), - "in": _lt("in"), - "not in": _lt("not in"), - - "child_of": _lt("child of"), - "parent_of": _lt("parent of"), - "like": "like", - "not like": "not like", - "=like": "=like", - "=ilike": "=ilike", - - // custom - "set": _lt("is set"), - "not set": _lt("is not set"), -}; - -/** - * Abstraction for widgets which can represent and allow edition of a domain. - */ -var DomainNode = Widget.extend({ - events: { - // If click on the node add or delete button, notify the parent and let - // it handle the addition/removal - "click .o_domain_add_node_button": "_onAddButtonClick", - "click .o_domain_delete_node_button": "_onDeleteButtonClick", - // Handle visual feedback and animation - "mouseenter button": "_onButtonEntered", - "mouseleave button": "_onButtonLeft", - }, - /** - * A DomainNode needs a model and domain to work. It can also receive a set - * of options. - * - * @param {Object} parent - * @param {string} model - the model name - * @param {Array|string} domain - the prefix representation of the domain - * @param {Object} [options] - an object with possible values: - * @param {boolean} [options.readonly=true] - true if is readonly - * @param {Array} [options.default] - default domain used when creating a - * new node - * @param {string[]} [options.operators=null] - * a list of available operators (null = all of supported ones) - * @param {boolean} [options.debugMode=false] - true if should be in debug - * - * @see ModelFieldSelector for other options - */ - init: function (parent, model, domain, options) { - this._super.apply(this, arguments); - - this.model = model; - this.options = Object.assign({ - readonly: true, - operators: null, - debugMode: false, - }, options || {}); - - this.readonly = this.options.readonly; - this.debug = this.options.debugMode; - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Should return if the node is representing a well-formed domain, whose - * field chains properly belong to the associated model. - * - * @abstract - * @returns {boolean} - */ - isValid: function () {}, - /** - * Should return the prefix domain the widget is currently representing - * (an array). - * - * @abstract - * @returns {Array} - */ - getDomain: function () {}, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when the add button is clicked -> trigger_up an event to ask - * creation of a new child in its parent. - * - * @param {Event} e - */ - _onAddButtonClick: function (e) { - e.preventDefault(); - e.stopPropagation(); - this.trigger_up("add_node_clicked", {newBranch: !!$(e.currentTarget).data("branch"), child: this}); - }, - /** - * Called when the delete button is clicked -> trigger_up an event to ask - * deletion of this node from its parent. - * - * @param {Event} e - */ - _onDeleteButtonClick: function (e) { - e.preventDefault(); - e.stopPropagation(); - this.trigger_up("delete_node_clicked", {child: this}); - }, - /** - * Called when a "controlpanel" button is hovered -> add classes to the - * domain node to add animation effects. - * - * @param {Event} e - */ - _onButtonEntered: function (e) { - e.preventDefault(); - e.stopPropagation(); - var $target = $(e.currentTarget); - this.$el.toggleClass("o_hover_btns", $target.hasClass("o_domain_delete_node_button")); - this.$el.toggleClass("o_hover_add_node", $target.hasClass("o_domain_add_node_button")); - this.$el.toggleClass("o_hover_add_inset_node", !!$target.data("branch")); - }, - /** - * Called when a "controlpanel" button is not hovered anymore -> remove - * classes from the domain node to stop animation effects. - * - * @param {Event} e - */ - _onButtonLeft: function (e) { - e.preventDefault(); - e.stopPropagation(); - this.$el.removeClass("o_hover_btns o_hover_add_node o_hover_add_inset_node"); - }, -}); - -/** - * DomainNode which can handle subdomains (a domain which is composed of - * multiple parts). It thus will be composed of other DomainTree instances - * and/or leaf parts of a domain (@see DomainLeaf). - */ -var DomainTree = DomainNode.extend({ - template: "DomainTree", - events: Object.assign({}, DomainNode.prototype.events, { - "click .o_domain_tree_operator_selector .dropdown-item": "_onOperatorChange", - }), - custom_events: { - // If a domain child sends a request to add a child or remove one, call - // the appropriate methods. Propagates the event until success. - "add_node_clicked": "_onNodeAdditionAsk", - "delete_node_clicked": "_onNodeDeletionAsk", - }, - /** - * @constructor - * @see DomainNode.init - * The initialization of a DomainTree creates a "children" array attribute - * which will contain the DomainNode children. It also deduces the - * operator from the domain. - * @see DomainTree._addFlattenedChildren - */ - init: function (parent, model, domain) { - this._super.apply(this, arguments); - var parsedDomain = this._parseDomain(domain); - if (parsedDomain) { - this._initialize(parsedDomain); - } - }, - /** - * @see DomainNode.start - * @returns {Promise} - */ - start: function () { - this._postRender(); - return Promise.all([ - this._super.apply(this, arguments), - this._renderChildrenTo(this.$childrenContainer) - ]); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @see DomainNode.isValid - * @returns {boolean} - */ - isValid: function () { - for (var i = 0 ; i < this.children.length ; i++) { - var cValid = this.children[i].isValid(); - if (!cValid) { - return cValid; - } - } - return this._isValid; - }, - /** - * @see DomainNode.getDomain - * @returns {Array} - */ - getDomain: function () { - var childDomains = []; - var nbChildren = 0; - this.children.forEach((child) => { - var childDomain = child.getDomain(); - if (childDomain.length) { - nbChildren++; - childDomains = childDomains.concat(child.getDomain()); - } - }); - var nbChildRequired = this.operator === "!" ? 1 : 2; - var operators = new Array(nbChildren - nbChildRequired + 1).fill(this.operator); - return operators.concat(childDomains); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Adds a domain part to the widget. - * -> trigger_up "domain_changed" if the child is added - * - * @private - * @param {Array} domain - the prefix-like domain to build and add to the - * widget - * @param {DomainNode} afterNode - the node after which the new domain part - * must be added (at the end if not given) - * @returns {boolean} true if the part was added - * false otherwise (the afterNode was not found) - */ - _addChild: function (domain, afterNode) { - var i = afterNode ? this.children.indexOf(afterNode) : this.children.length; - if (i < 0) return false; - - this.children.splice(i+1, 0, instantiateNode(this, this.model, domain, this.options)); - this.trigger_up("domain_changed", {child: this}); - return true; - }, - /** - * Adds a child which represents the given domain. If the child has children - * and that the child main domain operator is the same as the current widget - * one, the 2-children prefix hierarchy is then simplified by making the - * child's children the widget's own children. - * - * @private - * @param {Array|string} domain - the domain of the child to add - */ - _addFlattenedChildren: function (domain) { - var node = instantiateNode(this, this.model, domain, this.options); - if (node === null) { - return; - } - if (!node.children || node.operator !== this.operator) { - this.children.push(node); - return; - } - node.children.forEach( - ((child) => { - child.setParent(this); - this.children.push(child); - }).bind(this) - ); - node.destroy(); - }, - /** - * Changes the operator of the domain tree and notifies the parent if - * necessary (not silent). - * - * @private - * @param {string} operator - the new operator - * @param {boolean} silent - true if the parents should not be notified of - * the change - */ - _changeOperator: function (operator, silent) { - this.operator = operator; - if (!silent) this.trigger_up("domain_changed", {child: this}); - }, - /** - * @see DomainTree.init - * @private - */ - _initialize: function (domain) { - this._isValid = true; - this.operator = domain[0]; - this.children = []; - if (domain.length <= 1) { - return; - } - - // Add flattened children by search the appropriate number of children - // in the rest of the domain (after the operator) - var nbLeafsToFind = 1; - for (var i = 1 ; i < domain.length ; i++) { - if (domain[i] === "&" || domain[i] === "|") { - nbLeafsToFind++; - } else if (domain[i] !== "!") { - nbLeafsToFind--; - } - - if (!nbLeafsToFind) { - var partLeft = domain.slice(1, i+1); - var partRight = domain.slice(i+1); - if (partLeft.length) { - this._addFlattenedChildren(partLeft); - } - if (partRight.length) { - this._addFlattenedChildren(partRight); - } - break; - } - } - this._isValid = (nbLeafsToFind === 0); - - // Mark "!" tree children so that they do not allow to add other - // children around them - if (this.operator === "!") { - this.children[0].noControlPanel = true; - } - }, - /** - * @see DomainTree.start - * Initializes variables which depend on the rendered widget. - * @private - */ - _postRender: function () { - this.$childrenContainer = this.$("> .o_domain_node_children_container"); - }, - /** - * Removes a given child from the widget. - * -> trigger_up domain_changed if the child is removed - * - * @private - * @param {DomainNode} oldChild - the child instance to remove - * @returns {boolean} true if the child was removed, false otherwise (the - * widget does not own the child) - */ - _removeChild: function (oldChild) { - var i = this.children.indexOf(oldChild); - if (i < 0) return false; - - this.children[i].destroy(); - this.children.splice(i, 1); - this.trigger_up("domain_changed", {child: this}); - return true; - }, - /** - * @see DomainTree.start - * Appends the children domain node to the given node. This is used to - * render the children widget in a dummy element before adding them in the - * DOM, otherwhise they could be misordered as they rendering is not - * synchronous. - * - * @private - * @param {jQuery} $to - the jQuery node to which the children must be added - * @returns {Promise} - */ - _renderChildrenTo: function ($to) { - var $div = $("
"); - const children = this.children; - return Promise.all( - children.map( - ((child) => { - return child.appendTo($div); - }).bind(this) - ) - ).then((function () { - children.forEach((child) => { - child.$el.appendTo($to); // Forced to do it this way so that the - // children are not misordered - }); - }).bind(this)); - }, - /** - * @param {string} domain - * @returns {Array[]} - */ - _parseDomain: function (domain) { - var parsedDomain = false; - try { - parsedDomain = Domain.prototype.stringToArray(domain); - this.invalidDomain = false; - } catch { - // TODO: domain could contain `parent` for example, which is - // currently not handled by the DomainSelector - this.invalidDomain = true; - this.children = []; - } - return parsedDomain; - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when the operator select value is changed -> change the internal - * operator state - * - * @param {Event} e - */ - _onOperatorChange: function (e) { - e.preventDefault(); - e.stopPropagation(); - this._changeOperator($(e.target).data("operator")); - }, - /** - * Called when a node addition was asked -> add the new domain part if on - * the right node or let the propagation continue. - * - * @param {OdooEvent} e - */ - _onNodeAdditionAsk: function (e) { - var domain = this.options.default || [["id", "=", 1]]; - if (e.data.newBranch) { - domain = [this.operator === "&" ? "|" : "&"].concat(domain).concat(domain); - } - if (this._addChild(domain, e.data.child)) { - e.stopPropagation(); - } - }, - /** - * Called when a node deletion was asked -> remove the domain part if on - * the right node or let the propagation continue. - * - * @param {OdooEvent} e - */ - _onNodeDeletionAsk: function (e) { - if (this._removeChild(e.data.child)) { - e.stopPropagation(); - } - }, -}); - -/** - * The DomainSelector widget can be used to build prefix char domain. It is the - * DomainTree specialization to use to have a fully working widget. - * - * Known limitations: - * - * - Some operators like "child_of", "parent_of", "like", "not like", - * "=like", "=ilike" will come only if you use them from demo data or - * debug input. - * - Some kind of domain can not be build right now - * e.g ("country_id", "in", [1,2,3]) but you can insert from debug input. - */ -var DomainSelector = DomainTree.extend({ - template: "DomainSelector", - events: Object.assign({}, DomainTree.prototype.events, { - "click .o_domain_add_first_node_button": "_onAddFirstButtonClick", - "change .o_domain_debug_input": "_onDebugInputChange", - }), - custom_events: Object.assign({}, DomainTree.prototype.custom_events, { - domain_changed: "_onDomainChange", - }), - - init(parent, model, domain) { - this._super(...arguments); - this.rawDomain = domain; - this._redrawId = 0; - }, - - start: function () { - var self = this; - return this._super.apply(this, arguments).then(function () { - if (self.invalidDomain) { - var msg = _t("This domain is not supported."); - self.$el.html(msg); - } - }); - }, - /** - * If 'DomainSelector' is attached within modal, the modal should have - * visible overflow to allow user to see fields drop-down. - */ - on_attach_callback() { - let $modalBodyEl = this.$el.closest('.modal-body'); - if ($modalBodyEl.length !== 0) { - $modalBodyEl.css('overflow', 'visible'); - } - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Changes the internal domain value and forces a reparsing and rerendering. - * If the internal domain value was already equal to the given one, this - * does nothing. - * - * @param {string} domain - * @returns {Promise} resolved when the rerendering is finished - */ - setDomain: function (domain) { - if (domain === Domain.prototype.arrayToString(this.getDomain())) { - return Promise.resolve(); - } - var parsedDomain = this._parseDomain(domain); - if (parsedDomain) { - return this._redraw(parsedDomain); - } - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @see DomainTree._initialize - */ - _initialize: function (domain) { - // Check if the domain starts with implicit "&" operators and make them - // explicit. As the DomainSelector is a specialization of a DomainTree, - // it is waiting for a tree and not a leaf. So [] and [A] will be made - // explicit with ["&"], ["&", A] so that tree parsing is made correctly. - // Note: the domain is considered to be a valid one - if (domain.length > 1) { - Domain.prototype.normalizeArray(domain); - } else { - domain = ["&"].concat(domain); - } - return this._super(domain); - }, - /** - * @see DomainTree._postRender - * Warns the user if the domain is not valid after rendering. - */ - _postRender: function () { - this._super.apply(this, arguments); - - // Display technical domain if in debug mode - this.$debugInput = this.$(".o_domain_debug_input"); - if (this.$debugInput.length) { - this.$debugInput.val(this.rawDomain); - dom.autoresize(this.$debugInput); - } - - // Warn the user if the domain is not valid after rendering - if (!this._isValid) { - this.displayNotification({ message: _t("Domain not supported"), type: 'danger' }); - } - }, - /** - * This method is ugly but achieves the right behavior without flickering. - * - * @param {Array|string} domain - * @returns {Promise} - */ - _redraw: function (domain) { - const _redrawId = ++this._redrawId; - var oldChildren = this.children.slice(); - this._initialize(domain || this.getDomain()); - return this._renderChildrenTo($("
")).then((function () { - if (_redrawId !== this._redrawId) { - return; - } - oldChildren.forEach((child) => { child.destroy(); }); - this.renderElement(); - this._postRender(); - this.children.forEach( - ((child) => { - child.$el.appendTo(this.$childrenContainer); - }).bind(this) - ); - }).bind(this)); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when the "add a filter" button is clicked -> adds a first domain - * node - */ - _onAddFirstButtonClick: function () { - this._addChild(this.options.default || [["id", "=", 1]]); - }, - /** - * Called when the debug input value is changed -> notifies the change if - * valid or warn the user if invalid. - * - * @param {Event} e - */ - _onDebugInputChange: function (e) { - // When the debug input changes, the string prefix domain is read. If it - // is syntax-valid a "domain_changed" event is triggered to notify the - // parent, but the widget isn't redrawn. - // If the domain is not valid, a warning is shown to the user. - const rawDomain = e.currentTarget.value; - try { - Domain.prototype.stringToArray(rawDomain); - } catch { // If there is a syntax error, just ignore the change - this.displayNotification({ title: _t("Syntax error"), message: _t("Domain not properly formed"), type: 'danger' }); - return; - } - this.trigger_up("domain_changed", { - child: this, - noRedraw: true, - domain: rawDomain, - debug: true, - }); - }, - /** - * Called when a (child's) domain has changed -> redraw the entire tree - * representation if necessary - * - * @param {OdooEvent} e - */ - _onDomainChange: function (e) { - // Add the current domain to the payload if not already there - e.data.domain = e.data.domain || this.getDomain(); - this.rawDomain = Domain.prototype.arrayToString(e.data.domain); - // If a subdomain notifies that it underwent some modifications, the - // DomainSelector catches the message and performs a full re-rendering. - if (!e.data.noRedraw) { - this._redraw(); - } - }, -}); - -/** - * DomainNode which handles a domain which cannot be split in another - * subdomains, i.e. composed of a field chain, an operator and a value. - */ -var DomainLeaf = DomainNode.extend({ - template: "DomainLeaf", - events: Object.assign({}, DomainNode.prototype.events, { - "change .o_domain_leaf_operator_select": "_onOperatorSelectChange", - "change .o_domain_leaf_value_input": "_onValueInputChange", - - // Handle the tags widget part (TODO should be an independant widget) - "click .o_domain_leaf_value_add_tag_button": "on_add_tag", - "keyup .o_domain_leaf_value_tags input": "on_add_tag", - "click .o_domain_leaf_value_remove_tag_button": "on_remove_tag", - }), - custom_events: { - "field_chain_changed": "_onFieldChainChange", - }, - /** - * @see DomainNode.init - */ - init: function (parent, model, domain, options) { - this._super.apply(this, arguments); - - var currentDomain = Domain.prototype.stringToArray(domain); - this.chain = currentDomain[0][0]; - this.operator = currentDomain[0][1]; - this.value = currentDomain[0][2]; - - this.operator_mapping = operator_mapping; - }, - /** - * Prepares the information the rendering of the widget will need by - * pre-instantiating its internal field selector widget. - * - * @returns {Promise} - */ - willStart: function () { - var defs = [this._super.apply(this, arguments)]; - - // In edit mode, instantiate a field selector. This is done here in - // willStart and prepared by appending it to a dummy element because the - // DomainLeaf rendering need some information which cannot be computed - // before the ModelFieldSelector is fully rendered (TODO). - this.fieldSelector = new ModelFieldSelector( - this, - this.model, - this.chain !== undefined ? this.chain.toString().split(".") : [], - this.options - ); - defs.push(this.fieldSelector.appendTo($("
")).then((function () { - var wDefs = []; - - if (!this.readonly) { - // Set list of operators according to field type - var selectedField = this.fieldSelector.getSelectedField() || {}; - this.operators = this._getOperatorsFromType(selectedField.type); - if (["child_of", "parent_of", "like", "not like", "=like", "=ilike"].includes(this.operator)) { - // In case user entered manually or from demo data - this.operators[this.operator] = operator_mapping[this.operator]; - } else if (!this.operators[this.operator]) { - // In case the domain uses an unsupported operator for the - // field type - this.operators[this.operator] = "?"; - } - - // Set list of values according to field type - this.selectionChoices = null; - if (selectedField.type === "boolean") { - this.selectionChoices = [["1", _t("set (true)")], ["0", _t("not set (false)")]]; - } else if (selectedField.type === "selection") { - this.selectionChoices = selectedField.selection; - } - - // Adapt display value and operator for rendering - this.displayValue = this.value; - try { - if (selectedField && !selectedField.relation && !Array.isArray(this.value)) { - this.displayValue = field_utils.format[selectedField.type](this.value, selectedField); - } - } catch {/**/} - this.displayOperator = this.operator; - if (selectedField.type === "boolean") { - this.displayValue = this.value ? "1" : "0"; - } else if ((this.operator === "!=" || this.operator === "=") && this.value === false) { - this.displayOperator = this.operator === "!=" ? "set" : "not set"; - } - - // TODO the value could be a m2o input, etc... - if (["date", "datetime"].includes(selectedField.type)) { - this.valueWidget = new (selectedField.type === "datetime" ? datepicker.DateTimeWidget : datepicker.DateWidget)(this); - wDefs.push(this.valueWidget.appendTo("
").then((function () { - this.valueWidget.$el.addClass("o_domain_leaf_value_input"); - this.valueWidget.setValue(moment(this.value)); - this.valueWidget.on("datetime_changed", this, function () { - this._changeValue(this.valueWidget.getValue()); - }); - }).bind(this))); - } - - return Promise.all(wDefs); - } - }).bind(this))); - - return Promise.all(defs); - }, - /** - * @see DomainNode.start - * Appends the prepared field selector and value widget. - * - * @returns {Promise} - */ - start: function () { - this.fieldSelector.$el.prependTo(this.$("> .o_domain_leaf_info, > .o_domain_leaf_edition")); // place the field selector - if (!this.readonly && this.valueWidget) { // In edit mode, place the value widget if any - this.$(".o_domain_leaf_value_input").replaceWith(this.valueWidget.$el); - } - return this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @see DomainNode.isValid - * @returns {boolean} - */ - isValid: function () { - return this.fieldSelector && this.fieldSelector.isValid(); - }, - /** - * @see DomainNode.getDomain - * @returns {Array} - */ - getDomain: function () { - return [[this.chain, this.operator, this.value]]; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Handles a field chain change in the domain. In that case, the operator - * should be adapted to a valid one for the new field and the value should - * also be adapted to the new field and/or operator. - * - * -> trigger_up domain_changed event to ask for a re-rendering (if not - * silent) - * - * @param {string[]} chain - the new field chain - * @param {boolean} silent - true if the method call should not trigger_up a - * domain_changed event - */ - _changeFieldChain: function (chain, silent) { - this.chain = chain.join("."); - this.fieldSelector.setChain(chain).then((function () { - if (!this.fieldSelector.isValid()) return; - - var selectedField = this.fieldSelector.getSelectedField() || {}; - var operators = this._getOperatorsFromType(selectedField.type); - if (operators[this.operator] === undefined) { - this._changeOperator("=", true); - } - this._changeValue(this.value, true); - - if (!silent) this.trigger_up("domain_changed", {child: this}); - }).bind(this)); - }, - /** - * Handles an operator change in the domain. In that case, the value should - * be adapted to a valid one for the new operator. - * - * -> trigger_up domain_changed event to ask for a re-rendering - * (if not silent) - * - * @param {string} operator - the new operator - * @param {boolean} silent - true if the method call should not trigger_up a - * domain_changed event - */ - _changeOperator: function (operator, silent) { - this.operator = operator; - - if (["set", "not set"].includes(this.operator)) { - this.operator = this.operator === "not set" ? "=" : "!="; - this.value = false; - } else if (["in", "not in"].includes(this.operator)) { - this.value = Array.isArray(this.value) ? this.value : this.value ? ("" + this.value).split(",") : []; - } else { - if (Array.isArray(this.value)) { - this.value = this.value.join(","); - } - this._changeValue(this.value, true); - } - - if (!silent) this.trigger_up("domain_changed", {child: this}); - }, - /** - * Handles a formatted value change in the domain. In that case, the value - * should be adapted to a valid technical one. - * - * -> trigger_up "domain_changed" event to ask for a re-rendering (if not - * silent) - * - * @param {*} value - the new formatted value - * @param {boolean} silent - true if the method call should not trigger_up a - * domain_changed event - */ - _changeValue: function (value, silent) { - var couldNotParse = false; - var selectedField = this.fieldSelector.getSelectedField() || {}; - try { - this.value = field_utils.parse[selectedField.type](value, selectedField); - } catch { - this.value = value; - couldNotParse = true; - } - - if (selectedField.type === "boolean") { - if (typeof this.value !== "boolean") { // Convert boolean-like value to boolean - this.value = !!parseFloat(this.value); - } - } else if (selectedField.type === "selection") { - if (!selectedField.selection.some((option) => option[0] === this.value)) { - this.value = selectedField.selection[0][0]; - } - } else if (["date", "datetime"].includes(selectedField.type)) { - if (couldNotParse || typeof this.value === "boolean") { - this.value = field_utils.parse[selectedField.type](field_utils.format[selectedField.type](moment())).toJSON(); // toJSON to get date with server format - } else { - this.value = this.value.toJSON(); // toJSON to get date with server format - } - } else { - // Never display "true" or "false" strings from boolean value - if (typeof this.value === "boolean") { - this.value = ""; - } else if (typeof this.value === "object" && !Array.isArray(this.value)) { // Can be object if parsed to x2x representation - this.value = this.value.id || value || ""; - } - } - - if (!silent) this.trigger_up("domain_changed", {child: this}); - }, - /** - * Returns the mapping of "technical operator" to "display operator value" - * of the operators which are available for the given field type. - * - * @private - * @param {string} type - the field type - * @returns {Object} a map of all associated operators and their label - */ - _getOperatorsFromType: function (type) { - var operators = {}; - - switch (type) { - case "boolean": - operators = { - "=": _t("is"), - "!=": _t("is not"), - }; - break; - - case "char": - case "text": - case "html": - operators = pick(operator_mapping, "=", "!=", "ilike", "not ilike", "set", "not set", "in", "not in"); - break; - - case "many2many": - case "one2many": - case "many2one": - operators = pick(operator_mapping, "=", "!=", "ilike", "not ilike", "set", "not set"); - break; - - case "integer": - case "float": - case "monetary": - operators = pick(operator_mapping, "=", "!=", ">", "<", ">=", "<=", "ilike", "not ilike", "set", "not set"); - break; - - case "selection": - operators = pick(operator_mapping, "=", "!=", "set", "not set"); - break; - - case "date": - case "datetime": - operators = pick(operator_mapping, "=", "!=", ">", "<", ">=", "<=", "set", "not set"); - break; - - default: - operators = Object.assign({}, operator_mapping); - break; - } - - if (this.options.operators) { - operators = pick(operators, ...Object.keys(operators).concat(this.options.operators)); - } - - return operators; - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when the operator select value is change -> change the operator - * internal state and adapt - * - * @param {Event} e - */ - _onOperatorSelectChange: function (e) { - this._changeOperator($(e.currentTarget).val()); - }, - /** - * Called when the value input value is changed -> change the internal value - * state and adapt - * - * @param {Event} e - */ - _onValueInputChange: function (e) { - if (e.currentTarget !== e.target) return; - this._changeValue($(e.currentTarget).val()); - }, - /** - * Called when the field selector value is changed -> change the internal - * chain state and adapt - * - * @param {OdooEvent} e - */ - _onFieldChainChange: function (e) { - this._changeFieldChain(e.data.chain); - }, - - // TODO The two following functions should be in an independant widget - on_add_tag: function (e) { - if (e.type === "keyup" && e.which !== $.ui.keyCode.ENTER) return; - if (!["not in", "in"].includes(this.operator)) return; - - var values = Array.isArray(this.value) ? this.value.slice() : []; - - var $input = this.$(".o_domain_leaf_value_tags input"); - var val = $input.val().trim(); - if (val && values.indexOf(val) < 0) { - values.push(val); - setTimeout(() => this._changeValue.bind(this, values)); - $input.focus(); - } - }, - on_remove_tag: function (e) { - var values = Array.isArray(this.value) ? this.value.slice() : []; - var val = this.$(e.currentTarget).data("value"); - - var index = values.indexOf(val); - if (index >= 0) { - values.splice(index, 1); - setTimeout(() => this._changeValue.bind(this, values)); - } - }, -}); - -/** - * Instantiates a DomainTree if the given domain contains several parts and a - * DomainLeaf if it only contains one part. Returns null otherwise. - * - * @param {Object} parent - * @param {string} model - the model name - * @param {Array|string} domain - the prefix representation of the domain - * @param {Object} options - @see DomainNode.init.options - * @returns {DomainTree|DomainLeaf|null} - */ -function instantiateNode(parent, model, domain, options) { - if (domain.length > 1) { - return new DomainTree(parent, model, domain, options); - } else if (domain.length === 1) { - return new DomainLeaf(parent, model, domain, options); - } - return null; -} - -export default DomainSelector; diff --git a/addons/web/static/src/legacy/js/widgets/domain_selector_dialog.js b/addons/web/static/src/legacy/js/widgets/domain_selector_dialog.js deleted file mode 100644 index ab3566b8a20..00000000000 --- a/addons/web/static/src/legacy/js/widgets/domain_selector_dialog.js +++ /dev/null @@ -1,67 +0,0 @@ -/** @odoo-module alias=web.DomainSelectorDialog **/ - -import core from "web.core"; -import Dialog from "web.Dialog"; -import Domain from "web.Domain"; -import DomainSelector from "web.DomainSelector"; - -var _t = core._t; - -/** - * @class DomainSelectorDialog - */ -export default Dialog.extend({ - custom_events: Object.assign({}, Dialog.prototype.custom_events, { - domain_changed: "_onDomainChange", - }), - init: function (parent, model, domain, options) { - this.model = model; - this.newDomain = null; - this.options = Object.assign({ - readonly: true, - debugMode: false, - }, options || {}); - - var buttons; - if (this.options.readonly) { - buttons = [ - {text: _t("Close"), close: true}, - ]; - } else { - buttons = [ - {text: _t("Save"), classes: "btn-primary", close: true, click: function () { - this.trigger_up("domain_selected", { - domain: this.newDomain !== null ? this.newDomain : this.domainSelector.getDomain(), - }); - }}, - {text: _t("Discard"), close: true}, - ]; - } - - this._super(parent, Object.assign({}, { - title: _t("Domain"), - buttons: buttons, - }, options || {})); - this.domainSelector = new DomainSelector(this, model, Domain.prototype.arrayToString(domain), options); - }, - start: function () { - var self = this; - this.opened().then(function () { - // this restores default modal height (bootstrap) and allows field selector to overflow - self.$el.css('overflow', 'visible').closest('.modal-dialog').css('height', 'auto'); - }); - return Promise.all([ - this._super.apply(this, arguments), - this.domainSelector.appendTo(this.$el) - ]); - }, - /** - * Called when the domain selector value is changed. - * - * @param {OdooEvent} ev - */ - _onDomainChange: function (ev) { - ev.stopPropagation(); - this.newDomain = ev.data.domain; - }, -}); diff --git a/addons/web/static/src/legacy/js/widgets/model_field_selector.js b/addons/web/static/src/legacy/js/widgets/model_field_selector.js deleted file mode 100644 index 0b494f32a00..00000000000 --- a/addons/web/static/src/legacy/js/widgets/model_field_selector.js +++ /dev/null @@ -1,180 +0,0 @@ -/** @odoo-module alias=web.ModelFieldSelector **/ - -import core from "web.core"; -import Widget from "web.Widget"; -import ModelFieldSelectorPopover from "web.ModelFieldSelectorPopover"; - - /** - * The ModelFieldSelector widget can be used to display/select a particular - * field chain from a given model. - */ -var ModelFieldSelector = Widget.extend({ - template: "ModelFieldSelector", - events: {}, - edition_events: { - // Handle popover opening and closing - "focusin": "_onFocusIn", - "focusout": "_onFocusOut" - }, - custom_events: { - "field_selector_render": "_render" - }, - /** - * @constructor - * The ModelFieldSelector requires a model and a field chain to work with. - * - * @param parent - * @param {string} model - the model name (e.g. "res.partner") - * @param {string[]} chain - list of the initial field chain parts - * @param {Object} [options] - some key-value options - * @param {string} [options.order='string'] - * an ordering key for displayed fields - * @param {boolean} [options.readonly=true] - true if should be readonly - * @param {function} [options.filter] - * a function to filter the fetched fields - * @param {Object} [options.filters] - * some key-value options to filter the fetched fields - * @param {boolean} [options.filters.searchable=true] - * true if only the searchable fields have to be used - * @param {Object[]} [options.fields=null] - * the list of fields info to use when no relation has - * been followed (null indicates the widget has to request - * the fields itself) - * @param {boolean|function} [options.followRelations=true] - * true if can follow relation when building the chain - * @param {boolean} [options.showSearchInput=true] - * false to hide a search input to filter displayed fields - * @param {boolean} [options.needDefaultValue=false] - * true if a default value can be entered after the selected chain - * @param {boolean} [options.cancelOnEscape=false] - * true if a the chain selected should be ignored when the user hit ESC - * @param {boolean} [options.debugMode=false] - * true if the widget is in debug mode, false otherwise - */ - init: function (parent, model, chain, options) { - this._super.apply(this, arguments); - - this.model = model; - this.chain = chain; - this.options = Object.assign({ - order: 'string', - readonly: true, - fields: null, - debugMode: false - }, options || {}); - - this.dirty = false; - this.popover = new ModelFieldSelectorPopover(this, model, chain, options); - - if (!this.options.readonly) { - Object.assign(this.events, this.edition_events); - } - }, - /** - * @see Widget.willStart() - * @returns {Promise} - */ - willStart: function () { - return this._super.apply(this, arguments); - }, - /** - * @see Widget.start - * @returns {Promise} - */ - start: async function () { - this.$value = this.$(".o_field_selector_value"); - this.$valid = this.$(".o_field_selector_warning"); - - if (this.options.readonly) { - this.popover.prefill().then(this._render.bind(this)); - } - - await this._super.apply(this, arguments); - - if (!this.options.readonly) { - let popoverReadyPromiseResolve; - const popoverReadyPromise = new Promise((resolve) => popoverReadyPromiseResolve = resolve); - this.popover.on("field_chain_changed", undefined, (ev) => { - // Ensure the event target is the ModelFieldSelector. - ev.stopPropagation(); - this.trigger_up("field_chain_changed", ev.data); - }); - this.popover.appendTo($("
")); - this.popover.once("field_selector_started", undefined, () => { - this.$el.append(this.popover.$el); - this.$popover = this.$(".o_field_selector_popover"); - popoverReadyPromiseResolve(); - }); - await popoverReadyPromise; - } - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - /** - * Returns the field information selected by the field chain. - * - * @returns {Object} - */ - getSelectedField: function () { - return this.popover.getSelectedField(); - }, - /** - * Indicates if the field chain is valid. If the field chain has not been - * processed yet (the widget is not ready), this method will return - * undefined. - * - * @returns {boolean} - */ - isValid: function () { - return this.popover.valid; - }, - /** - * Saves a new field chain (array) and re-render. - * - * @param {string[]} chain - the new field chain - * @returns {Promise} resolved once the re-rendering is finished - */ - setChain: function (chain) { - return this.popover.setChain(chain).then(() => this.chain = this.popover.chain); - }, - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Updates the rendering of the value (the serie of tags separated by - * arrows). - * - * @private - */ - _render: function () { - // Render the chain value - this.chain = this.popover.chain; - this.$value.html(core.qweb.render(this.template + ".value", { - chain: this.popover.chain, - pages: this.popover.pages, - findByName : (array, name) => { - return (array || []).find((arr) => arr.name === name) - } - })); - - // Toggle the warning message - this.$valid.toggleClass('d-none', !!this.isValid()); - }, - /** - * Called when the widget is focused -> opens the popover - */ - _onFocusIn: function () { - this.popover._onFocusIn(); - }, - /** - * Called when the widget is blurred -> closes the popover - */ - _onFocusOut: function () { - this.popover._onFocusOut(); - }, -}); - -export default ModelFieldSelector; diff --git a/addons/web/static/src/legacy/js/widgets/model_field_selector_popover.js b/addons/web/static/src/legacy/js/widgets/model_field_selector_popover.js deleted file mode 100644 index 6e91cb72ee6..00000000000 --- a/addons/web/static/src/legacy/js/widgets/model_field_selector_popover.js +++ /dev/null @@ -1,734 +0,0 @@ -/** @odoo-module alias=web.ModelFieldSelectorPopover **/ - -import core from "web.core"; -import Widget from "web.Widget"; -import { fuzzyLookup } from "@web/core/utils/search"; -import { sortBy } from "@web/core/utils/arrays"; - -var _t = core._t; - -/** - * Field Selector Cache - TODO Should be improved to use external cache ? - * - Stores fields per model used in field selector - * @see ModelFieldSelector._getModelFieldsFromCache - */ -var modelFieldsCache = { - cache: {}, - cacheDefs: {}, -}; - -core.bus.on('clear_cache', null, function () { - modelFieldsCache.cache = {}; - modelFieldsCache.cacheDefs = {}; -}); -/** - * The ModelFieldSelector widget can be used to display/select a particular - * field chain from a given model. - */ -var ModelFieldSelectorPopover = Widget.extend({ - baseTemplate: "ModelFieldSelector", - template: "ModelFieldSelector.popover", - events: { - // Handle popover opening and closing - "focusin": "_onFocusIn", - "focusout": "_onFocusOut", - "click .o_field_selector_close": "_onCloseClick", - - // Handle popover field navigation - "click .o_field_selector_prev_page": "_onPrevPageClick", - "click .o_field_selector_next_page": "_onNextPageClick", - "click li.o_field_selector_select_button": "_onLastFieldClick", - - // Handle a direct change in the debug input - "change input.o_field_selector_debug": "_onDebugInputChange", - - // Handle a change in the search input - "keyup .o_field_selector_search > input": "_onSearchInputChange", - // Handle a change in the default value input - "keyup .o_field_selector_default_value_input > input": "_onDefaultValueChange", - - // Handle keyboard and mouse navigation to build the field chain - "mouseover li.o_field_selector_item": "_onItemHover", - "keydown": "_onKeydown", - }, - /** - * @constructor - * The ModelFieldSelector requires a model and a field chain to work with. - * - * @param parent - * @param {string} model - the model name (e.g. "res.partner") - * @param {string[]} chain - list of the initial field chain parts - * @param {Object} [options] - some key-value options - * @param {string} [options.order='string'] - * an ordering key for displayed fields - * @param {function} [options.filter] - * a function to filter the fetched fields - * @param {Object} [options.filters] - * some key-value options to filter the fetched fields - * @param {boolean} [options.filters.searchable=true] - * true if only the searchable fields have to be used - * @param {Object[]} [options.fields=null] - * the list of fields info to use when no relation has - * been followed (null indicates the widget has to request - * the fields itself) - * @param {boolean|function} [options.followRelations=true] - * true if can follow relation when building the chain - * @param {boolean} [options.showSearchInput=true] - * false to hide a search input to filter displayed fields - * @param {boolean} [options.needDefaultValue=false] - * true if a default value can be entered after the selected chain - * @param {boolean} [options.cancelOnEscape=false] - * true if a the chain selected should be ignored when the user hit ESC - * @param {boolean} [options.debugMode=false] - * true if the widget is in debug mode, false otherwise - */ - init: function (parent, model, chain, options) { - this._super.apply(this, arguments); - - this.model = model; - this.chain = chain; - this.options = Object.assign({ - order: 'string', - filters: {}, - fields: null, - filter: () => true, - followRelations: true, - debugMode: false, - showSearchInput: true, - needDefaultValue: false, - cancelOnEscape: false, - chainedTitle: false - }, options || {}); - this.options.filters = Object.assign({ - searchable: true, - }, this.options.filters); - - if (typeof this.options.followRelations !== 'function') { - this.options.followRelations = this.options.followRelations ? - (() => true) : - (() => false); - } - - this.pages = []; - this.titlesNames = {}; - this.dirty = false; - this.searchValue = ''; - this.defaultValue = ''; - }, - /** - * @see Widget.willStart() - * @returns {Promise} - */ - willStart: function () { - return Promise.all([ - this._super.apply(this, arguments), - this.prefill() - ]); - }, - /** - * @see Widget.start - * @returns {Promise} - */ - start: function () { - this.$input = this.$el.find(".o_field_selector_popover_footer > input"); - this.$searchInput = this.$el.find(".o_field_selector_search > input"); - this.$defaultValueInput = this.$el.find(".o_field_selector_default_value_input > input"); - - this._render(); - return this._super.apply(this, arguments).then(() => { - this.trigger_up("field_selector_started"); - }); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * Returns the field information selected by the field chain. - * - * @returns {Object} - */ - getSelectedField: function () { - return this.pages[this.chain.length - 1].find((page) => page.name === this.chain.at(-1)); - }, - /** - * Saves a new field chain (array) and re-render. - * - * @param {string[]} chain - the new field chain - * @returns {Promise} resolved once the re-rendering is finished - */ - setChain: function (chain) { - if (JSON.stringify(chain) === JSON.stringify(this.chain)) { - return Promise.resolve(); - } - - this.chain = chain; - return this.prefill().then(this._render.bind(this)); - }, - - /** - * Manualy open the popover and force the chain - * - * @param {string[]} chain - the new field chain - * @param {boolean} focus - force the focus in the search field - * @returns {Promise} resolved once the re-rendering is finished - */ - open: async function (chain = [], focus = false) { - clearTimeout(this._hidePopoverTimeout); - - this.$searchInput.val(''); - await this.setChain(chain); - this._showPopover(); - - if (focus) { - this.$searchInput.focus(); - } - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Adds a field name to the current field chain and marks it as dirty. - * - * @private - * @param {string} fieldName - the new field name to add at the end of the - * current field chain - */ - _addChainNode: function (fieldName) { - this.dirty = true; - this.chain = this.chain.slice(0, this.pages.length - 1); - this.chain.push(fieldName); - - this.searchValue = ''; - this.$searchInput.val(''); - }, - /** - * Searches a field in the last page by its name. - * - * @private - * @param {string} name - the name of the field to find - * @returns {Object} the field data found in the last popover page thanks - * to its name - /*/ - _getLastPageField: function (name) { - return this.pages.at(-1).find((page) => page.name === name); - }, - /** - * Searches the cache for the given model fields, according to the given - * filter. If the cache does not know about the model, the cache is updated. - * - * @private - * @param {string} model - * @param {Object} filters @see ModelFieldSelector.init.options.filters - * @returns {Object[]} a list of the model fields info, sorted by field - * non-technical names - */ - _getModelFieldsFromCache: function (model, filters) { - var self = this; - var def = modelFieldsCache.cacheDefs[model]; - if (!def) { - def = modelFieldsCache.cacheDefs[model] = this._rpc({ - model: model, - method: 'fields_get', - args: [ - false, - ["store", "searchable", "type", "string", "relation", "selection", "related"] - ], - context: this.getSession().user_context, - }) - .then((function (fields) { - modelFieldsCache.cache[model] = sortFields(fields, model, self.options.order); - }).bind(this)); - } - return def.then((function () { - return modelFieldsCache.cache[model].filter(f => { - return (!filters.searchable || f.searchable) && self.options.filter(f); - }); - }).bind(this)); - }, - /** - * Return true if the widget is currently open on the default value page - * - * @private - * @returns {Boolean} Is on default value page - */ - _isOnDefaultValuePage: function () { - return this.pages[this.pages.length - 1] === 'defaultValuePage'; - }, - /** - * Adds a new page to the popover following the given field relation and - * show the defauld value choice page - * - * @private - * @param {String} name - the name of the field to add to the chain node - */ - _goToDefaultValuePage: function (name) { - this._validate(true); - - this._addChainNode(name); - this.pages.push('defaultValuePage'); - this.$defaultValueInput.val(''); - this.defaultValue = ''; - - this._renderDefaultValue(); - }, - /** - * Adds a new page to the popover following the given field relation and - * adapts the chain node according to this given field. - * - * @private - * @param {Object} field - the field to add to the chain node - */ - _goToNextPage: function (field) { - if (JSON.stringify(this._getLastPageField(field.name)) !== JSON.stringify(field)) return; - - this._validate(true); - this._addChainNode(field.name); - this._pushPageData(field.relation).then(this._render.bind(this)); - }, - /** - * Removes the last page, adapts the field chain and displays the new - * last page. - * - * @private - */ - _goToPrevPage: function () { - if (this.pages.length <= 0) return; - - this._validate(true); - this._removeChainNode(); - if (this.pages.length > 1) { - this.pages.pop(); - } - this._render(); - }, - /** - * Closes the popover and marks the field as selected. If the field chain - * changed, it notifies its parents. If not open, this does nothing. - * - * @private - * @param {boolean} cancel - prevent the triggerUp of the `field_chain_changed` event - */ - _hidePopover: function (cancel = false) { - if (!this._isOpen) return; - - this._isOpen = false; - this.$searchInput.parent().removeClass('d-none'); - this.$defaultValueInput.parent().addClass('d-none'); - this.$el.addClass('d-none'); - - if (cancel) { - this.trigger_up("field_chain_cancel"); - } else if (this.dirty) { - this.trigger_up("field_chain_changed", {chain: this.chain, defaultValue: this.defaultValue}); - } - this.dirty = false; - this.defaultValue = ''; - }, - /** - * Prepares the popover by filling its pages according to the current field - * chain. - * - * @private - * @returns {Promise} resolved once the whole field chain has been - * processed - */ - prefill: function () { - this.pages = []; - return this._pushPageData(this.model).then((function () { - this._validate(true); - return (this.chain.length ? processChain.call(this, this.chain.slice().reverse()) : Promise.resolve()); - }).bind(this)); - - function processChain(chain) { - var fieldName = chain.pop(); - var field = this._getLastPageField(fieldName); - if (field && field.relation) { - if (chain.length) { // Fetch next chain node if any and possible - return this._pushPageData(field.relation).then(processChain.bind(this, chain)); - } else { // Simply update the last popover page - return this._pushPageData(field.relation); - } - } else if (field && chain.length === 0) { // Last node fetched - return Promise.resolve(); - } else if (!field && fieldName === "1") { // TRUE_LEAF - this._validate(true); - } else if (!field && fieldName === "0") { // FALSE_LEAF - this._validate(true); - } else { // Wrong node chain - this._validate(false); - } - return Promise.resolve(); - } - }, - /** - * Gets the fields of a particular model and adds them to a new last - * popover page. - * - * @private - * @param {string} model - the model name whose fields have to be fetched - * @returns {Promise} resolved once the fields have been added - */ - _pushPageData: function (model) { - var def; - if (this.model === model && this.options.fields) { - def = Promise.resolve(sortFields(this.options.fields, model, this.options.order)); - } else { - def = this._getModelFieldsFromCache(model, this.options.filters); - } - return def.then((function (fields) { - this.pages.push(fields); - }).bind(this)); - }, - /** - * Removes the last field name at the end of the current field chain and - * marks it as dirty. - * - * @private - */ - _removeChainNode: function () { - this.dirty = true; - this.chain = this.chain.slice(0, this.pages.length - 1); - this.chain.pop(); - }, - /** - * Get the current value for the popover header - * - * @private - */ - _getTitle: function () { - var title = ""; - if (this.pages.length > 1) { - var prevField = this.pages[this.pages.length - 2].find((page) => { - return page.name === (this.chain.length === this.pages.length) - ? this.chain[this.chain.length - 2] - : this.chain.at(-1); - }); - if (prevField) { - this.titlesNames[this.chain.at(-1)] = prevField.string; - title = prevField.string; - } - } - if (this.options.chainedTitle) { - title = this.chain.map((chain) => this.titlesNames[chain]).join(' > '); - } - - return title; - }, - /** - * Adapts the content of the popover. - * - * @private - */ - _render: function () { - this._adaptInputVisibility(); - - // Adapt the popover content - var page = this.pages.at(-1); - this.$(".o_field_selector_popover_header .o_field_selector_title").text(this._getTitle()); - - var lines = page.filter((item) => { - return Object.keys(this.options.filter).every((key) => { - return item[key] === this.options.filter[key]; - }); - }); - - if (this.searchValue) { - lines = fuzzyLookup(this.searchValue, lines, (l) => l.string); - } - - this.$(".o_field_selector_page").replaceWith(core.qweb.render(this.baseTemplate + ".page", { - lines: lines, - followRelations: this.options.followRelations, - debug: this.options.debugMode, - })); - this.$input.val(this.chain.join(".")); - - this.trigger_up("field_selector_render"); - }, - /** - * Adapts the content of the popover with the default value page. - * - * @private - */ - _renderDefaultValue: function () { - this._adaptInputVisibility(); - this.$defaultValueInput.focus(); - - // Adapt the popover content - let title = this._getTitle() + '
' + _t("Default value"); - this.$(".o_field_selector_popover_header .o_field_selector_title")[0].replaceChildren( - ...$('
' + title + '
')[0].childNodes - ); - - this.$(".o_field_selector_page").replaceWith(core.qweb.render(this.baseTemplate + ".defaultValue", { - line: { - string: '', - description: this.defaultValue.length ? _t("As a default text when no value are set") : _t("Default text is used when no values are set"), - } - })); - this.$input.val(this.chain.join(".") + (this.defaultValue ? " OR " + this.defaultValue : "")); - - this.trigger_up("field_selector_render"); - }, - /** - * Adapt the visibilitty of the search and default view input. - * - * @private - */ - _adaptInputVisibility() { - this.$searchInput.parent().toggleClass("d-none", this._isOnDefaultValuePage()); - this.$defaultValueInput.parent().toggleClass("d-none", !this._isOnDefaultValuePage()); - }, - /** - * Selects the given field and adapts the chain node according to it. - * It also closes the popover and so notifies the parents about the change. - * - * @param {Object} field - the field to select - */ - _selectField: function (field) { - if (JSON.stringify(this._getLastPageField(field.name)) !== JSON.stringify(field)) return; - - this._validate(true); - this._addChainNode(field.name); - this._render(); - this._hidePopover(); - }, - /** - * Shows the popover to select the field chain. This assumes that the - * popover has finished its rendering (fully rendered widget or resolved - * deferred of @see setChain). If already open, this does nothing. - * - * @private - */ - _showPopover: function () { - if (this._isOpen) return; - - this._isOpen = true; - this.$el.removeClass('d-none'); - }, - /** - * Toggles the valid status of the widget and display the error message if - * it is not valid. - * - * @private - * @param {boolean} valid - true if the widget is valid, false otherwise - */ - _validate: function (valid) { - this.valid = !!valid; - - if (!this.valid) { - this.displayNotification({ - message: _t("Invalid field chain. You may have used a non-existing field name or followed a non-relational field."), - type: 'danger', - }); - } - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when the widget is focused -> opens the popover - */ - _onFocusIn: function () { - clearTimeout(this._hidePopoverTimeout); - this._showPopover(); - }, - /** - * Called when the widget is blurred -> closes the popover - */ - _onFocusOut: function () { - clearTimeout(this._hidePopoverTimeout); - this._hidePopoverTimeout = setTimeout(() => this._hidePopover(this.options.cancelOnEscape)); - }, - /** - * Called when the popover "cross" icon is clicked -> closes the popover - */ - _onCloseClick: function () { - this._hidePopover(this.options.cancelOnEscape); - }, - /** - * Called when the popover "previous" icon is clicked -> removes last chain - * node - */ - _onPrevPageClick: function () { - this._goToPrevPage(); - }, - /** - * Called when a popover relation field button is clicked -> adds it to - * the chain - * - * @param {Event} e - */ - _onNextPageClick: function (e) { - e.stopPropagation(); - this._goToNextPage(this._getLastPageField($(e.currentTarget).data("name"))); - }, - /** - * Called when a popover non-relation field button is clicked -> adds it to - * chain and closes the popover - * - * @param {Event} e - */ - _onLastFieldClick: function (e) { - const name = $(e.currentTarget).data("name"); - if (this.options.needDefaultValue && this._isOnDefaultValuePage()) { - this._hidePopover(); - } else if (this.options.needDefaultValue) { - this._goToDefaultValuePage(name); - } else { - this._selectField(this._getLastPageField(name)); - } - }, - /** - * Called when the debug input value is changed -> adapts the chain - */ - _onDebugInputChange: function () { - var userChainStr = this.$input.val(); - var userChain = userChainStr.split("."); - if (!this.options.followRelations && userChain.length > 1) { - this.displayNotification({ - title: _t("Relation not allowed"), - message: _t("You cannot follow relations for this field chain construction"), - type: 'danger', - }); - userChain = [userChain[0]]; - } - this.setChain(userChain).then((function () { - this.trigger_up("field_chain_changed", {chain: this.chain}); - }).bind(this)); - }, - /** - * Called when the search input value is changed -> adapts the popover - */ - _onSearchInputChange: function () { - this.searchValue = this.$searchInput.val(); - this._render(); - }, - /** - * Called when the default value input value is changed -> adapts the popover - */ - _onDefaultValueChange: function () { - this.defaultValue = this.$defaultValueInput.val(); - if (this._isOnDefaultValuePage()) { - this._renderDefaultValue(); - } - }, - /** - * Called when a popover field button item is hovered -> toggles its - * "active" status - * - * @param {Event} e - */ - _onItemHover: function (e) { - this.$("li.o_field_selector_item").removeClass("active"); - $(e.currentTarget).addClass("active"); - }, - /** - * Called when the user uses the keyboard when the widget is focused - * -> handles field keyboard navigation - * - * @param {Event} e - */ - _onKeydown: function (e) { - if (!this.$el.is(":visible")) return; - var inputHasFocus = this.$input.is(":focus"); - let name = this.$("li.o_field_selector_item.active").data("name"); - - switch (e.which) { - case $.ui.keyCode.UP: - case $.ui.keyCode.DOWN: - e.preventDefault(); - var $active = this.$("li.o_field_selector_item.active"); - var $to = $active[e.which === $.ui.keyCode.DOWN ? "next" : "prev"](".o_field_selector_item"); - if ($to.length) { - $active.removeClass("active"); - $to.addClass("active"); - this.$el.focus(); - - var $page = $to.closest(".o_field_selector_page"); - var full_height = $page.height(); - var el_position = $to.position().top; - var el_height = $to.outerHeight(); - var current_scroll = $page.scrollTop(); - if (el_position < 0) { - $page.scrollTop(current_scroll - el_height); - } else if (full_height < el_position + el_height) { - $page.scrollTop(current_scroll + el_height); - } - } - break; - case $.ui.keyCode.RIGHT: - if (inputHasFocus) break; - e.preventDefault(); - if (name) { - var field = this._getLastPageField(name); - if (field.relation) { - this._goToNextPage(field); - } else if (this.options.needDefaultValue && !this._isOnDefaultValuePage()) { - this._goToDefaultValuePage(name); - } - } - break; - case $.ui.keyCode.LEFT: { - if (inputHasFocus) break; - e.preventDefault(); - const isOnDefaultValuePage = this._isOnDefaultValuePage(); - this._goToPrevPage(); - if (isOnDefaultValuePage) { - this.$searchInput.focus(); - } - break; - } - case $.ui.keyCode.ESCAPE: - e.stopPropagation(); - this._hidePopover(this.options.cancelOnEscape); - break; - case $.ui.keyCode.ENTER: - e.preventDefault(); - if (!name && !this._isOnDefaultValuePage()) { - this._hidePopover(this.options.cancelOnEscape); - break; - } - if (this.options.needDefaultValue && this._isOnDefaultValuePage()) { - this._hidePopover(); - } else if (this.options.needDefaultValue) { - this._goToDefaultValuePage(name); - } else { - this._selectField(this._getLastPageField(name)); - } - break; - } - } -}); - -export default ModelFieldSelectorPopover; - -/** - * Allows to transform a mapping field name -> field info in an array of the - * field infos, sorted by field user name ("string" value). The field infos in - * the final array contain an additional key "name" with the field name. - * - * @param {Object} fields - the mapping field name -> field info - * @param {string} model - IE : "partner" - * @returns {Object[]} the field infos sorted by field "string" (field infos - * contain additional keys "model" and "name" with the field - * name) - */ -function sortFields(fields, model, order) { - const fieldsArray = Object.entries(fields).map(([key, value]) => { - return Object.assign( - { - name: key, - model: model, - }, - value - ); - }); - return sortBy(fieldsArray, (field) => field[order]); -} diff --git a/addons/web/static/src/legacy/legacy_client_actions.js b/addons/web/static/src/legacy/legacy_client_actions.js deleted file mode 100644 index d2a48c5f2dc..00000000000 --- a/addons/web/static/src/legacy/legacy_client_actions.js +++ /dev/null @@ -1,84 +0,0 @@ -/** @odoo-module **/ - -import { registry } from "@web/core/registry"; -import { useService } from "@web/core/utils/hooks"; -import { action_registry as legacyActionRegistry } from "web.core"; -import Widget from "web.Widget"; -import { useSetupAction } from "../webclient/actions/action_hook"; -import { ClientActionAdapter } from "./action_adapters"; -import { breadcrumbsToLegacy } from "./backend_utils"; -import { useLegacyRefs } from "./utils"; -import { LegacyComponent } from "./legacy_component"; - -import { xml } from "@odoo/owl"; -const actionRegistry = registry.category("actions"); - -const legacyClientActionTemplate = xml` - `; - -// registers an action from the legacy action registry to the wowl one, ensuring -// that widget actions are actually Components -function registerClientAction(name, action) { - if (action.prototype instanceof Widget) { - // the action is a widget, wrap it into a Component and register that component - class Action extends LegacyComponent { - setup() { - this.Widget = action; - const options = {}; - for (const key in this.props) { - if (key === "action" || key === "actionId") { - continue; - } else { - options[key] = this.props[key]; - } - } - - const legacyRefs = useLegacyRefs(); - - if (this.env.config.breadcrumbs) { - options.breadcrumbs = breadcrumbsToLegacy(this.env.config.breadcrumbs); - } - - // always add user context to the action context - this.user = useService("user"); - const clientAction = Object.assign({}, this.props.action, { - context: Object.assign({}, this.user.context, this.props.action.context), - }); - - this.widgetArgs = [clientAction, options]; - this.widget = this.props.state && this.props.state.__legacy_widget__; - this.onReverseBreadcrumb = - this.props.state && this.props.state.__on_reverse_breadcrumb__; - - const rootRef = { - get el() { - return legacyRefs.widget && legacyRefs.widget.el; - }, - }; - - useSetupAction({ - beforeLeave: () => legacyRefs.widget.canBeRemoved(), - getLocalState: () => legacyRefs.component.exportState(), - rootRef, - }); - } - } - Action.template = legacyClientActionTemplate; - Action.components = { ClientActionAdapter }; - Action.isLegacy = true; - Action.target = action.prototype.target; - actionRegistry.add(name, Action); - } else { - // the action is either a Component or a function, register it directly - actionRegistry.add(name, action); - } -} - -// register action already in the legacy registry, and listens to future registrations -for (const [name, action] of Object.entries(legacyActionRegistry.entries())) { - if (!actionRegistry.contains(name)) { - registerClientAction(name, action); - } -} -legacyActionRegistry.onAdd(registerClientAction); diff --git a/addons/web/static/src/legacy/scss/domain_selector.scss b/addons/web/static/src/legacy/scss/domain_selector.scss deleted file mode 100644 index e9f7c59d83e..00000000000 --- a/addons/web/static/src/legacy/scss/domain_selector.scss +++ /dev/null @@ -1,185 +0,0 @@ - -.o_domain_selector_legacy { - - &.o_domain_node, .o_domain_node { - $o-domain-selector-indent: 32px; - $o-domain-selector-panel-space: 60px; - $o-domain-selector-row-height: 35px; // should be greater than a "normal" row so that "header" parts are the same size of rows - $o-domain-animation-bar-height: 15px; - - position: relative; - - // Rows in a domain node (selector) have a minimum height and holds the control panel - .o_domain_selector_row { - display: flex; - align-items: center; - min-height: $o-domain-selector-row-height; - - > * { - flex: 0 0 auto; - } - - > .o_domain_node_control_panel { - @include o-position-absolute($top: 0, $bottom: 0, $right: -$o-domain-selector-panel-space); - - > button { - float: left; - width: $o-domain-selector-panel-space/2; - height: 100%; - padding: 0; - background-color: transparent; - font-size: 16px; - opacity: 0.2; - cursor: pointer; - - &.o_domain_delete_node_button { - font-size: 12px; - padding-left: 5px; - } - } - } - } - - // Rules specific to domain parts which contain sub-domain parts - &.o_domain_tree { - > .o_domain_tree_header { - position: relative; - } - .o_domain_tree_operator_caret::after { - @include o-caret-down; - } - - & .o_domain_node_children_container { - padding-left: $o-domain-selector-indent; - } - - // Rules specific to domain root - &.o_domain_selector { - > .o_domain_tree_header { - min-height: 22px; // prevents "records button" to overflow 1-row domain - } - - > .o_domain_node_children_container { - padding-left: 0; - } - - &.o_edit_mode > .o_domain_node_children_container { - padding-right: $o-domain-selector-panel-space; // delete button is over the row - } - - > .o_domain_debug_container { - display: block; - margin-top: 16px; - padding: 8px 10px 12px; - background: var(--DomainDebugContainer-background-colomap-get, #{$o-gray-900}); - color: rgba(white, 0.5); - font-family: monospace; - font-weight: normal; - - > textarea { - border: none; - padding-top: 8px; - background: transparent; - color: white; - } - } - } - } - - // Rules specific to domain parts which do not contain sub-domain parts - &.o_domain_leaf { - &.o_read_mode { - display: inline-flex; - margin-right: 4px; - } - - > .o_domain_leaf_info { - background: $o-brand-lightsecondary; - border: 1px solid darken($o-brand-lightsecondary, 10%); - padding: 2px 4px; - - > .o_field_selector { - float: left; - margin-right: 4px; - - > .o_field_selector_value > .o_field_selector_chain_part { - border: none; - font-weight: 700; - } - } - .o_domain_leaf_operator { - font-style: italic; - } - .o_domain_leaf_value { - font-weight: 700; - } - } - - > .o_domain_leaf_edition { - display: flex; - width: 100%; - padding: 4px ($o-domain-selector-panel-space/2) 4px 0; - - > * { - flex: 1 1 15%; // operator select - width: auto; - background-color: white; - - &:first-child { // field selector - flex: 0 1 55%; - } - - &:last-child { // field value - flex: 1 1 25%; - } - - + * { - margin-left: 4px; - } - - input, select, .o_datepicker, .o_datepicker_input { - height: 100%; - } - } - - .o_domain_leaf_value_tags { - display: flex; - - > * { - flex: 0 0 auto; - } - > input { - flex: 1 1 auto; - width: 0; - min-width: 50px; - } - .o_domain_leaf_value_remove_tag_button { - cursor: pointer; - } - } - } - } - - // Animation effects - transition: margin-bottom .05s ease .05s; - - &::after { - content: ""; - @include o-position-absolute($left: 0, $right: 0, $top: 100%); - display: block; - height: $o-domain-animation-bar-height; - max-height: 0; - background-image: linear-gradient(45deg, rgba($o-brand-primary, 0.4), rgba($o-brand-primary, 0.1)); - transition: max-height .05s ease .05s; - } - - &.o_hover_btns, &.o_hover_add_node { - .o_domain_node_control_panel > button { - opacity: 0.5; - &:hover { - opacity: 1.0; - } - } - } - } -} diff --git a/addons/web/static/src/legacy/xml/base.xml b/addons/web/static/src/legacy/xml/base.xml index 5d2467b1eb5..60693c4f326 100644 --- a/addons/web/static/src/legacy/xml/base.xml +++ b/addons/web/static/src/legacy/xml/base.xml @@ -3,80 +3,6 @@ - -
-
-
- -
-
- -
-
-
-
- - -
-
- - - -
-
-
-
- - - - - - - - - - - -