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 @@ - -
-
-
- -
-
- -
-
-
-
- - -
-
- - - -
-
-
-
- - - - - - - - - - - -